feat(kernel): finish phase3 bridge cutover and phase4 tree projection
- add generic mnote-web query transport and bridge routes for workspace/request/trace - route sidebar compat traffic through mnote-web and tighten fixture fallback to dev/test - unify sidebar/page tree/file tree/picker consumers on page_tree projection - sync phase3/phase4 checklist, breakdown docs, and harness progress state
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
@@ -17,11 +17,11 @@ import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual";
|
||||
import { ChevronRight, GripVertical, MoreHorizontal, Plus } from "lucide-react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { flattenDocumentTree } from "@/lib/sidebar-tree";
|
||||
|
||||
interface PrivateTreeProps {
|
||||
nodes: SidebarTreeNode[];
|
||||
rows: PageTreeProjectionItem[];
|
||||
expanded: Set<string>;
|
||||
activeId: string;
|
||||
onToggleExpand: (id: string) => void;
|
||||
@@ -33,7 +33,7 @@ interface PrivateTreeProps {
|
||||
const ROW_HEIGHT = 36;
|
||||
|
||||
export function PrivateTree({
|
||||
nodes,
|
||||
rows,
|
||||
expanded,
|
||||
activeId,
|
||||
onToggleExpand,
|
||||
@@ -44,18 +44,6 @@ export function PrivateTree({
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const [activeDragId, setActiveDragId] = useState<string | null>(null);
|
||||
|
||||
const flatNodes = useMemo(() => {
|
||||
const flattened = flattenDocumentTree(nodes, expanded);
|
||||
const seen = new Set<string>();
|
||||
const deduped: typeof flattened = [];
|
||||
for (const item of flattened) {
|
||||
if (seen.has(item.node.id)) continue;
|
||||
seen.add(item.node.id);
|
||||
deduped.push(item);
|
||||
}
|
||||
return deduped;
|
||||
}, [nodes, expanded]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 5 },
|
||||
@@ -64,7 +52,7 @@ export function PrivateTree({
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flatNodes.length,
|
||||
count: rows.length,
|
||||
getScrollElement: () => scrollAreaRef.current,
|
||||
estimateSize: () => ROW_HEIGHT,
|
||||
overscan: 10,
|
||||
@@ -82,25 +70,25 @@ export function PrivateTree({
|
||||
if (!overId || activeId === overId) {
|
||||
return;
|
||||
}
|
||||
const activeIndex = flatNodes.findIndex((item) => item.node.id === activeId);
|
||||
const overIndex = flatNodes.findIndex((item) => item.node.id === overId);
|
||||
const activeIndex = rows.findIndex((item) => item.nodeId === activeId);
|
||||
const overIndex = rows.findIndex((item) => item.nodeId === overId);
|
||||
if (activeIndex === -1 || overIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const targetParent = flatNodes[overIndex].parentId;
|
||||
const siblingList = flatNodes.filter((item) => item.parentId === targetParent);
|
||||
const siblingIndex = siblingList.findIndex((item) => item.node.id === overId);
|
||||
const targetParent = rows[overIndex].parentNodeId;
|
||||
const siblingList = rows.filter((item) => item.parentNodeId === targetParent);
|
||||
const siblingIndex = siblingList.findIndex((item) => item.nodeId === overId);
|
||||
const position = siblingIndex === -1 ? siblingList.length : siblingIndex;
|
||||
onMove(activeId, targetParent, position);
|
||||
},
|
||||
[flatNodes, onMove],
|
||||
[onMove, rows],
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveDragId(null);
|
||||
}, []);
|
||||
|
||||
if (flatNodes.length === 0) {
|
||||
if (rows.length === 0) {
|
||||
return <div className="px-4 py-6 text-sm text-gray-400">暂无页面,点击下方按钮创建</div>;
|
||||
}
|
||||
|
||||
@@ -114,22 +102,22 @@ export function PrivateTree({
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext items={flatNodes.map((item) => item.node.id)} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext items={rows.map((item) => item.nodeId)} strategy={verticalListSortingStrategy}>
|
||||
<div className="relative h-full">
|
||||
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const item = flatNodes[virtualRow.index];
|
||||
const item = rows[virtualRow.index];
|
||||
return (
|
||||
<VirtualRow key={item.node.id} virtualRow={virtualRow}>
|
||||
<VirtualRow key={item.nodeId} virtualRow={virtualRow}>
|
||||
<SortableTreeRow
|
||||
node={item.node}
|
||||
depth={item.depth}
|
||||
expanded={expanded.has(item.node.id)}
|
||||
hasChildren={item.node.children.length > 0}
|
||||
expanded={expanded.has(item.nodeId)}
|
||||
hasChildren={item.childCount > 0}
|
||||
activeId={activeId}
|
||||
isDragging={activeDragId === item.node.id}
|
||||
onToggleExpand={() => onToggleExpand(item.node.id)}
|
||||
onCreateChild={() => onCreateChild(item.node.id)}
|
||||
isDragging={activeDragId === item.nodeId}
|
||||
onToggleExpand={() => onToggleExpand(item.nodeId)}
|
||||
onCreateChild={() => onCreateChild(item.nodeId)}
|
||||
onContextMenu={(event) => onContextMenu(event, item.node)}
|
||||
/>
|
||||
</VirtualRow>
|
||||
|
||||
@@ -38,7 +38,11 @@ import { useSidebarStore } from "@/store/sidebar";
|
||||
import type { SidebarInitialData, SidebarSectionId } from "@/components/sidebar/types";
|
||||
import { useSidebarData } from "@/hooks/use-sidebar-data";
|
||||
import { PrivateTree } from "@/components/sidebar/private-tree";
|
||||
import { buildSidebarSectionsFromTree, flattenDocumentTree } from "@/lib/sidebar-tree";
|
||||
import { buildSidebarSectionsFromTree } from "@/lib/sidebar-tree";
|
||||
import {
|
||||
buildPageTreeProjectionItems,
|
||||
filterVisiblePageTreeProjectionItems,
|
||||
} from "@/lib/tree-projection";
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
import { useEditorBridgeStore } from "@/store/editor-bridge";
|
||||
import { useCurrentDocumentStore } from "@/store/current-document";
|
||||
@@ -172,7 +176,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
const activeId = segments?.[1] ?? "";
|
||||
const editorBridge = useEditorBridgeStore((state) => state.bridge);
|
||||
|
||||
const [tree, setTree] = useState<SidebarTreeNode[]>(() => sidebarData.kernelSidebarTree ?? []);
|
||||
const [tree, setTree] = useState<SidebarTreeNode[]>(() => sidebarData.kernelSidebarTree);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => collectNodeIds(tree));
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
@@ -243,7 +247,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setTree(() => {
|
||||
const nextTree = sidebarData.kernelSidebarTree ?? [];
|
||||
const nextTree = sidebarData.kernelSidebarTree;
|
||||
setExpanded((expandedPrev) => collectNodeIds(nextTree, new Set(expandedPrev)));
|
||||
return nextTree;
|
||||
});
|
||||
@@ -468,9 +472,21 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
() => (filter ? filterTree(privateTree, filter.toLowerCase()) : privateTree),
|
||||
[filter, privateTree],
|
||||
);
|
||||
const privatePageRows = useMemo(
|
||||
() => buildPageTreeProjectionItems(privateTree),
|
||||
[privateTree],
|
||||
);
|
||||
const filteredPrivatePageRows = useMemo(
|
||||
() => buildPageTreeProjectionItems(filteredPrivateTree),
|
||||
[filteredPrivateTree],
|
||||
);
|
||||
const flattenedPrivate = useMemo(
|
||||
() => flattenDocumentTree(privateTree, expanded),
|
||||
[privateTree, expanded],
|
||||
() => filterVisiblePageTreeProjectionItems(privatePageRows, expanded),
|
||||
[expanded, privatePageRows],
|
||||
);
|
||||
const visibleFilteredPrivatePageRows = useMemo(
|
||||
() => filterVisiblePageTreeProjectionItems(filteredPrivatePageRows, expanded),
|
||||
[expanded, filteredPrivatePageRows],
|
||||
);
|
||||
const filteredTrash = useMemo(() => {
|
||||
const keyword = trashSearch.trim().toLowerCase();
|
||||
@@ -574,13 +590,19 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
const fileTreeRows = useMemo(
|
||||
() =>
|
||||
buildVisibleRows({
|
||||
nodes: filteredPrivateTree,
|
||||
pageRows: visibleFilteredPrivatePageRows,
|
||||
expanded,
|
||||
assetsByDoc,
|
||||
assetChildrenByAssetId: mindmapChildrenSnapshot.childAssetsByMindmapId,
|
||||
expandedAssetFolderIds: expandedAssetFolders,
|
||||
}),
|
||||
[assetsByDoc, expanded, expandedAssetFolders, filteredPrivateTree, mindmapChildrenSnapshot.childAssetsByMindmapId],
|
||||
[
|
||||
assetsByDoc,
|
||||
expanded,
|
||||
expandedAssetFolders,
|
||||
mindmapChildrenSnapshot.childAssetsByMindmapId,
|
||||
visibleFilteredPrivatePageRows,
|
||||
],
|
||||
);
|
||||
|
||||
const fileTreeVisibleRowIds = useMemo(() => fileTreeRows.map((row) => row.rowId), [fileTreeRows]);
|
||||
@@ -1816,12 +1838,12 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
const handleConvertToChild = useCallback(
|
||||
async (documentId: string) => {
|
||||
const flat = flattenedPrivate;
|
||||
const currentIndex = flat.findIndex((item) => item.node.id === documentId);
|
||||
const currentIndex = flat.findIndex((item) => item.nodeId === documentId);
|
||||
if (currentIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
const targetParentId = flat[currentIndex - 1].node.id;
|
||||
await handleMove(documentId, targetParentId, flat[currentIndex - 1].node.children.length);
|
||||
const targetParentId = flat[currentIndex - 1].nodeId;
|
||||
await handleMove(documentId, targetParentId, flat[currentIndex - 1].childCount);
|
||||
},
|
||||
[flattenedPrivate, handleMove],
|
||||
);
|
||||
@@ -2271,20 +2293,20 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
<div className="max-h-52 overflow-auto rounded-md border border-[#eff2f6] bg-white px-2 py-2">
|
||||
{(() => {
|
||||
const renderList = (nodes: SidebarTreeNode[]) => {
|
||||
const flat = flattenDocumentTree(nodes, collectNodeIds(nodes));
|
||||
const flat = buildPageTreeProjectionItems(nodes);
|
||||
if (flat.length === 0) {
|
||||
return <div className="px-2 py-2 text-xs text-gray-400">暂无内容</div>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{flat.map(({ node, depth }) => (
|
||||
{flat.map((item) => (
|
||||
<Link
|
||||
key={node.id}
|
||||
href={`/documents/${node.id}`}
|
||||
key={item.nodeId}
|
||||
href={`/documents/${item.nodeId}`}
|
||||
className="block truncate rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-[#f5f7fb] hover:text-[#2563eb]"
|
||||
style={{ paddingLeft: 8 + depth * 12 }}
|
||||
style={{ paddingLeft: 8 + item.depth * 12 }}
|
||||
>
|
||||
{node.title || "无标题"}
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -2499,7 +2521,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
|
||||
<div className="flex-1 px-1 pb-2">
|
||||
<div className="h-full rounded-md border border-[#eff2f6] bg-white">
|
||||
<PrivateTree
|
||||
nodes={filteredPrivateTree}
|
||||
rows={visibleFilteredPrivatePageRows}
|
||||
expanded={expanded}
|
||||
activeId={activeId}
|
||||
onToggleExpand={toggleExpand}
|
||||
|
||||
@@ -17,8 +17,8 @@ export interface SidebarInitialData {
|
||||
activeWorkspaceId: string;
|
||||
workspaces: WorkspaceSummary[];
|
||||
documents: DocumentRecord[];
|
||||
kernelSidebarProjection?: KernelSidebarProjection | null;
|
||||
kernelSidebarTree?: SidebarTreeNode[];
|
||||
kernelSidebarProjection: KernelSidebarProjection;
|
||||
kernelSidebarTree: SidebarTreeNode[];
|
||||
trashedDocuments: TrashRecord[];
|
||||
trashedMediaAssets?: MediaAsset[];
|
||||
trashedMindmapAssets?: MediaAsset[];
|
||||
|
||||
Reference in New Issue
Block a user