feat: continue tree rust family cutover
- add rust renderer/state-family scaffolds and inline compat host thinning for page tree, file tree, and picker - route tree/filetree preflight, file projection, resource artifact, and stream delta contracts through rust plans - preserve canonical move-order validation, file-tree search projection, and related frontend/runtime regression coverage
This commit is contained in:
@@ -458,7 +458,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
|
||||
const emptySurface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
expect(emptySurface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-host"]'),
|
||||
).not.toBeNull();
|
||||
@@ -494,7 +494,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
|
||||
const resultSurface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
expect(resultSurface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-host"]'),
|
||||
).not.toBeNull();
|
||||
@@ -658,7 +658,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
|
||||
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SIDEBAR_SOURCE = path.join(process.cwd(), "src/components/sidebar/sidebar.tsx");
|
||||
|
||||
describe("sidebar file tree delete preflight source", () => {
|
||||
it("rust_family 删除链应走 Rust delete preflight,而不是本地 delete target helper", () => {
|
||||
const source = fs.readFileSync(SIDEBAR_SOURCE, "utf8");
|
||||
|
||||
expect(source).toContain("preflightFileTreeDelete(");
|
||||
expect(source).toContain("buildFileTreeShellDeletePreflightPayload(");
|
||||
expect(source).not.toContain("computeFileTreeShellDeleteTargets(");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SIDEBAR_SOURCE = path.join(process.cwd(), "src/components/sidebar/sidebar.tsx");
|
||||
|
||||
describe("sidebar file tree paste preflight source", () => {
|
||||
it("rust_family 粘贴链应走 Rust paste preflight,而不是本地 shell row 语义推导", () => {
|
||||
const source = fs.readFileSync(SIDEBAR_SOURCE, "utf8");
|
||||
const preflightIndex = source.indexOf("preflightFileTreePaste(");
|
||||
const rustBranchStart = source.lastIndexOf("if (isRustFamilyTreeRenderer) {", preflightIndex);
|
||||
const legacyBranchStart = source.indexOf("const targetDocId = inferPasteTargetDocId", preflightIndex);
|
||||
|
||||
expect(preflightIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(rustBranchStart).toBeGreaterThanOrEqual(0);
|
||||
expect(legacyBranchStart).toBeGreaterThan(rustBranchStart);
|
||||
const rustPasteBranch = source.slice(rustBranchStart, legacyBranchStart);
|
||||
|
||||
expect(rustPasteBranch).toContain("preflightFileTreePaste(");
|
||||
expect(rustPasteBranch).toContain("buildFileTreeShellPastePreflightPayload(");
|
||||
expect(rustPasteBranch).toContain("pastePlan.docItems");
|
||||
expect(rustPasteBranch).toContain("pastePlan.resourceTransferPlan");
|
||||
expect(rustPasteBranch).not.toContain("docItemsMap");
|
||||
expect(rustPasteBranch).not.toContain("copyableAssetIds");
|
||||
expect(source).not.toContain("getOrderedFileTreeShellRows");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SIDEBAR_SOURCE = path.join(process.cwd(), "src/components/sidebar/sidebar.tsx");
|
||||
|
||||
describe("sidebar file tree selection source", () => {
|
||||
it("rust_family renderer selection snapshot 只能由 filetree selection event 写入", () => {
|
||||
const source = fs.readFileSync(SIDEBAR_SOURCE, "utf8");
|
||||
const writes = source.match(/setResourceRendererSelection\(/g) ?? [];
|
||||
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(source).toContain("const [resourceRendererSelection, setResourceRendererSelection]");
|
||||
expect(source).toContain("const handleFileTreeShellSelectionChange = useCallback");
|
||||
expect(source).toContain("materializeRendererSelectionSnapshot");
|
||||
expect(source).not.toContain("selectedRowIds={resourceSelection.selectedRowIds}");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SIDEBAR_SOURCE = path.join(process.cwd(), "src/components/sidebar/sidebar.tsx");
|
||||
|
||||
describe("sidebar file tree upload target preflight source", () => {
|
||||
it("外部上传链应走 Rust upload target preflight,而不是在 Sidebar 解释目标行与工作区", () => {
|
||||
const source = fs.readFileSync(SIDEBAR_SOURCE, "utf8");
|
||||
const handlerStart = source.indexOf("const handleResourcePaneDropFiles = useCallback");
|
||||
const handlerEnd = source.indexOf("const handleResourcePaneInternalDrop = useCallback");
|
||||
|
||||
expect(handlerStart).toBeGreaterThanOrEqual(0);
|
||||
expect(handlerEnd).toBeGreaterThan(handlerStart);
|
||||
const handlerSource = source.slice(handlerStart, handlerEnd);
|
||||
|
||||
expect(handlerSource).toContain("preflightFileTreeUploadTarget(");
|
||||
expect(handlerSource).toContain("buildFileTreeShellUploadTargetPreflightPayload(");
|
||||
expect(handlerSource).toContain("uploadTargetPlan.workspaceId");
|
||||
expect(handlerSource).toContain("uploadTargetPlan.targetDocumentId");
|
||||
expect(handlerSource).toContain("uploadTargetPlan.targetMindmapId");
|
||||
expect(handlerSource).not.toContain("resolveFileTreeShellMindmapTargetId");
|
||||
expect(handlerSource).not.toContain("inferFileTreeShellTargetDocumentId");
|
||||
expect(handlerSource).not.toContain("sidebarData.documents.find");
|
||||
});
|
||||
});
|
||||
@@ -54,18 +54,38 @@ import {
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
import { useEditorBridgeStore } from "@/store/editor-bridge";
|
||||
import { useCurrentDocumentStore } from "@/store/current-document";
|
||||
import { buildVisibleRows, filterKernelFileTreeProjectionItems } from "@/lib/file-tree/rows";
|
||||
import { buildParentById, filterTopLevelDocIds } from "@/lib/file-tree/dnd";
|
||||
import type { KernelFileTreeProjection } from "@/lib/kernel-file-tree";
|
||||
import { buildVisibleRows } from "@/lib/file-tree/rows";
|
||||
import { fetchKernelFileTreeProjection } from "@/lib/file-tree/projection-client";
|
||||
import {
|
||||
copyFileTreeResourceAssets,
|
||||
deleteFileTreeResourceAssets,
|
||||
moveFileTreeResourceAssets,
|
||||
preflightFileTreeDelete,
|
||||
preflightFileTreeInternalDrop,
|
||||
preflightFileTreePaste,
|
||||
preflightFileTreeUploadTarget,
|
||||
renameFileTreeResourceAsset,
|
||||
restoreFileTreeResourceAssets,
|
||||
uploadFileTreeResourceAsset,
|
||||
} from "@/lib/file-tree/resource-command-client";
|
||||
import { buildParentById } from "@/lib/file-tree/dnd";
|
||||
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
||||
import {
|
||||
computeFileTreeShellDeleteTargets,
|
||||
buildFileTreeShellDeletePreflightPayload,
|
||||
buildFileTreeShellInternalDropPreflightPayload,
|
||||
buildFileTreeShellPastePreflightPayload,
|
||||
buildFileTreeShellUploadTargetPreflightPayload,
|
||||
buildFileTreeShellRowById,
|
||||
buildFileTreeShellVisibleRowIds,
|
||||
collectFileTreeShellAssetHints,
|
||||
type FileTreeShellRow,
|
||||
inferFileTreeShellTargetDocumentId,
|
||||
getOrderedFileTreeShellRows,
|
||||
resolveFileTreeShellMindmapTargetId,
|
||||
} from "@/lib/file-tree/shell";
|
||||
import {
|
||||
createEmptyFileTreeSelectionState,
|
||||
materializeRendererSelectionSnapshot,
|
||||
resolveActiveFileTreeSelection,
|
||||
} from "@/lib/file-tree/selection-source";
|
||||
import { MoveEmbedPickerDialog, type MoveEmbedMode } from "@/components/documents/move-embed-picker-dialog";
|
||||
import {
|
||||
computeTreePaneDeleteTargets,
|
||||
@@ -263,11 +283,15 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
const [assetMenu, setAssetMenu] = useState<{ asset: MediaAsset; x: number; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
const [resourceSelection, setResourceSelection] = useState<TreePaneSelectionState>(() => ({
|
||||
selectedRowIds: new Set(),
|
||||
anchorRowId: null,
|
||||
focusedRowId: null,
|
||||
}));
|
||||
const [legacyResourceSelection, setLegacyResourceSelection] = useState<TreePaneSelectionState>(
|
||||
() => createEmptyFileTreeSelectionState(),
|
||||
);
|
||||
const [resourceRendererSelection, setResourceRendererSelection] = useState<TreePaneSelectionState>(
|
||||
() => createEmptyFileTreeSelectionState(),
|
||||
);
|
||||
const [searchFileTreeProjection, setSearchFileTreeProjection] =
|
||||
useState<KernelFileTreeProjection | null>(null);
|
||||
const [searchFileTreeProjectionKey, setSearchFileTreeProjectionKey] = useState<string | null>(null);
|
||||
const [sidebarHydrated, setSidebarHydrated] = useState(false);
|
||||
const treeSyncKeyRef = useRef<string>(buildSidebarTreeSyncKey(sidebarData.kernelSidebarTree));
|
||||
const mediaAssetsSyncKeyRef = useRef<string>(buildMediaAssetListSyncKey(sidebarData.mediaAssets ?? []));
|
||||
@@ -297,6 +321,42 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
pageTreeFocusedDocumentIdRef.current = activeId || null;
|
||||
}, [activeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const query = filter.trim();
|
||||
const workspaceId = sidebarData.activeWorkspaceId?.trim();
|
||||
if (!query || !workspaceId) {
|
||||
setSearchFileTreeProjection(null);
|
||||
setSearchFileTreeProjectionKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestKey = `${workspaceId}:${query}`;
|
||||
let cancelled = false;
|
||||
setSearchFileTreeProjectionKey(requestKey);
|
||||
setSearchFileTreeProjection(null);
|
||||
void fetchKernelFileTreeProjection({
|
||||
workspaceId,
|
||||
query,
|
||||
maxResults: 80,
|
||||
})
|
||||
.then((projection) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setSearchFileTreeProjection(projection);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setSearchFileTreeProjection(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [filter, sidebarData.activeWorkspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextAssets = sidebarData.mediaAssets ?? [];
|
||||
const nextSyncKey = buildMediaAssetListSyncKey(nextAssets);
|
||||
@@ -594,22 +654,21 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
|
||||
const [expandedAssetFolders, setExpandedAssetFolders] = useState<Set<string>>(() => new Set());
|
||||
|
||||
const normalizedFileTreeSearchQuery = filter.trim();
|
||||
const expectedSearchFileTreeProjectionKey =
|
||||
normalizedFileTreeSearchQuery && sidebarData.activeWorkspaceId
|
||||
? `${sidebarData.activeWorkspaceId}:${normalizedFileTreeSearchQuery}`
|
||||
: null;
|
||||
const resourceTreeShellItems = useMemo(
|
||||
() =>
|
||||
filter.trim().length === 0
|
||||
? undefined
|
||||
: filterKernelFileTreeProjectionItems({
|
||||
fileTreeItems: sidebarData.kernelFileTreeProjection.items,
|
||||
visibleDocumentIds: new Set(visibleFilteredPrivatePageRows.map((item) => item.nodeId)),
|
||||
expandedDocumentIds: expanded,
|
||||
expandedAssetFolderIds: expandedAssetFolders,
|
||||
}),
|
||||
expectedSearchFileTreeProjectionKey &&
|
||||
searchFileTreeProjectionKey === expectedSearchFileTreeProjectionKey
|
||||
? (searchFileTreeProjection?.items ?? [])
|
||||
: undefined,
|
||||
[
|
||||
expanded,
|
||||
expandedAssetFolders,
|
||||
sidebarData.kernelFileTreeProjection.items,
|
||||
visibleFilteredPrivatePageRows,
|
||||
filter,
|
||||
expectedSearchFileTreeProjectionKey,
|
||||
searchFileTreeProjection,
|
||||
searchFileTreeProjectionKey,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -619,8 +678,15 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
);
|
||||
|
||||
const effectiveResourceTreeShellItems = useMemo(
|
||||
() => resourceTreeShellItems ?? sidebarData.kernelFileTreeProjection.items,
|
||||
[resourceTreeShellItems, sidebarData.kernelFileTreeProjection.items],
|
||||
() =>
|
||||
normalizedFileTreeSearchQuery.length > 0
|
||||
? (resourceTreeShellItems ?? [])
|
||||
: sidebarData.kernelFileTreeProjection.items,
|
||||
[
|
||||
normalizedFileTreeSearchQuery,
|
||||
resourceTreeShellItems,
|
||||
sidebarData.kernelFileTreeProjection.items,
|
||||
],
|
||||
);
|
||||
|
||||
const resourceShellVisibleRowIds = useMemo(
|
||||
@@ -663,12 +729,24 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
const resourceSelectionVisibleRowIds = isRustFamilyTreeRenderer
|
||||
? resourceShellVisibleRowIds
|
||||
: resourceVisibleRowIds;
|
||||
const resourceSelection = useMemo(
|
||||
() =>
|
||||
resolveActiveFileTreeSelection({
|
||||
preferRendererSnapshot: isRustFamilyTreeRenderer,
|
||||
legacySelection: legacyResourceSelection,
|
||||
rendererSelection: resourceRendererSelection,
|
||||
}),
|
||||
[isRustFamilyTreeRenderer, legacyResourceSelection, resourceRendererSelection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setResourceSelection((prev) =>
|
||||
if (isRustFamilyTreeRenderer) {
|
||||
return;
|
||||
}
|
||||
setLegacyResourceSelection((prev) =>
|
||||
normalizeTreePaneSelectionForVisibleRows(prev, resourceSelectionVisibleRowIds),
|
||||
);
|
||||
}, [resourceSelectionVisibleRowIds]);
|
||||
}, [isRustFamilyTreeRenderer, resourceSelectionVisibleRowIds]);
|
||||
|
||||
const docParentById = useMemo(
|
||||
() =>
|
||||
@@ -680,6 +758,16 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
),
|
||||
[sidebarData.documents],
|
||||
);
|
||||
const documentWorkspaceById = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
(sidebarData.documents ?? []).map((doc) => [
|
||||
doc.id,
|
||||
typeof doc.workspace_id === "string" ? doc.workspace_id : null,
|
||||
]),
|
||||
),
|
||||
[sidebarData.documents],
|
||||
);
|
||||
|
||||
const childrenCountByParentId = useMemo(() => {
|
||||
const map = new Map<string | null, number>();
|
||||
@@ -868,12 +956,12 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
}, [activeId, editorBridge, router, setOpen]);
|
||||
|
||||
const handleResourcePaneBlankMouseDown = useCallback(() => {
|
||||
setResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
setLegacyResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
}, []);
|
||||
|
||||
const handleResourceRowClick = useCallback(
|
||||
(row: TreePaneRow, event: React.MouseEvent) => {
|
||||
setResourceSelection((prev) =>
|
||||
setLegacyResourceSelection((prev) =>
|
||||
reduceTreePaneSelection(prev, {
|
||||
type: "click",
|
||||
rowId: row.rowId,
|
||||
@@ -902,7 +990,7 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
);
|
||||
|
||||
const handleResourceRowDragStart = useCallback((row: TreePaneRow) => {
|
||||
setResourceSelection((prev) => {
|
||||
setLegacyResourceSelection((prev) => {
|
||||
if (prev.selectedRowIds.has(row.rowId)) return prev;
|
||||
return reduceTreePaneSelection(prev, {
|
||||
type: "click",
|
||||
@@ -928,7 +1016,7 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
(row: TreePaneRow, event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setResourceSelection((prev) =>
|
||||
setLegacyResourceSelection((prev) =>
|
||||
reduceTreePaneSelection(prev, { type: "contextmenu", rowId: row.rowId }),
|
||||
);
|
||||
|
||||
@@ -1047,25 +1135,14 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
anchorRowId: string | null;
|
||||
focusedRowId: string | null;
|
||||
}) => {
|
||||
const normalized = normalizeTreePaneSelectionForVisibleRows(
|
||||
{
|
||||
selectedRowIds: new Set(
|
||||
payload.selectedRowIds.filter((rowId) => resourceShellRowById.has(rowId)),
|
||||
),
|
||||
anchorRowId:
|
||||
payload.anchorRowId && resourceShellRowById.has(payload.anchorRowId)
|
||||
? payload.anchorRowId
|
||||
: null,
|
||||
focusedRowId:
|
||||
payload.focusedRowId && resourceShellRowById.has(payload.focusedRowId)
|
||||
? payload.focusedRowId
|
||||
: null,
|
||||
},
|
||||
resourceShellVisibleRowIds,
|
||||
setResourceRendererSelection(
|
||||
materializeRendererSelectionSnapshot({
|
||||
payload,
|
||||
hasRowId: (rowId) => resourceShellRowById.has(rowId),
|
||||
}),
|
||||
);
|
||||
setResourceSelection(normalized);
|
||||
},
|
||||
[resourceShellRowById, resourceShellVisibleRowIds],
|
||||
[resourceShellRowById],
|
||||
);
|
||||
|
||||
const handleFileTreeShellAssetOpen = useCallback(
|
||||
@@ -1132,17 +1209,63 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDocId = isRustFamilyTreeRenderer
|
||||
? inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
rowById: resourceShellRowById,
|
||||
activeDocId: activeId || null,
|
||||
})
|
||||
: inferPasteTargetDocId({
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
rowById: resourceRowById,
|
||||
activeDocId: activeId || null,
|
||||
});
|
||||
if (isRustFamilyTreeRenderer) {
|
||||
let pastePlan;
|
||||
try {
|
||||
pastePlan = await preflightFileTreePaste(
|
||||
buildFileTreeShellPastePreflightPayload({
|
||||
workspaceId: sidebarData.activeWorkspaceId ?? null,
|
||||
targetDocumentId: null,
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
activeDocId: activeId || null,
|
||||
rowIds: payload.rowIds,
|
||||
rowById: resourceShellRowById,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "文件树粘贴预检失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pastePlan.docItems.length > 0) {
|
||||
try {
|
||||
await copyTreeCommand({
|
||||
items: pastePlan.docItems,
|
||||
targetParentId: pastePlan.targetDocumentId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "粘贴页面失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
emitDocumentsChanged(pastePlan.targetDocumentId);
|
||||
}
|
||||
|
||||
if (pastePlan.resourceTransferPlan && pastePlan.resourceTransferPlan.assetIds.length > 0) {
|
||||
try {
|
||||
await copyFileTreeResourceAssets({
|
||||
assetIds: pastePlan.resourceTransferPlan.assetIds,
|
||||
targetDocumentId: pastePlan.resourceTransferPlan.targetDocumentId,
|
||||
targetSubPath: pastePlan.resourceTransferPlan.targetSubPath,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "粘贴附件失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
emitAssetsChanged(pastePlan.resourceTransferPlan.targetDocumentId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDocId = inferPasteTargetDocId({
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
rowById: resourceRowById,
|
||||
activeDocId: activeId || null,
|
||||
});
|
||||
if (!targetDocId) {
|
||||
setTimeout(() => window.alert("请选择一个目标页面后再粘贴"), 0);
|
||||
return;
|
||||
@@ -1151,50 +1274,28 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
const docItemsMap = new Map<string, boolean>();
|
||||
const copyableAssetIds: string[] = [];
|
||||
|
||||
if (isRustFamilyTreeRenderer) {
|
||||
const rows = getOrderedFileTreeShellRows({
|
||||
rowIds: payload.rowIds,
|
||||
visibleRowIds: resourceShellVisibleRowIds,
|
||||
rowById: resourceShellRowById,
|
||||
});
|
||||
const rows = payload.rowIds
|
||||
.map((rowId) => resourceRowById.get(rowId as any))
|
||||
.filter(Boolean) as TreePaneRow[];
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row.rowKind === "doc") {
|
||||
docItemsMap.set(row.documentId, true);
|
||||
return;
|
||||
rows.forEach((row) => {
|
||||
if (row.kind === "doc") {
|
||||
docItemsMap.set(row.docId, true);
|
||||
} else if (row.kind === "index") {
|
||||
if (!docItemsMap.has(row.docId)) {
|
||||
docItemsMap.set(row.docId, false);
|
||||
}
|
||||
if (row.rowKind === "index" && !docItemsMap.has(row.documentId)) {
|
||||
docItemsMap.set(row.documentId, false);
|
||||
return;
|
||||
}
|
||||
if (row.rowKind === "asset" && row.asset && isRealFileAsset(row.asset)) {
|
||||
copyableAssetIds.push(row.asset.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const rows = payload.rowIds
|
||||
.map((rowId) => resourceRowById.get(rowId as any))
|
||||
.filter(Boolean) as TreePaneRow[];
|
||||
}
|
||||
});
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row.kind === "doc") {
|
||||
docItemsMap.set(row.docId, true);
|
||||
} else if (row.kind === "index") {
|
||||
if (!docItemsMap.has(row.docId)) {
|
||||
docItemsMap.set(row.docId, false);
|
||||
}
|
||||
}
|
||||
rows
|
||||
.filter((row): row is Extract<TreePaneRow, { kind: "asset" }> => row.kind === "asset")
|
||||
.map((row) => row.asset)
|
||||
.filter((asset) => isRealFileAsset(asset))
|
||||
.forEach((asset) => {
|
||||
copyableAssetIds.push(asset.id);
|
||||
});
|
||||
|
||||
rows
|
||||
.filter((row): row is Extract<TreePaneRow, { kind: "asset" }> => row.kind === "asset")
|
||||
.map((row) => row.asset)
|
||||
.filter((asset) => isRealFileAsset(asset))
|
||||
.forEach((asset) => {
|
||||
copyableAssetIds.push(asset.id);
|
||||
});
|
||||
}
|
||||
|
||||
if (docItemsMap.size > 0) {
|
||||
try {
|
||||
await copyTreeCommand({
|
||||
@@ -1214,18 +1315,14 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
}
|
||||
|
||||
if (copyableAssetIds.length > 0) {
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "copy",
|
||||
try {
|
||||
await copyFileTreeResourceAssets({
|
||||
assetIds: copyableAssetIds,
|
||||
targetDocumentId: targetDocId,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
setTimeout(() => window.alert(data?.error ?? "粘贴附件失败"), 0);
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "粘贴附件失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
@@ -1370,14 +1467,10 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
const input = window.prompt("输入新文件名", asset.file_name ?? "");
|
||||
if (!input || !input.trim()) return;
|
||||
const newName = input.trim();
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "rename", assetIds: [asset.id], newName }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "重命名失败");
|
||||
try {
|
||||
await renameFileTreeResourceAsset({ assetId: asset.id, newName });
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "重命名失败");
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
@@ -1399,18 +1492,13 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
}
|
||||
const target = window.prompt("输入目标页面 ID", asset.document_id);
|
||||
if (!target || !target.trim()) return;
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
try {
|
||||
await moveFileTreeResourceAssets({
|
||||
assetIds: [asset.id],
|
||||
targetDocumentId: target.trim(),
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "移动失败");
|
||||
});
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "移动失败");
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
@@ -1475,14 +1563,10 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
}
|
||||
|
||||
if (fileAssetIds.length > 0) {
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "delete", assetIds: fileAssetIds }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "删除失败");
|
||||
try {
|
||||
await deleteFileTreeResourceAssets(fileAssetIds);
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "删除失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1504,14 +1588,35 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
);
|
||||
|
||||
const handleDeleteResourceSelection = useCallback(async () => {
|
||||
const shellDeleteTargets = isRustFamilyTreeRenderer
|
||||
? computeFileTreeShellDeleteTargets({
|
||||
visibleRowIds: resourceShellVisibleRowIds,
|
||||
rowById: resourceShellRowById,
|
||||
selectedRowIds: resourceSelection.selectedRowIds,
|
||||
parentById: docParentById,
|
||||
})
|
||||
: null;
|
||||
const selectedRowIds = Array.from(resourceSelection.selectedRowIds);
|
||||
let shellDeleteTargets: {
|
||||
docIds: string[];
|
||||
assetIds: string[];
|
||||
assetHints: MediaAsset[];
|
||||
} | null = null;
|
||||
if (isRustFamilyTreeRenderer) {
|
||||
try {
|
||||
const deletePlan = await preflightFileTreeDelete(
|
||||
buildFileTreeShellDeletePreflightPayload({
|
||||
workspaceId: sidebarData.activeWorkspaceId ?? null,
|
||||
rowIds: selectedRowIds,
|
||||
rowById: resourceShellRowById,
|
||||
parentById: docParentById,
|
||||
}),
|
||||
);
|
||||
shellDeleteTargets = {
|
||||
docIds: deletePlan.docIds,
|
||||
assetIds: deletePlan.assetIds,
|
||||
assetHints: collectFileTreeShellAssetHints({
|
||||
rowById: resourceShellRowById,
|
||||
assetIds: deletePlan.assetIds,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "文件树删除预检失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const legacyDeleteTargets = !isRustFamilyTreeRenderer
|
||||
? computeTreePaneDeleteTargets({
|
||||
visibleRows: resourceRows,
|
||||
@@ -1595,7 +1700,9 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
|
||||
await refreshTree();
|
||||
setContextMenu(null);
|
||||
setResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
if (!isRustFamilyTreeRenderer) {
|
||||
setLegacyResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "删除失败");
|
||||
}
|
||||
@@ -1605,11 +1712,11 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
isRustFamilyTreeRenderer,
|
||||
resourceRows,
|
||||
resourceShellRowById,
|
||||
resourceShellVisibleRowIds,
|
||||
resourceSelection.selectedRowIds,
|
||||
handleDeleteAssets,
|
||||
refreshTree,
|
||||
router,
|
||||
sidebarData.activeWorkspaceId,
|
||||
]);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
@@ -1764,59 +1871,43 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
void (async () => {
|
||||
const droppedFiles = Array.from(payload.files ?? []);
|
||||
if (droppedFiles.length === 0) return;
|
||||
const targetRow =
|
||||
payload.targetRowId
|
||||
? (resourceShellRowById.get(payload.targetRowId) ?? null)
|
||||
: null;
|
||||
|
||||
const targetMindmapId = resolveFileTreeShellMindmapTargetId(targetRow);
|
||||
|
||||
if (targetMindmapId) {
|
||||
setExpandedAssetFolders((prev) => new Set(prev).add(targetMindmapId));
|
||||
}
|
||||
|
||||
const inferredTargetDocId =
|
||||
payload.targetDocumentId ||
|
||||
inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
rowById: resourceShellRowById,
|
||||
activeDocId: activeId || null,
|
||||
}) ||
|
||||
"";
|
||||
|
||||
if (!inferredTargetDocId) {
|
||||
setTimeout(() => window.alert("请选择一个目标页面后再拖入文件"), 0);
|
||||
let uploadTargetPlan;
|
||||
try {
|
||||
uploadTargetPlan = await preflightFileTreeUploadTarget(
|
||||
buildFileTreeShellUploadTargetPreflightPayload({
|
||||
workspaceId: sidebarData.activeWorkspaceId ?? null,
|
||||
targetDocumentId: payload.targetDocumentId,
|
||||
targetRowId: payload.targetRowId,
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
activeDocId: activeId || null,
|
||||
rowById: resourceShellRowById,
|
||||
documentWorkspaceById,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "文件树上传目标预检失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetDoc = sidebarData.documents.find((doc) => doc.id === inferredTargetDocId) ?? null;
|
||||
const workspaceId = targetDoc?.workspace_id ?? sidebarData.activeWorkspaceId ?? "";
|
||||
if (!workspaceId) {
|
||||
setTimeout(() => window.alert("无法识别当前工作区,上传失败"), 0);
|
||||
return;
|
||||
if (uploadTargetPlan.targetMindmapId) {
|
||||
setExpandedAssetFolders((prev) => new Set(prev).add(uploadTargetPlan.targetMindmapId));
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const file of droppedFiles) {
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
form.append("workspaceId", workspaceId);
|
||||
form.append("documentId", inferredTargetDocId);
|
||||
if (targetMindmapId) {
|
||||
form.append("mindmapId", targetMindmapId);
|
||||
}
|
||||
const resp = await fetch("/api/media/upload", { method: "POST", body: form });
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
errors.push(`${file.name}: ${payload?.error ?? "上传失败"}`);
|
||||
continue;
|
||||
}
|
||||
const payload = (await resp.json()) as { asset?: MediaAsset };
|
||||
const payload = await uploadFileTreeResourceAsset({
|
||||
file,
|
||||
workspaceId: uploadTargetPlan.workspaceId,
|
||||
documentId: uploadTargetPlan.targetDocumentId,
|
||||
mindmapId: uploadTargetPlan.targetMindmapId,
|
||||
});
|
||||
if (payload.asset?.id) {
|
||||
emitAssetsChanged(inferredTargetDocId, payload.asset);
|
||||
emitAssetsChanged(uploadTargetPlan.targetDocumentId, payload.asset);
|
||||
// 拖拽文件进文件树:如果落点就是当前打开的页面,则把文件也插入到主编辑区
|
||||
if (inferredTargetDocId === activeId && !targetMindmapId) {
|
||||
if (uploadTargetPlan.targetDocumentId === activeId && !uploadTargetPlan.targetMindmapId) {
|
||||
editorBridge?.insertMediaAsset?.(payload.asset);
|
||||
}
|
||||
} else {
|
||||
@@ -1843,11 +1934,11 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
},
|
||||
[
|
||||
activeId,
|
||||
documentWorkspaceById,
|
||||
editorBridge,
|
||||
resourceShellRowById,
|
||||
resourceSelection.focusedRowId,
|
||||
sidebarData.activeWorkspaceId,
|
||||
sidebarData.documents,
|
||||
sidebarQuery,
|
||||
],
|
||||
);
|
||||
@@ -1862,64 +1953,44 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
copy: boolean;
|
||||
}) => {
|
||||
void (async () => {
|
||||
const targetRow =
|
||||
payload.targetRowId
|
||||
? (resourceShellRowById.get(payload.targetRowId) ?? null)
|
||||
: null;
|
||||
const targetDocId =
|
||||
payload.targetDocumentId ??
|
||||
targetRow?.documentId ??
|
||||
inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
rowById: resourceShellRowById,
|
||||
activeDocId: activeId || null,
|
||||
});
|
||||
if (!targetDocId) {
|
||||
setTimeout(() => window.alert("无法识别拖拽目标页面"), 0);
|
||||
const preflightPayload = buildFileTreeShellInternalDropPreflightPayload({
|
||||
workspaceId: sidebarData.activeWorkspaceId ?? null,
|
||||
copy: payload.copy,
|
||||
targetDocumentId: payload.targetDocumentId,
|
||||
targetRowId: payload.targetRowId,
|
||||
rowIds: payload.rowIds,
|
||||
rowById: resourceShellRowById,
|
||||
focusedRowId: resourceSelection.focusedRowId,
|
||||
activeDocId: activeId || null,
|
||||
parentById: docParentById,
|
||||
});
|
||||
let dropPlan;
|
||||
try {
|
||||
dropPlan = await preflightFileTreeInternalDrop(preflightPayload);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "文件树拖放预检失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMindmapId = resolveFileTreeShellMindmapTargetId(targetRow);
|
||||
|
||||
const targetSubPath = targetMindmapId ? `mindmaps/${targetMindmapId}` : undefined;
|
||||
const {
|
||||
targetDocumentId: targetDocId,
|
||||
targetMindmapId,
|
||||
documentTransferPlan,
|
||||
resourceTransferPlan,
|
||||
sourceAssetDocumentIds,
|
||||
} = dropPlan;
|
||||
|
||||
if (targetMindmapId) {
|
||||
setExpandedAssetFolders((prev) => new Set(prev).add(targetMindmapId));
|
||||
}
|
||||
|
||||
const uniqueRowIds: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
payload.rowIds.forEach((id) => {
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
uniqueRowIds.push(id);
|
||||
});
|
||||
|
||||
const rows = uniqueRowIds
|
||||
.map((rowId) => resourceShellRowById.get(rowId) ?? null)
|
||||
.filter((row): row is FileTreeShellRow => Boolean(row));
|
||||
|
||||
const docIds = rows.filter((row) => row.rowKind === "doc").map((row) => row.documentId);
|
||||
const assetRows = rows.filter(
|
||||
(row): row is FileTreeShellRow & { rowKind: "asset"; asset: MediaAsset } =>
|
||||
row.rowKind === "asset" && Boolean(row.asset),
|
||||
);
|
||||
const copyableAssetIds = assetRows
|
||||
.map((row) => row.asset)
|
||||
.filter((asset) => isRealFileAsset(asset))
|
||||
.map((asset) => asset.id);
|
||||
|
||||
if (docIds.length === 0 && copyableAssetIds.length === 0) {
|
||||
setTimeout(() => window.alert("没有可拖拽的对象(虚拟附件暂不支持)"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.copy) {
|
||||
if (docIds.length > 0) {
|
||||
if (documentTransferPlan && documentTransferPlan.copyItems.length > 0) {
|
||||
try {
|
||||
await copyTreeCommand({
|
||||
items: docIds.map((documentId) => ({ documentId, recursive: true })),
|
||||
targetParentId: targetDocId,
|
||||
items: documentTransferPlan.copyItems,
|
||||
targetParentId: documentTransferPlan.targetParentId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "复制页面失败";
|
||||
@@ -1930,20 +2001,16 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
emitDocumentsChanged(targetDocId);
|
||||
}
|
||||
|
||||
if (copyableAssetIds.length > 0) {
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "copy",
|
||||
assetIds: copyableAssetIds,
|
||||
targetDocumentId: targetDocId,
|
||||
targetSubPath,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
setTimeout(() => window.alert(payload?.error ?? "复制附件失败"), 0);
|
||||
if (resourceTransferPlan && resourceTransferPlan.assetIds.length > 0) {
|
||||
try {
|
||||
await copyFileTreeResourceAssets({
|
||||
assetIds: resourceTransferPlan.assetIds,
|
||||
targetDocumentId: resourceTransferPlan.targetDocumentId,
|
||||
targetSubPath: resourceTransferPlan.targetSubPath,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "复制附件失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
@@ -1953,23 +2020,23 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
return;
|
||||
}
|
||||
|
||||
const topLevelDocIds = filterTopLevelDocIds(docIds, docParentById);
|
||||
if (topLevelDocIds.length > 0) {
|
||||
if (documentTransferPlan && documentTransferPlan.topLevelDocumentIds.length > 0) {
|
||||
const topLevelDocIds = documentTransferPlan.topLevelDocumentIds;
|
||||
const baseIndex = childrenCountByParentId.get(targetDocId) ?? 0;
|
||||
setTree((prev) => {
|
||||
let next = prev;
|
||||
topLevelDocIds.forEach((id, offset) => {
|
||||
next = moveLocalNode(next, id, targetDocId, baseIndex + offset);
|
||||
next = moveLocalNode(next, id, documentTransferPlan.targetParentId, baseIndex + offset);
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setExpanded((prev) => new Set(prev).add(targetDocId));
|
||||
setExpanded((prev) => new Set(prev).add(documentTransferPlan.targetParentId));
|
||||
|
||||
try {
|
||||
for (let i = 0; i < topLevelDocIds.length; i += 1) {
|
||||
await moveDocumentCommand({
|
||||
documentId: topLevelDocIds[i],
|
||||
parentId: targetDocId,
|
||||
parentId: documentTransferPlan.targetParentId,
|
||||
position: baseIndex + i,
|
||||
});
|
||||
}
|
||||
@@ -1983,29 +2050,20 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
emitDocumentsChanged(targetDocId);
|
||||
}
|
||||
|
||||
if (copyableAssetIds.length > 0) {
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
assetIds: copyableAssetIds,
|
||||
targetDocumentId: targetDocId,
|
||||
targetSubPath,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
setTimeout(() => window.alert(payload?.error ?? "移动附件失败"), 0);
|
||||
if (resourceTransferPlan && resourceTransferPlan.assetIds.length > 0) {
|
||||
try {
|
||||
await moveFileTreeResourceAssets({
|
||||
assetIds: resourceTransferPlan.assetIds,
|
||||
targetDocumentId: resourceTransferPlan.targetDocumentId,
|
||||
targetSubPath: resourceTransferPlan.targetSubPath,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "移动附件失败";
|
||||
setTimeout(() => window.alert(message), 0);
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
const sourceDocIds = new Set(
|
||||
assetRows
|
||||
.map((row) => row.asset?.document_id ?? null)
|
||||
.filter((documentId): documentId is string => Boolean(documentId)),
|
||||
);
|
||||
sourceDocIds.forEach((id) => emitAssetsChanged(id));
|
||||
sourceAssetDocumentIds.forEach((id) => emitAssetsChanged(id));
|
||||
emitAssetsChanged(targetDocId);
|
||||
}
|
||||
})();
|
||||
@@ -2013,6 +2071,7 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
[
|
||||
childrenCountByParentId,
|
||||
docParentById,
|
||||
sidebarData.activeWorkspaceId,
|
||||
resourceSelection.focusedRowId,
|
||||
activeId,
|
||||
resourceShellRowById,
|
||||
@@ -2091,12 +2150,14 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
|
||||
try {
|
||||
await handleDeleteAssets(uniqueAssetIds, assetHint);
|
||||
setResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
if (!isRustFamilyTreeRenderer) {
|
||||
setLegacyResourceSelection((prev) => reduceTreePaneSelection(prev, { type: "clear" }));
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "删除失败");
|
||||
}
|
||||
},
|
||||
[handleDeleteAssets, mediaAssets, mindmapAssets, tableAssets],
|
||||
[handleDeleteAssets, isRustFamilyTreeRenderer, mediaAssets, mindmapAssets, tableAssets],
|
||||
);
|
||||
|
||||
const handleConvertToChild = useCallback(
|
||||
@@ -2182,14 +2243,10 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
(filteredTrashedMediaAssets ?? []).find((a) => a.id === assetId) ??
|
||||
(sidebarData.trashedMediaAssets ?? []).find((a) => a.id === assetId) ??
|
||||
null;
|
||||
const response = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "restore", assetIds: [assetId] }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "恢复附件失败,请稍后再试");
|
||||
try {
|
||||
await restoreFileTreeResourceAssets([assetId]);
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "恢复附件失败,请稍后再试");
|
||||
return;
|
||||
}
|
||||
await Promise.all([sidebarQuery.refetch(), refreshTree()]);
|
||||
@@ -2822,7 +2879,6 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
|
||||
rows={isRustFamilyTreeRenderer ? undefined : resourceRows}
|
||||
treeShellItems={effectiveResourceTreeShellItems}
|
||||
activeId={activeId}
|
||||
selectedRowIds={resourceSelection.selectedRowIds}
|
||||
onRowClick={handleResourceRowClick}
|
||||
onRowDoubleClick={(row, event) => handleResourceRowDoubleClick(row, event)}
|
||||
onRowContextMenu={handleResourceRowContextMenu}
|
||||
|
||||
@@ -13,6 +13,8 @@ export type TreeRendererFamily = "react" | "rust_family";
|
||||
|
||||
export type TreeShellHostMode = "page" | "filetree" | "picker";
|
||||
|
||||
const RUST_RENDERER_CONTRACT = "rust_renderer_input_v1";
|
||||
|
||||
export type TreeShellPickerCommand = {
|
||||
kind: "next" | "previous" | "home" | "end" | "pick";
|
||||
seq: number;
|
||||
@@ -118,8 +120,9 @@ export function TreeShellHost({
|
||||
const useRustHost = rendererFamily === "rust_family";
|
||||
const useIframeHost = useRustHost && Boolean(workspaceId?.trim());
|
||||
const hostKind = rendererFamily === "rust_family" ? "rust_family" : "react";
|
||||
const rendererContract = useRustHost ? RUST_RENDERER_CONTRACT : undefined;
|
||||
const implementation = useIframeHost
|
||||
? "mnote_web_iframe_proxy"
|
||||
? "rust_inline_compat_host"
|
||||
: fallbackImplementation ??
|
||||
(rendererFamily === "rust_family" ? "react_fallback" : "react_primary");
|
||||
|
||||
@@ -130,6 +133,7 @@ export function TreeShellHost({
|
||||
data-renderer-family={rendererFamily}
|
||||
data-tree-host-kind={hostKind}
|
||||
data-tree-host-implementation={implementation}
|
||||
data-tree-renderer-contract={rendererContract}
|
||||
data-page-tree-focused-id={mode === "page" ? (focusedDocumentId ?? "") : undefined}
|
||||
className={cn(className)}
|
||||
>
|
||||
@@ -139,6 +143,7 @@ export function TreeShellHost({
|
||||
data-tree-host-mode={mode}
|
||||
data-tree-host-kind="rust_family"
|
||||
data-tree-host-implementation={implementation}
|
||||
data-tree-renderer-contract={rendererContract}
|
||||
className="contents"
|
||||
>
|
||||
{useIframeHost && workspaceId ? (
|
||||
|
||||
@@ -17,6 +17,24 @@ import {
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function readTreeShellState(srcDoc: string | null | undefined) {
|
||||
const match = (srcDoc ?? "").match(
|
||||
/<script id="tree-shell-state" type="application\/json">([^<]*)<\/script>/,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error("missing tree shell state");
|
||||
}
|
||||
return JSON.parse(match[1] ?? "{}") as {
|
||||
items?: unknown[];
|
||||
rendererInput?: {
|
||||
projectionItemIds?: string[];
|
||||
expandedIds?: string[];
|
||||
activePickerItem?: string | null;
|
||||
excludedPickerIds?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
describe("tree-shell-iframe-host", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
@@ -61,6 +79,33 @@ describe("tree-shell-iframe-host", () => {
|
||||
expect(url.searchParams.get("host")).toBe("tree-picker-surface");
|
||||
});
|
||||
|
||||
it("未提供 inline projection 时也应使用本地 srcDoc,避免默认回源 3104 显示 fetch failed", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("fetch failed"));
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<TreeShellIframeHost
|
||||
mode="page"
|
||||
surfaceTestId="sidebar-page-tree-shell"
|
||||
workspaceId="ws_1"
|
||||
activeDocumentId="doc_1"
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(iframe?.getAttribute("src")).toBeNull();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("<script>window.__MNOTE_TREE_SHELL_OVERRIDE__ =");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('data-rust-page-renderer="initial_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("fetch failed");
|
||||
});
|
||||
|
||||
it("应能把 picker 搜索结果转换为 inline shell items 并注入到 HTML", () => {
|
||||
const pickerItems = buildTreeShellInlinePickerItems([
|
||||
{ kind: "doc", id: "doc_target", title: "目标页面", depth: 0 },
|
||||
@@ -179,12 +224,19 @@ describe("tree-shell-iframe-host", () => {
|
||||
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("__MNOTE_TREE_SHELL_OVERRIDE__");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("<script>window.__MNOTE_TREE_SHELL_OVERRIDE__ =");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("父页面");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("tree-action-menu");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("tree-action-create");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("tree-action-rename");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("/api/tree/commands");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchPageTreeActiveDom();");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_page_focus_keyboard_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyPageKeyboardAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "page" && usedRustInitialRenderer) {patchPageTreeActiveDom();if (focusedNodeId) focusRowElement(focusedNodeId);return;}');
|
||||
expect(readTreeShellState(iframe?.getAttribute("srcdoc")).items).toEqual([
|
||||
expect.objectContaining({ nodeId: "doc_parent" }),
|
||||
]);
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("\n");
|
||||
});
|
||||
|
||||
@@ -256,7 +308,8 @@ describe("tree-shell-iframe-host", () => {
|
||||
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("__MNOTE_TREE_SHELL_OVERRIDE__");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("<script>window.__MNOTE_TREE_SHELL_OVERRIDE__ =");
|
||||
expect(readTreeShellState(iframe?.getAttribute("srcdoc")).items).toEqual([]);
|
||||
});
|
||||
|
||||
it("file tree 提供应直接消费的 kernel items 时,应本地生成 shell 并注入正式 item contract", async () => {
|
||||
@@ -347,9 +400,33 @@ describe("tree-shell-iframe-host", () => {
|
||||
const iframe = container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("__MNOTE_TREE_SHELL_OVERRIDE__");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("<script>window.__MNOTE_TREE_SHELL_OVERRIDE__ =");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"rowKind":"asset"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('data-rust-filetree-renderer="initial_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('data-rust-rendered-row="filetree"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("hydrateInitialFileTree");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const usedRustInitialRenderer = hydrateInitialRenderer();");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchFileTreeActiveDom();");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "filetree" && usedRustInitialRenderer) {patchFileTreeActiveDom();syncFileTreeSelectionDom();return;}');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"rendererInput"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"mode":"fileTree"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"expandedIds":["doc_a"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const rendererExpandedIds = normalizeStringArray(rendererInput.expandedIds);");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"filetreeSelection"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"selectedRowIds":["doc:doc_a","index:doc_a"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_filetree_selection_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyFileTreeSelectionAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const rendererFiletreeSelection =");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererFiletreeSelection.selectedRowIds");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("computeFileTreeSelectionActionResult");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const selectFileTreeContextRow = (rowId) =>");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("guide.pdf");
|
||||
const state = readTreeShellState(iframe?.getAttribute("srcdoc"));
|
||||
expect(state.items).toEqual([
|
||||
expect.objectContaining({ rowId: "doc:doc_a", nodeId: "doc_a" }),
|
||||
expect.objectContaining({ rowId: "asset:asset_pdf", nodeId: "asset:asset_pdf" }),
|
||||
]);
|
||||
expect(state.rendererInput?.projectionItemIds).toEqual(["doc:doc_a", "asset:asset_pdf"]);
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("\n");
|
||||
});
|
||||
|
||||
@@ -370,6 +447,7 @@ describe("tree-shell-iframe-host", () => {
|
||||
workspaceId="ws_1"
|
||||
activeDocumentId="doc_1"
|
||||
activePickerItemKey="doc_1"
|
||||
excludeIds={["doc_hidden"]}
|
||||
pickerItems={pickerItems}
|
||||
/>,
|
||||
);
|
||||
@@ -380,6 +458,20 @@ describe("tree-shell-iframe-host", () => {
|
||||
});
|
||||
|
||||
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('data-rust-picker-renderer="initial_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('data-rust-rendered-row="picker"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("hydrateInitialPickerTree");
|
||||
expect(readTreeShellState(iframe?.getAttribute("srcdoc")).items).toEqual([
|
||||
expect.objectContaining({ nodeId: "doc_1" }),
|
||||
]);
|
||||
expect(readTreeShellState(iframe?.getAttribute("srcdoc")).rendererInput?.activePickerItem).toBe("doc_1");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"activePickerItem":"doc_1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"excludedPickerIds":["doc_hidden"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_picker_state_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyPickerStateAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchPickerActiveDom");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererInput.activePickerItem");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererInput.excludedPickerIds");
|
||||
const postMessage = vi.fn();
|
||||
Object.defineProperty(iframe, "contentWindow", {
|
||||
configurable: true,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,20 +54,25 @@ describe("tree-shell-surface", () => {
|
||||
|
||||
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("page tree surface 在 rust_family 下应把 focusedDocumentId 透传到 iframe", () => {
|
||||
it("page tree surface 在 rust_family 下应把 focusedDocumentId 作为宿主状态暴露,并使用 postMessage patch 同步 iframe", () => {
|
||||
renderPageSurface("rust_family", "doc_focus");
|
||||
|
||||
const iframe = container.querySelector(
|
||||
'[data-testid="sidebar-page-tree-shell-rust-iframe"]',
|
||||
) as HTMLIFrameElement | null;
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
|
||||
expect(iframe?.getAttribute("src")).toContain("focusedDocumentId=doc_focus");
|
||||
expect(surface?.getAttribute("data-page-tree-focused-id")).toBe("doc_focus");
|
||||
expect(iframe?.getAttribute("src")).toBeNull();
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"focusedDocumentId":"doc_focus"');
|
||||
});
|
||||
|
||||
it("page tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
|
||||
@@ -90,7 +95,7 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
@@ -129,7 +134,6 @@ describe("tree-shell-surface", () => {
|
||||
treeShellEnabled
|
||||
rows={[]}
|
||||
activeId=""
|
||||
selectedRowIds={new Set<string>()}
|
||||
onRowClick={() => undefined}
|
||||
onRowDoubleClick={() => undefined}
|
||||
onRowContextMenu={() => undefined}
|
||||
@@ -147,7 +151,9 @@ describe("tree-shell-surface", () => {
|
||||
|
||||
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
|
||||
@@ -163,7 +169,6 @@ describe("tree-shell-surface", () => {
|
||||
treeShellEnabled={false}
|
||||
rows={[]}
|
||||
activeId=""
|
||||
selectedRowIds={new Set<string>()}
|
||||
onRowClick={() => undefined}
|
||||
onRowDoubleClick={() => undefined}
|
||||
onRowContextMenu={() => undefined}
|
||||
@@ -174,7 +179,7 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-file-tree-shell"]');
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
@@ -189,7 +194,6 @@ describe("tree-shell-surface", () => {
|
||||
treeShellEnabled
|
||||
rows={[]}
|
||||
activeId=""
|
||||
selectedRowIds={new Set<string>()}
|
||||
onRowClick={() => undefined}
|
||||
onRowDoubleClick={() => undefined}
|
||||
onRowContextMenu={() => undefined}
|
||||
@@ -218,7 +222,6 @@ describe("tree-shell-surface", () => {
|
||||
treeShellEnabled
|
||||
rows={[]}
|
||||
activeId=""
|
||||
selectedRowIds={new Set<string>()}
|
||||
onRowClick={() => undefined}
|
||||
onRowDoubleClick={() => undefined}
|
||||
onRowContextMenu={() => undefined}
|
||||
@@ -309,7 +312,9 @@ describe("tree-shell-surface", () => {
|
||||
|
||||
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(onPick).not.toHaveBeenCalled();
|
||||
@@ -339,7 +344,7 @@ describe("tree-shell-surface", () => {
|
||||
const rustHost = container.querySelector('[data-testid="tree-picker-surface-rust-host"]');
|
||||
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
|
||||
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
|
||||
@@ -46,7 +46,6 @@ type SidebarFileTreeSurfaceProps = {
|
||||
rows?: FileTreeRow[];
|
||||
treeShellItems?: KernelFileTreeProjectionItem[];
|
||||
activeId: string;
|
||||
selectedRowIds: Set<string>;
|
||||
className?: string;
|
||||
onRowClick: (row: FileTreeRow, event: MouseEvent) => void;
|
||||
onRowDoubleClick: (row: FileTreeRow, event: MouseEvent) => void;
|
||||
|
||||
Reference in New Issue
Block a user