4-26 树rust-2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildPageTreeProjectionItems } from "@/lib/tree-projection";
|
||||
import { buildVisibleRows } from "./rows";
|
||||
import { buildVisibleRows, filterKernelFileTreeProjectionItems } from "./rows";
|
||||
import { parseFileTreeRowId } from "./types";
|
||||
|
||||
describe("buildVisibleRows", () => {
|
||||
@@ -283,6 +283,176 @@ describe("buildVisibleRows", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("过滤态应优先从 kernel file_tree items 收敛可见行,而不是回退 pageRows + assets 二次重建", () => {
|
||||
const fileTreeItems = [
|
||||
{
|
||||
rowId: "doc:page_root",
|
||||
rowKind: "document",
|
||||
nodeId: "page_root",
|
||||
parentNodeId: null,
|
||||
nodeType: "page",
|
||||
projectionKind: "file_tree",
|
||||
title: "根页面",
|
||||
depth: 0,
|
||||
position: 0,
|
||||
childCount: 3,
|
||||
expandable: true,
|
||||
expandedByDefault: true,
|
||||
capabilities: ["expand", "open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "document",
|
||||
documentId: "page_root",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "page",
|
||||
},
|
||||
iconHint: "page",
|
||||
},
|
||||
{
|
||||
rowId: "index:page_root",
|
||||
rowKind: "index",
|
||||
nodeId: "index:page_root",
|
||||
parentNodeId: "page_root",
|
||||
nodeType: "index",
|
||||
projectionKind: "file_tree",
|
||||
title: "index.md",
|
||||
depth: 1,
|
||||
position: 0,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "index",
|
||||
documentId: "page_root",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "index",
|
||||
},
|
||||
iconHint: "index",
|
||||
},
|
||||
{
|
||||
rowId: "asset-folder:mind_1",
|
||||
rowKind: "asset_folder",
|
||||
nodeId: "asset-folder:mind_1",
|
||||
parentNodeId: "page_root",
|
||||
nodeType: "mindmap",
|
||||
projectionKind: "file_tree",
|
||||
title: "头脑风暴",
|
||||
depth: 1,
|
||||
position: 1,
|
||||
childCount: 1,
|
||||
expandable: true,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["expand", "open-asset", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "mindmap",
|
||||
documentId: "page_root",
|
||||
assetId: "mind_1",
|
||||
workspaceId: "ws_1",
|
||||
assetKind: "mindmap",
|
||||
iconHint: "mindmap",
|
||||
},
|
||||
iconHint: "mindmap",
|
||||
},
|
||||
{
|
||||
rowId: "asset:asset_child_1",
|
||||
rowKind: "asset",
|
||||
nodeId: "asset:asset_child_1",
|
||||
parentNodeId: "asset-folder:mind_1",
|
||||
nodeType: "asset",
|
||||
projectionKind: "file_tree",
|
||||
title: "节点图片.png",
|
||||
depth: 2,
|
||||
position: 0,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open-asset", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "asset",
|
||||
documentId: "page_root",
|
||||
assetId: "asset_child_1",
|
||||
workspaceId: "ws_1",
|
||||
assetKind: "image",
|
||||
iconHint: "image",
|
||||
},
|
||||
iconHint: "image",
|
||||
},
|
||||
{
|
||||
rowId: "doc:page_child",
|
||||
rowKind: "document",
|
||||
nodeId: "page_child",
|
||||
parentNodeId: "page_root",
|
||||
nodeType: "page",
|
||||
projectionKind: "file_tree",
|
||||
title: "子页面",
|
||||
depth: 1,
|
||||
position: 2,
|
||||
childCount: 1,
|
||||
expandable: true,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["expand", "open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "document",
|
||||
documentId: "page_child",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "page",
|
||||
},
|
||||
iconHint: "page",
|
||||
},
|
||||
{
|
||||
rowId: "index:page_child",
|
||||
rowKind: "index",
|
||||
nodeId: "index:page_child",
|
||||
parentNodeId: "page_child",
|
||||
nodeType: "index",
|
||||
projectionKind: "file_tree",
|
||||
title: "index.md",
|
||||
depth: 2,
|
||||
position: 0,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "index",
|
||||
documentId: "page_child",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "index",
|
||||
},
|
||||
iconHint: "index",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const filteredItems = filterKernelFileTreeProjectionItems({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
visibleDocumentIds: new Set(["page_root", "page_child"]),
|
||||
expandedDocumentIds: new Set(["page_root"]),
|
||||
expandedAssetFolderIds: new Set(["mind_1"]),
|
||||
});
|
||||
|
||||
expect(filteredItems.map((item) => item.rowId)).toEqual([
|
||||
"doc:page_root",
|
||||
"index:page_root",
|
||||
"asset-folder:mind_1",
|
||||
"asset:asset_child_1",
|
||||
"doc:page_child",
|
||||
]);
|
||||
|
||||
const rows = buildVisibleRows({
|
||||
fileTreeItems: filteredItems,
|
||||
expanded: new Set(["page_root"]),
|
||||
expandedAssetFolderIds: new Set(["mind_1"]),
|
||||
});
|
||||
|
||||
expect(rows.map((row) => `${row.kind}:${row.rowId}`)).toEqual([
|
||||
"doc:doc:page_root",
|
||||
"index:index:page_root",
|
||||
"asset-folder:asset-folder:mind_1",
|
||||
"asset:asset:asset_child_1",
|
||||
"doc:doc:page_child",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseFileTreeRowId", () => {
|
||||
|
||||
@@ -12,6 +12,40 @@ import type { MediaAsset } from "@/types/media";
|
||||
import type { FileTreeRow } from "./types";
|
||||
import { makeAssetFolderRowId, makeAssetRowId, makeDocRowId, makeIndexRowId } from "./types";
|
||||
|
||||
export function filterKernelFileTreeProjectionItems(input: {
|
||||
fileTreeItems: KernelFileTreeProjectionItem[];
|
||||
visibleDocumentIds: ReadonlySet<string>;
|
||||
expandedDocumentIds: ReadonlySet<string>;
|
||||
expandedAssetFolderIds?: ReadonlySet<string>;
|
||||
}): KernelFileTreeProjectionItem[] {
|
||||
const expandedAssetFolderIds = input.expandedAssetFolderIds ?? new Set<string>();
|
||||
|
||||
return input.fileTreeItems.filter((item) => {
|
||||
const docId = getDocIdFromFileTreeItem(item);
|
||||
if (!input.visibleDocumentIds.has(docId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (item.rowKind) {
|
||||
case "document":
|
||||
return true;
|
||||
case "index":
|
||||
case "asset_folder":
|
||||
return input.expandedDocumentIds.has(docId);
|
||||
case "asset": {
|
||||
if (!input.expandedDocumentIds.has(docId)) {
|
||||
return false;
|
||||
}
|
||||
const parentNodeId = String(item.parentNodeId ?? "").trim();
|
||||
if (parentNodeId.startsWith("asset-folder:")) {
|
||||
return expandedAssetFolderIds.has(parentNodeId.slice("asset-folder:".length));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildRowsFromKernelFileTreeProjection(input: {
|
||||
fileTreeItems: KernelFileTreeProjectionItem[];
|
||||
expanded: Set<string>;
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import {
|
||||
buildFileTreeShellRowById,
|
||||
buildFileTreeShellVisibleRowIds,
|
||||
computeFileTreeShellDeleteTargets,
|
||||
getOrderedFileTreeShellRows,
|
||||
inferFileTreeShellTargetDocumentId,
|
||||
resolveFileTreeShellMindmapTargetId,
|
||||
} from "./shell";
|
||||
|
||||
describe("file-tree shell helpers", () => {
|
||||
const nodeById = new Map<string, SidebarTreeNode>([
|
||||
[
|
||||
"doc_root",
|
||||
{
|
||||
id: "doc_root",
|
||||
title: "根页面",
|
||||
} as SidebarTreeNode,
|
||||
],
|
||||
]);
|
||||
|
||||
const assetById = new Map<string, MediaAsset>([
|
||||
[
|
||||
"mind_1",
|
||||
{
|
||||
id: "mind_1",
|
||||
document_id: "doc_root",
|
||||
workspace_id: "ws_1",
|
||||
asset_type: "mindmap",
|
||||
file_name: "mindmap.json",
|
||||
storage_path: "mindmaps/mind_1/mindmap.json",
|
||||
} as MediaAsset,
|
||||
],
|
||||
[
|
||||
"asset_child_1",
|
||||
{
|
||||
id: "asset_child_1",
|
||||
document_id: "doc_root",
|
||||
workspace_id: "ws_1",
|
||||
asset_type: "file",
|
||||
file_name: "node.png",
|
||||
storage_path: "mindmaps/mind_1/assets/node.png",
|
||||
} as MediaAsset,
|
||||
],
|
||||
[
|
||||
"pdf_1",
|
||||
{
|
||||
id: "pdf_1",
|
||||
document_id: "doc_root",
|
||||
workspace_id: "ws_1",
|
||||
asset_type: "file",
|
||||
file_name: "guide.pdf",
|
||||
storage_path: "uploads/guide.pdf",
|
||||
} as MediaAsset,
|
||||
],
|
||||
]);
|
||||
|
||||
const fileTreeItems: KernelFileTreeProjectionItem[] = [
|
||||
{
|
||||
rowId: "doc:doc_root",
|
||||
rowKind: "document",
|
||||
nodeId: "doc_root",
|
||||
parentNodeId: null,
|
||||
nodeType: "page",
|
||||
projectionKind: "file_tree",
|
||||
title: "根页面",
|
||||
depth: 0,
|
||||
position: 0,
|
||||
childCount: 3,
|
||||
expandable: true,
|
||||
expandedByDefault: true,
|
||||
capabilities: ["expand", "open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "document",
|
||||
documentId: "doc_root",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "page",
|
||||
},
|
||||
iconHint: "page",
|
||||
},
|
||||
{
|
||||
rowId: "index:doc_root",
|
||||
rowKind: "index",
|
||||
nodeId: "index:doc_root",
|
||||
parentNodeId: "doc_root",
|
||||
nodeType: "index",
|
||||
projectionKind: "file_tree",
|
||||
title: "index.md",
|
||||
depth: 1,
|
||||
position: 0,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "index",
|
||||
documentId: "doc_root",
|
||||
workspaceId: "ws_1",
|
||||
iconHint: "index",
|
||||
},
|
||||
iconHint: "index",
|
||||
},
|
||||
{
|
||||
rowId: "asset-folder:mind_1",
|
||||
rowKind: "asset_folder",
|
||||
nodeId: "asset-folder:mind_1",
|
||||
parentNodeId: "doc_root",
|
||||
nodeType: "mindmap",
|
||||
projectionKind: "file_tree",
|
||||
title: "mindmap",
|
||||
depth: 1,
|
||||
position: 1,
|
||||
childCount: 1,
|
||||
expandable: true,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["expand", "open-asset", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "mindmap",
|
||||
documentId: "doc_root",
|
||||
assetId: "mind_1",
|
||||
workspaceId: "ws_1",
|
||||
assetKind: "mindmap",
|
||||
iconHint: "mindmap",
|
||||
},
|
||||
iconHint: "mindmap",
|
||||
},
|
||||
{
|
||||
rowId: "asset:asset_child_1",
|
||||
rowKind: "asset",
|
||||
nodeId: "asset:asset_child_1",
|
||||
parentNodeId: "asset-folder:mind_1",
|
||||
nodeType: "asset",
|
||||
projectionKind: "file_tree",
|
||||
title: "node.png",
|
||||
depth: 2,
|
||||
position: 0,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open-asset", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "asset",
|
||||
documentId: "doc_root",
|
||||
assetId: "asset_child_1",
|
||||
workspaceId: "ws_1",
|
||||
assetKind: "image",
|
||||
iconHint: "image",
|
||||
},
|
||||
iconHint: "image",
|
||||
},
|
||||
{
|
||||
rowId: "asset:pdf_1",
|
||||
rowKind: "asset",
|
||||
nodeId: "asset:pdf_1",
|
||||
parentNodeId: "doc_root",
|
||||
nodeType: "pdf",
|
||||
projectionKind: "file_tree",
|
||||
title: "guide.pdf",
|
||||
depth: 1,
|
||||
position: 2,
|
||||
childCount: 0,
|
||||
expandable: false,
|
||||
expandedByDefault: false,
|
||||
capabilities: ["open-asset", "select"],
|
||||
resourceMeta: {
|
||||
resourceKind: "pdf",
|
||||
documentId: "doc_root",
|
||||
assetId: "pdf_1",
|
||||
workspaceId: "ws_1",
|
||||
assetKind: "pdf",
|
||||
iconHint: "pdf",
|
||||
},
|
||||
iconHint: "pdf",
|
||||
},
|
||||
];
|
||||
|
||||
it("应直接从 kernel file_tree items 构造宿主 row map", () => {
|
||||
const rowById = buildFileTreeShellRowById({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
nodeById,
|
||||
assetById,
|
||||
});
|
||||
|
||||
expect(buildFileTreeShellVisibleRowIds([...fileTreeItems])).toEqual([
|
||||
"doc:doc_root",
|
||||
"index:doc_root",
|
||||
"asset-folder:mind_1",
|
||||
"asset:asset_child_1",
|
||||
"asset:pdf_1",
|
||||
]);
|
||||
|
||||
expect(rowById.get("doc:doc_root")).toMatchObject({
|
||||
rowId: "doc:doc_root",
|
||||
rowKind: "doc",
|
||||
documentId: "doc_root",
|
||||
node: expect.objectContaining({
|
||||
id: "doc_root",
|
||||
}),
|
||||
});
|
||||
expect(rowById.get("asset-folder:mind_1")).toMatchObject({
|
||||
rowId: "asset-folder:mind_1",
|
||||
rowKind: "asset-folder",
|
||||
documentId: "doc_root",
|
||||
assetId: "mind_1",
|
||||
asset: expect.objectContaining({
|
||||
id: "mind_1",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("应正确解析 file tree shell 的导图投放目标", () => {
|
||||
const rowById = buildFileTreeShellRowById({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
nodeById,
|
||||
assetById,
|
||||
});
|
||||
|
||||
expect(resolveFileTreeShellMindmapTargetId(rowById.get("asset-folder:mind_1") ?? null)).toBe(
|
||||
"mind_1",
|
||||
);
|
||||
expect(resolveFileTreeShellMindmapTargetId(rowById.get("asset:asset_child_1") ?? null)).toBe(
|
||||
"mind_1",
|
||||
);
|
||||
expect(resolveFileTreeShellMindmapTargetId(rowById.get("asset:pdf_1") ?? null)).toBeNull();
|
||||
});
|
||||
|
||||
it("应能仅凭 focusedRowId 从 shell row map 推回目标页面", () => {
|
||||
const rowById = buildFileTreeShellRowById({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
nodeById,
|
||||
assetById,
|
||||
});
|
||||
|
||||
expect(
|
||||
inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: "asset-folder:mind_1",
|
||||
rowById,
|
||||
activeDocId: null,
|
||||
}),
|
||||
).toBe("doc_root");
|
||||
expect(
|
||||
inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: "asset:pdf_1",
|
||||
rowById,
|
||||
activeDocId: null,
|
||||
}),
|
||||
).toBe("doc_root");
|
||||
expect(
|
||||
inferFileTreeShellTargetDocumentId({
|
||||
focusedRowId: null,
|
||||
rowById,
|
||||
activeDocId: "doc_root",
|
||||
}),
|
||||
).toBe("doc_root");
|
||||
});
|
||||
|
||||
it("应按当前可见顺序返回选中的 shell rows", () => {
|
||||
const rowById = buildFileTreeShellRowById({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
nodeById,
|
||||
assetById,
|
||||
});
|
||||
|
||||
expect(
|
||||
getOrderedFileTreeShellRows({
|
||||
rowIds: ["asset:pdf_1", "doc:doc_root", "missing"],
|
||||
visibleRowIds: buildFileTreeShellVisibleRowIds([...fileTreeItems]),
|
||||
rowById,
|
||||
}).map((row) => row.rowId),
|
||||
).toEqual(["doc:doc_root", "asset:pdf_1"]);
|
||||
});
|
||||
|
||||
it("删除目标计算应跳过被父页面覆盖的附件", () => {
|
||||
const rowById = buildFileTreeShellRowById({
|
||||
fileTreeItems: [...fileTreeItems],
|
||||
nodeById,
|
||||
assetById,
|
||||
});
|
||||
|
||||
const result = computeFileTreeShellDeleteTargets({
|
||||
visibleRowIds: buildFileTreeShellVisibleRowIds([...fileTreeItems]),
|
||||
rowById,
|
||||
selectedRowIds: new Set(["doc:doc_root", "asset:pdf_1", "asset-folder:mind_1"]),
|
||||
parentById: new Map([["doc_root", null]]),
|
||||
});
|
||||
|
||||
expect(result.docIds).toEqual(["doc_root"]);
|
||||
expect(result.assetIds).toEqual([]);
|
||||
expect(result.assetHints).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
||||
import {
|
||||
getDocIdFromFileTreeItem,
|
||||
resolveFileTreeRowAsset,
|
||||
resolveFileTreeRowNode,
|
||||
} from "@/lib/kernel-file-tree";
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import { filterTopLevelDocIds } from "./dnd";
|
||||
|
||||
export type FileTreeShellRowKind = "doc" | "index" | "asset" | "asset-folder";
|
||||
|
||||
export type FileTreeShellRow = {
|
||||
rowId: string;
|
||||
rowKind: FileTreeShellRowKind;
|
||||
documentId: string;
|
||||
assetId: string | null;
|
||||
node: SidebarTreeNode | null;
|
||||
asset: MediaAsset | null;
|
||||
};
|
||||
|
||||
export type FileTreeShellDeleteTargets = {
|
||||
docIds: string[];
|
||||
assetIds: string[];
|
||||
assetHints: MediaAsset[];
|
||||
};
|
||||
|
||||
function toShellRowKind(item: KernelFileTreeProjectionItem): FileTreeShellRowKind {
|
||||
switch (item.rowKind) {
|
||||
case "document":
|
||||
return "doc";
|
||||
case "asset_folder":
|
||||
return "asset-folder";
|
||||
default:
|
||||
return item.rowKind;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildFileTreeShellVisibleRowIds(
|
||||
fileTreeItems: readonly KernelFileTreeProjectionItem[],
|
||||
): string[] {
|
||||
return fileTreeItems
|
||||
.map((item) => item.rowId)
|
||||
.filter((rowId): rowId is string => typeof rowId === "string" && rowId.trim().length > 0);
|
||||
}
|
||||
|
||||
export function buildFileTreeShellRowById(input: {
|
||||
fileTreeItems: readonly KernelFileTreeProjectionItem[];
|
||||
nodeById?: Map<string, SidebarTreeNode>;
|
||||
assetById?: Map<string, MediaAsset>;
|
||||
}): Map<string, FileTreeShellRow> {
|
||||
const rowById = new Map<string, FileTreeShellRow>();
|
||||
|
||||
input.fileTreeItems.forEach((item) => {
|
||||
const rowId = typeof item.rowId === "string" ? item.rowId.trim() : "";
|
||||
if (!rowId || rowById.has(rowId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rowKind = toShellRowKind(item);
|
||||
const documentId = getDocIdFromFileTreeItem(item);
|
||||
const isDocumentRow = rowKind === "doc" || rowKind === "index";
|
||||
|
||||
rowById.set(rowId, {
|
||||
rowId,
|
||||
rowKind,
|
||||
documentId,
|
||||
assetId: isDocumentRow ? null : item.resourceMeta.assetId ?? null,
|
||||
node: isDocumentRow ? resolveFileTreeRowNode(item, input.nodeById) : null,
|
||||
asset: isDocumentRow ? null : resolveFileTreeRowAsset(item, input.assetById),
|
||||
});
|
||||
});
|
||||
|
||||
return rowById;
|
||||
}
|
||||
|
||||
export function getOrderedFileTreeShellRows(input: {
|
||||
rowIds: Iterable<string>;
|
||||
visibleRowIds: readonly string[];
|
||||
rowById: Map<string, FileTreeShellRow>;
|
||||
}): FileTreeShellRow[] {
|
||||
const selectedRowIds = new Set<string>();
|
||||
for (const rowId of input.rowIds) {
|
||||
if (typeof rowId !== "string" || rowId.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (!input.rowById.has(rowId)) {
|
||||
continue;
|
||||
}
|
||||
selectedRowIds.add(rowId);
|
||||
}
|
||||
|
||||
return input.visibleRowIds
|
||||
.map((rowId) => (selectedRowIds.has(rowId) ? input.rowById.get(rowId) ?? null : null))
|
||||
.filter((row): row is FileTreeShellRow => Boolean(row));
|
||||
}
|
||||
|
||||
export function extractMindmapAssetIdFromStoragePath(
|
||||
storagePath: string | null | undefined,
|
||||
): string | null {
|
||||
if (!storagePath) return null;
|
||||
const normalized = storagePath.replaceAll("\\", "/");
|
||||
|
||||
const prefix = "mindmaps/";
|
||||
if (normalized.startsWith(prefix)) {
|
||||
const rest = normalized.slice(prefix.length);
|
||||
const id = rest.split("/")[0];
|
||||
return id ? id : null;
|
||||
}
|
||||
|
||||
const marker = "/mindmaps/";
|
||||
const idx = normalized.indexOf(marker);
|
||||
if (idx === -1) return null;
|
||||
const rest = normalized.slice(idx + marker.length);
|
||||
const id = rest.split("/")[0];
|
||||
return id ? id : null;
|
||||
}
|
||||
|
||||
export function resolveFileTreeShellMindmapTargetId(
|
||||
row: FileTreeShellRow | null,
|
||||
): string | null {
|
||||
if (!row?.asset) {
|
||||
return null;
|
||||
}
|
||||
if (row.rowKind === "asset-folder" && row.asset.asset_type === "mindmap") {
|
||||
return row.asset.id;
|
||||
}
|
||||
if (row.rowKind === "asset") {
|
||||
return extractMindmapAssetIdFromStoragePath(row.asset.storage_path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function computeFileTreeShellDeleteTargets(input: {
|
||||
visibleRowIds: readonly string[];
|
||||
rowById: Map<string, FileTreeShellRow>;
|
||||
selectedRowIds: ReadonlySet<string>;
|
||||
parentById: Map<string, string | null>;
|
||||
}): FileTreeShellDeleteTargets {
|
||||
const rows = getOrderedFileTreeShellRows({
|
||||
rowIds: input.selectedRowIds,
|
||||
visibleRowIds: input.visibleRowIds,
|
||||
rowById: input.rowById,
|
||||
});
|
||||
|
||||
const docCandidates: string[] = [];
|
||||
const assetCandidates: string[] = [];
|
||||
const assetDocIdByAssetId = new Map<string, string>();
|
||||
const assetHintById = new Map<string, MediaAsset>();
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row.rowKind === "doc" || row.rowKind === "index") {
|
||||
docCandidates.push(row.documentId);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((row.rowKind === "asset" || row.rowKind === "asset-folder") && row.assetId) {
|
||||
assetCandidates.push(row.assetId);
|
||||
assetDocIdByAssetId.set(row.assetId, row.documentId);
|
||||
if (row.asset) {
|
||||
assetHintById.set(row.assetId, row.asset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const docIds = filterTopLevelDocIds(docCandidates, input.parentById);
|
||||
const docIdSet = new Set(docIds);
|
||||
const seenAssets = new Set<string>();
|
||||
const assetIds: string[] = [];
|
||||
const assetHints: MediaAsset[] = [];
|
||||
|
||||
assetCandidates.forEach((assetId) => {
|
||||
if (!assetId || seenAssets.has(assetId)) {
|
||||
return;
|
||||
}
|
||||
seenAssets.add(assetId);
|
||||
|
||||
const ownerDocId = assetDocIdByAssetId.get(assetId);
|
||||
if (ownerDocId && docIdSet.has(ownerDocId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assetIds.push(assetId);
|
||||
const assetHint = assetHintById.get(assetId);
|
||||
if (assetHint) {
|
||||
assetHints.push(assetHint);
|
||||
}
|
||||
});
|
||||
|
||||
return { docIds, assetIds, assetHints };
|
||||
}
|
||||
|
||||
export function inferFileTreeShellTargetDocumentId(input: {
|
||||
focusedRowId: string | null;
|
||||
rowById: Map<string, FileTreeShellRow>;
|
||||
activeDocId: string | null;
|
||||
}): string | null {
|
||||
if (input.focusedRowId) {
|
||||
const row = input.rowById.get(input.focusedRowId) ?? null;
|
||||
if (row?.documentId) {
|
||||
return row.documentId;
|
||||
}
|
||||
}
|
||||
return input.activeDocId || null;
|
||||
}
|
||||
Reference in New Issue
Block a user