chore(tree): remove legacy iframe tree shell

Remove the active TreeShellIframeHost path and legacy env flag so rust_family only mounts the Rust/WASM DOM shell host.

Move legacy iframe host files to ignored recycle storage, clear tracked recycle cache entries, and update tree-domain design wording to the current no-iframe position.

Align filetree default active rows with doc:<documentId> and update the renderer artifact bridge contract to dom_wasm.
This commit is contained in:
lix-2026
2026-05-16 08:57:40 +08:00
parent 384da4e44c
commit f9336257a5
52 changed files with 145 additions and 13538 deletions
@@ -3,19 +3,16 @@ import fs from "node:fs";
import path from "node:path";
const DOM_HOST_SOURCE = path.join(process.cwd(), "src/components/sidebar/tree-shell-dom-host.tsx");
const IFRAME_HOST_SOURCE = path.join(process.cwd(), "src/components/sidebar/tree-shell-iframe-host.tsx");
const SIDEBAR_SOURCE = path.join(process.cwd(), "src/components/sidebar/sidebar.tsx");
const SSR_LAYOUT_SOURCE = path.join(process.cwd(), "..", "rust/crates/mnote-web/src/ssr/pages/layout.rs");
describe("sidebar filetree DnD modifier source", () => {
it("copy modifier 应同时支持 Alt / Ctrl / Meta", () => {
const domHost = fs.readFileSync(DOM_HOST_SOURCE, "utf8");
const iframeHost = fs.readFileSync(IFRAME_HOST_SOURCE, "utf8");
const ssrLayout = fs.readFileSync(SSR_LAYOUT_SOURCE, "utf8");
expect(domHost).toContain('event.altKey || event.ctrlKey || event.metaKey ? "copy" : "move"');
expect(domHost).toContain("const copy = Boolean(event.altKey || event.ctrlKey || event.metaKey)");
expect(iframeHost).toContain("copy: event?.altKey === true || event?.ctrlKey === true || event?.metaKey === true");
expect(ssrLayout).toContain("var copyModifier = event.altKey || event.ctrlKey || event.metaKey");
expect(ssrLayout).toContain("copy: event.altKey === true || event.ctrlKey === true || event.metaKey === true");
});
@@ -378,7 +378,7 @@ export function TreeShellRustDomShellHost({
const [fileTreeState, setFileTreeState] = useState<FileTreeRuntimeState>(() => {
const selection = buildTreeShellDomFiletreeSelection(activeDocumentId);
return {
activeRowId: activeDocumentId ? `index:${activeDocumentId}` : null,
activeRowId: activeDocumentId ? `doc:${activeDocumentId}` : null,
selection,
dragRowIds: [],
dragEffect: null,
@@ -803,7 +803,7 @@ export function TreeShellRustDomShellHost({
}
updateFileTreeState((prev) => ({
...prev,
activeRowId: activeDocumentId ? `index:${activeDocumentId}` : null,
activeRowId: activeDocumentId ? `doc:${activeDocumentId}` : null,
selection: shouldBackfillSelection ? selection : prev.selection,
}));
}, [activeDocumentId, commitFileTreeSelection, mode, readFileTreeState, updateFileTreeState]);
@@ -149,10 +149,10 @@ export function buildTreeShellDomFiletreeSelection(activeDocumentId?: string | n
focusedRowId: null as string | null,
};
}
const indexRowId = `index:${documentId}`;
const documentRowId = `doc:${documentId}`;
return {
selectedRowIds: [indexRowId],
anchorRowId: indexRowId,
focusedRowId: indexRowId,
selectedRowIds: [documentRowId],
anchorRowId: documentRowId,
focusedRowId: documentRowId,
};
}
@@ -15,13 +15,10 @@ type PendingRuntimeRequest = {
describe("tree-shell-host", () => {
let container: HTMLDivElement;
let root: Root;
let previousLegacyFlag: string | undefined;
let originalFetch: typeof fetch | undefined;
let pendingRuntimeRequests: PendingRuntimeRequest[];
beforeEach(() => {
previousLegacyFlag = process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
@@ -54,21 +51,16 @@ describe("tree-shell-host", () => {
} else {
delete (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch;
}
if (previousLegacyFlag === undefined) {
delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
} else {
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = previousLegacyFlag;
}
});
const fileTreeItems: KernelFileTreeProjectionItem[] = [
{
projectionKind: "file_tree",
rowId: "index:doc_1",
rowKind: "index",
nodeId: "doc_1:index",
rowId: "doc:doc_1",
rowKind: "document",
nodeId: "doc_1",
parentNodeId: null,
title: "Doc 1 索引",
title: "Doc 1.md",
depth: 0,
childCount: 0,
position: 0,
@@ -84,11 +76,11 @@ describe("tree-shell-host", () => {
},
{
projectionKind: "file_tree",
rowId: "index:doc_2",
rowKind: "index",
nodeId: "doc_2:index",
rowId: "doc:doc_2",
rowKind: "document",
nodeId: "doc_2",
parentNodeId: null,
title: "Doc 2 索引",
title: "Doc 2.md",
depth: 0,
childCount: 0,
position: 1,
@@ -104,11 +96,11 @@ describe("tree-shell-host", () => {
},
{
projectionKind: "file_tree",
rowId: "index:doc_3",
rowKind: "index",
nodeId: "doc_3:index",
rowId: "doc:doc_3",
rowKind: "document",
nodeId: "doc_3",
parentNodeId: null,
title: "Doc 3 索引",
title: "Doc 3.md",
depth: 0,
childCount: 0,
position: 2,
@@ -202,7 +194,7 @@ describe("tree-shell-host", () => {
});
}
it("rust_family 默认选择 Rust/WASM DOM shell host,不能进入 iframe_srcdoc", () => {
it("rust_family 默认选择 Rust/WASM DOM shell host,不再提供 iframe_srcdoc 回退", () => {
renderHost();
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
@@ -219,49 +211,31 @@ describe("tree-shell-host", () => {
expect(container.querySelector('[data-testid="legacy-react-fallback"]')).toBeNull();
});
it("只有显式 legacy flag 才允许进入旧 TreeShellIframeHost", async () => {
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = "1";
await act(async () => {
renderHost();
await Promise.resolve();
});
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_runtime_artifact_host");
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]')).toBeNull();
expect(iframe).not.toBeNull();
expect(iframe?.getAttribute("data-tree-browser-bridge")).toBe("iframe_srcdoc");
expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
});
it("filetree DOM host 应先本地提交 Ctrl/Shift selection,再异步等 runtime 对账", async () => {
renderDomHost();
const row2 = queryFileTreeRow("index:doc_2");
const row3 = queryFileTreeRow("index:doc_3");
const row2 = queryFileTreeRow("doc:doc_2");
const row3 = queryFileTreeRow("doc:doc_3");
expect(row2).not.toBeNull();
expect(row3).not.toBeNull();
act(() => {
row2?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
});
expectSelectedRowIds(["index:doc_2"]);
expectSelectedRowIds(["doc:doc_2"]);
act(() => {
row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, shiftKey: true }));
});
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
});
it("旧 runtime selection 回写不应覆盖更新后的多选,Delete 应使用当前稳定 selection", async () => {
const handleDeleteSelection = vi.fn();
renderDomHost({ onFileTreeDeleteSelection: handleDeleteSelection });
const row2 = queryFileTreeRow("index:doc_2");
const row3 = queryFileTreeRow("index:doc_3");
const row2 = queryFileTreeRow("doc:doc_2");
const row3 = queryFileTreeRow("doc:doc_3");
expect(row2).not.toBeNull();
expect(row3).not.toBeNull();
@@ -272,7 +246,7 @@ describe("tree-shell-host", () => {
act(() => {
row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
});
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
await flushRuntimeDispatch();
act(() => {
@@ -284,9 +258,9 @@ describe("tree-shell-host", () => {
state: {
activeRowId: null,
selection: {
selectedRowIds: ["index:doc_2"],
anchorRowId: "index:doc_2",
focusedRowId: "index:doc_2",
selectedRowIds: ["doc:doc_2"],
anchorRowId: "doc:doc_2",
focusedRowId: "doc:doc_2",
},
dragRowIds: [],
dragEffect: null,
@@ -296,7 +270,7 @@ describe("tree-shell-host", () => {
});
});
await flushRuntimeDispatch();
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
act(() => {
row3?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" }));
@@ -305,9 +279,9 @@ describe("tree-shell-host", () => {
expect(handleDeleteSelection).toHaveBeenCalledTimes(1);
expect(handleDeleteSelection.mock.calls[0]?.[0]).toMatchObject({
selectedRowIds: ["index:doc_2", "index:doc_3"],
anchorRowId: "index:doc_3",
focusedRowId: "index:doc_3",
selectedRowIds: ["doc:doc_2", "doc:doc_3"],
anchorRowId: "doc:doc_3",
focusedRowId: "doc:doc_3",
});
});
@@ -315,8 +289,8 @@ describe("tree-shell-host", () => {
const handleDeleteSelection = vi.fn();
renderDomHost({ onFileTreeDeleteSelection: handleDeleteSelection });
const row2 = queryFileTreeRow("index:doc_2");
const row3 = queryFileTreeRow("index:doc_3");
const row2 = queryFileTreeRow("doc:doc_2");
const row3 = queryFileTreeRow("doc:doc_3");
expect(row2).not.toBeNull();
expect(row3).not.toBeNull();
@@ -327,7 +301,7 @@ describe("tree-shell-host", () => {
act(() => {
row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
});
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
act(() => {
row3?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Backspace" }));
@@ -336,19 +310,19 @@ describe("tree-shell-host", () => {
expect(handleDeleteSelection).toHaveBeenCalledTimes(1);
expect(handleDeleteSelection.mock.calls[0]?.[0]).toMatchObject({
selectedRowIds: ["index:doc_2", "index:doc_3"],
anchorRowId: "index:doc_3",
focusedRowId: "index:doc_3",
selectedRowIds: ["doc:doc_2", "doc:doc_3"],
anchorRowId: "doc:doc_3",
focusedRowId: "doc:doc_3",
});
});
it("点击行内更多操作时不应因为事件冒泡把多选压成单选", async () => {
renderDomHost();
const row2 = queryFileTreeRow("index:doc_2");
const row3 = queryFileTreeRow("index:doc_3");
const row2 = queryFileTreeRow("doc:doc_2");
const row3 = queryFileTreeRow("doc:doc_3");
const row3Menu = container.querySelector(
'[data-row-id="index:doc_3"] [data-testid="filetree-action-menu"]',
'[data-row-id="doc:doc_3"] [data-testid="filetree-action-menu"]',
) as HTMLElement | null;
expect(row2).not.toBeNull();
expect(row3).not.toBeNull();
@@ -360,21 +334,21 @@ describe("tree-shell-host", () => {
act(() => {
row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
});
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
act(() => {
row3Menu?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushRuntimeDispatch();
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
});
it("activeDocumentId 变化时,已有 filetree selection 不应被无条件重置", () => {
renderDomHost();
const row2 = queryFileTreeRow("index:doc_2");
const row3 = queryFileTreeRow("index:doc_3");
const row2 = queryFileTreeRow("doc:doc_2");
const row3 = queryFileTreeRow("doc:doc_3");
expect(row2).not.toBeNull();
expect(row3).not.toBeNull();
@@ -384,18 +358,18 @@ describe("tree-shell-host", () => {
act(() => {
row3?.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
});
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
renderDomHost({ activeDocumentId: "doc_1" });
expectSelectedRowIds(["index:doc_2", "index:doc_3"]);
expectSelectedRowIds(["doc:doc_2", "doc:doc_3"]);
});
it("F2 应进入 filetree 行内重命名,并用 tree.node.rename 提交页面标题", async () => {
const handleTreeMutation = vi.fn();
renderDomHost({ onTreeMutation: handleTreeMutation });
const row2 = queryFileTreeRow("index:doc_2");
const row2 = queryFileTreeRow("doc:doc_2");
expect(row2).not.toBeNull();
act(() => {
@@ -403,7 +377,7 @@ describe("tree-shell-host", () => {
});
await flushRuntimeDispatch();
const input = container.querySelector('.tree-rename-input[data-rename-id="index:doc_2"]') as HTMLInputElement | null;
const input = container.querySelector('.tree-rename-input[data-rename-id="doc:doc_2"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
act(() => {
input!.value = "Doc 2 Renamed";
@@ -448,7 +422,7 @@ describe("tree-shell-host", () => {
it("F2 行内重命名应在输入框内阻断空名、非法字符和同级重名", async () => {
renderDomHost();
const row2 = queryFileTreeRow("index:doc_2");
const row2 = queryFileTreeRow("doc:doc_2");
expect(row2).not.toBeNull();
act(() => {
@@ -456,7 +430,7 @@ describe("tree-shell-host", () => {
});
await flushRuntimeDispatch();
const input = container.querySelector('.tree-rename-input[data-rename-id="index:doc_2"]') as HTMLInputElement | null;
const input = container.querySelector('.tree-rename-input[data-rename-id="doc:doc_2"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
act(() => {
@@ -483,7 +457,7 @@ describe("tree-shell-host", () => {
expect(pendingRuntimeRequests.some((entry) => (entry.request as { action?: unknown }).action === "rename")).toBe(false);
act(() => {
input!.value = "Doc 1 索引";
input!.value = "Doc 1.md";
input!.dispatchEvent(new Event("input", { bubbles: true }));
});
await act(async () => {
@@ -3,9 +3,6 @@
import type { ReactNode } from "react";
import { TreeShellRustDomShellHost } from "@/components/sidebar/tree-shell-dom-host";
import type { TreeShellPickerItem } from "@/components/sidebar/tree-shell-dom-model";
import {
TreeShellIframeHost,
} from "@/components/sidebar/tree-shell-iframe-host";
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
import { cn } from "@/lib/utils";
@@ -16,7 +13,6 @@ export type TreeShellHostMode = "page" | "filetree" | "picker";
const RUST_RENDERER_CONTRACT = "rust_renderer_input_v1";
const RUST_WASM_DOM_SHELL_HOST = "rust_wasm_dom_shell_host";
const RUST_LEGACY_IFRAME_HOST = "rust_runtime_artifact_host";
export type TreeShellPickerCommand = {
kind: "next" | "previous" | "home" | "end" | "pick";
@@ -146,16 +142,10 @@ export function TreeShellHost({
}: TreeShellHostProps) {
const useRustHost = rendererFamily === "rust_family";
const useDomHost = useRustHost && Boolean(workspaceId?.trim());
const useLegacyIframeHost =
useDomHost &&
typeof process !== "undefined" &&
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST === "1";
const hostKind = rendererFamily === "rust_family" ? "rust_family" : "react";
const rendererContract = useRustHost ? RUST_RENDERER_CONTRACT : undefined;
const implementation = useLegacyIframeHost
? RUST_LEGACY_IFRAME_HOST
: useDomHost
? RUST_WASM_DOM_SHELL_HOST
const implementation = useDomHost
? RUST_WASM_DOM_SHELL_HOST
: fallbackImplementation ??
(rendererFamily === "rust_family" ? "react_fallback" : "react_primary");
@@ -180,71 +170,38 @@ export function TreeShellHost({
className="contents"
>
{useDomHost && workspaceId ? (
useLegacyIframeHost ? (
<TreeShellIframeHost
mode={mode}
surfaceTestId={surfaceTestId}
workspaceId={workspaceId}
rootNodeId={rootNodeId}
activeDocumentId={activeDocumentId}
focusedDocumentId={focusedDocumentId}
activePickerItemKey={activePickerItemKey}
allowRootPick={allowRootPick}
excludeIds={excludeIds}
pickerCommand={pickerCommand}
pickerItems={pickerItems}
pageTreeItems={pageTreeItems}
inlineFileTreeItems={inlineFileTreeItems}
channel={channel}
host={host}
onNavigate={onNavigate}
onPick={onPick}
onPickerFocusChange={onPickerFocusChange}
onPageContextMenu={onPageContextMenu}
onPageExpandChange={onPageExpandChange}
onPageFocusChange={onPageFocusChange}
onFileTreeContextMenu={onFileTreeContextMenu}
onFileTreeSelectionChange={onFileTreeSelectionChange}
onFileTreeDeleteSelection={onFileTreeDeleteSelection}
onInternalDrop={onInternalDrop}
onDropFiles={onDropFiles}
onAssetOpen={onAssetOpen}
onTreeMutation={onTreeMutation}
/>
) : (
<TreeShellRustDomShellHost
mode={mode}
surfaceTestId={surfaceTestId}
workspaceId={workspaceId}
rootNodeId={rootNodeId}
activeDocumentId={activeDocumentId}
focusedDocumentId={focusedDocumentId}
activePickerItemKey={activePickerItemKey}
allowRootPick={allowRootPick}
excludeIds={excludeIds}
pickerCommand={pickerCommand}
pickerItems={pickerItems}
pageTreeItems={pageTreeItems}
inlineFileTreeItems={inlineFileTreeItems}
channel={channel}
host={host}
onNavigate={onNavigate}
onPick={onPick}
onPickerFocusChange={onPickerFocusChange}
onPageContextMenu={onPageContextMenu}
onPageExpandChange={onPageExpandChange}
onPageFocusChange={onPageFocusChange}
onFileTreeContextMenu={onFileTreeContextMenu}
onFileTreeSelectionChange={onFileTreeSelectionChange}
onFileTreeDeleteSelection={onFileTreeDeleteSelection}
onInternalDrop={onInternalDrop}
onDropFiles={onDropFiles}
onAssetOpen={onAssetOpen}
onTreeMutation={onTreeMutation}
>
{children}
</TreeShellRustDomShellHost>
)
<TreeShellRustDomShellHost
mode={mode}
surfaceTestId={surfaceTestId}
workspaceId={workspaceId}
rootNodeId={rootNodeId}
activeDocumentId={activeDocumentId}
focusedDocumentId={focusedDocumentId}
activePickerItemKey={activePickerItemKey}
allowRootPick={allowRootPick}
excludeIds={excludeIds}
pickerCommand={pickerCommand}
pickerItems={pickerItems}
pageTreeItems={pageTreeItems}
inlineFileTreeItems={inlineFileTreeItems}
channel={channel}
host={host}
onNavigate={onNavigate}
onPick={onPick}
onPickerFocusChange={onPickerFocusChange}
onPageContextMenu={onPageContextMenu}
onPageExpandChange={onPageExpandChange}
onPageFocusChange={onPageFocusChange}
onFileTreeContextMenu={onFileTreeContextMenu}
onFileTreeSelectionChange={onFileTreeSelectionChange}
onFileTreeDeleteSelection={onFileTreeDeleteSelection}
onInternalDrop={onInternalDrop}
onDropFiles={onDropFiles}
onAssetOpen={onAssetOpen}
onTreeMutation={onTreeMutation}
>
{children}
</TreeShellRustDomShellHost>
) : (
children
)}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff