diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
index f84765f4..899129b1 100644
--- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
+++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md
@@ -169,7 +169,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
- [ ] C4. `sidebar-filetree-command-runtime.js`:create/rename/delete/copy/move/trash/restore/purge command payload。
- [ ] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。
- [ ] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。
-- [ ] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
+- [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
验收命令:
diff --git a/rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
new file mode 100644
index 00000000..265284ef
--- /dev/null
+++ b/rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
@@ -0,0 +1,911 @@
+export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
+ const {
+ activeSidebarTreeMode,
+ cssEscape,
+ currentDocumentId,
+ currentFileTreeActiveRowId,
+ currentRootUri,
+ currentSourceKind,
+ currentWorkspaceId,
+ escapeHtml,
+ fileTreeRuntimeFunction,
+ localFilePathFromAssetId,
+ navigateToDocument,
+ refreshEditorLocalAttachmentExistence,
+ restoreSidebarTreeTab,
+ rowTitle,
+ schedulePendingLocalFolderRestoreFocus,
+ shortMindmapFileName,
+ syncSidebarFileTreeSelection,
+ } = dependencies;
+
+ function updateTitleEverywhere(documentId, title) {
+ if (!documentId) return;
+ var escaped = cssEscape(documentId);
+ var escapedDocRowId = cssEscape('doc:' + documentId);
+ var isCurrentDocument = currentDocumentId() === documentId;
+ var pageSelectors = [
+ '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
+ '.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
+ 'a[href="/documents/' + escaped + '"] > .wolai-row-title',
+ 'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
+ ];
+ pageSelectors.forEach(function(selector) {
+ document.querySelectorAll(selector).forEach(function(node) {
+ if (node instanceof HTMLElement) node.textContent = title;
+ });
+ });
+ document.querySelectorAll('.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title').forEach(function(node) {
+ if (node instanceof HTMLElement) node.textContent = fileTreePageTitle(title);
+ });
+ document.querySelectorAll('[data-page-title-input="true"][data-document-id="' + escaped + '"]').forEach(function(node) {
+ if (!(node instanceof HTMLTextAreaElement)) return;
+ node.value = title;
+ node.setAttribute('data-title-last-saved', title);
+ node.setAttribute('data-title-save-status', 'saved');
+ node.style.height = 'auto';
+ node.style.height = Math.max(48, node.scrollHeight) + 'px';
+ });
+ document.querySelectorAll('[data-document-pane="true"][data-pane-document-id="' + escaped + '"] [data-page-title-current="true"]').forEach(function(node) {
+ if (node instanceof HTMLElement) node.textContent = title;
+ });
+ if (isCurrentDocument) {
+ document.title = title;
+ var topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
+ if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
+ }
+ }
+
+ function fileTreePageTitle(title) {
+ var normalized = String(title || '无标题').trim() || '无标题';
+ return normalized.endsWith('.md') ? normalized : normalized + '.md';
+ }
+
+ function isFileTreePageRow(row) {
+ if (!(row instanceof HTMLElement)) return false;
+ if (row.getAttribute('data-asset-id')) return false;
+ var rowKind = row.getAttribute('data-row-kind') || '';
+ return rowKind === 'document' || rowKind === 'doc' || rowKind === 'index' || rowKind === 'markdown';
+ }
+
+ function normalizeFileTreePageRenameTitle(value) {
+ var normalized = String(value || '').trim();
+ if (/\.md$/i.test(normalized)) normalized = normalized.slice(0, -3).trim();
+ return normalized;
+ }
+
+ function validateFileTreeRename(row, draft) {
+ var raw = String(draft || '').trim();
+ if (!raw) return '名称不能为空';
+ if (/[\\/:*?"<>|]/.test(raw)) return '文件名不能包含 / \\ : * ? " < > |';
+ if (!isFileTreePageRow(row)) return '';
+ var pageTitle = normalizeFileTreePageRenameTitle(raw);
+ if (!pageTitle) return '名称不能为空';
+ var expectedFileName = fileTreePageTitle(pageTitle).toLocaleLowerCase();
+ var rowId = row.getAttribute('data-row-id') || '';
+ var parentId = row.getAttribute('data-parent-id') || '';
+ var siblings = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="document"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="doc"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="markdown"]'));
+ var duplicate = siblings.some(function(sibling) {
+ if (!(sibling instanceof HTMLElement)) return false;
+ if ((sibling.getAttribute('data-row-id') || '') === rowId) return false;
+ if ((sibling.getAttribute('data-parent-id') || '') !== parentId) return false;
+ return fileTreePageTitle(normalizeFileTreePageRenameTitle(rowTitle(sibling))).toLocaleLowerCase() === expectedFileName;
+ });
+ return duplicate ? '同级已存在同名页面' : '';
+ }
+
+ function documentIdFromDelta(data) {
+ return String(data && (data.documentId || data.id || (data.document && data.document.id) || (data.node && data.node.id) || (data.args && data.args.documentId)) || '').trim();
+ }
+
+ function parentIdFromDelta(data) {
+ if (!data || typeof data !== 'object') return null;
+ if (Object.prototype.hasOwnProperty.call(data, 'parentId')) return data.parentId ? String(data.parentId).trim() : null;
+ if (Object.prototype.hasOwnProperty.call(data, 'parent_id')) return data.parent_id ? String(data.parent_id).trim() : null;
+ if (data.args && Object.prototype.hasOwnProperty.call(data.args, 'parentId')) return data.args.parentId ? String(data.args.parentId).trim() : null;
+ return null;
+ }
+
+ function treeRootForMode(mode) {
+ var id = mode === 'filetree' ? 'sidebar-file-tree-root' : 'sidebar-tree-root';
+ return document.querySelector('#' + id + ' .tree-root');
+ }
+
+ function rowSelectorForDocument(mode, documentId) {
+ var escaped = cssEscape(documentId);
+ if (mode === 'filetree') {
+ return '.tree-row[data-shell-mode="filetree"][data-doc-id="' + escaped + '"], .tree-row[data-shell-mode="filetree"][data-document-id="' + escaped + '"]';
+ }
+ return '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"]';
+ }
+
+ function ensureTreeChildren(parentRow) {
+ var parentNode = parentRow ? parentRow.closest('.tree-node') : null;
+ if (!parentNode) return null;
+ var children = parentNode.querySelector(':scope > .tree-children');
+ if (!children) {
+ children = document.createElement('ul');
+ children.className = 'tree-children';
+ parentNode.appendChild(children);
+ }
+ children.classList.remove('tree-children--collapsed');
+ parentRow.setAttribute('aria-expanded', 'true');
+ var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
+ if (toggle) toggle.setAttribute('aria-expanded', 'true');
+ return children;
+ }
+
+ function removeDocumentRowForMode(mode, documentId) {
+ var row = document.querySelector(rowSelectorForDocument(mode, documentId));
+ var node = row ? row.closest('.tree-node') : null;
+ if (!node || !node.parentElement) return false;
+ node.parentElement.removeChild(node);
+ return true;
+ }
+
+ function commandDocumentId(result, fallback) {
+ var value = result && (
+ (result.execution && (result.execution.documentId || result.execution.id || result.execution.nodeId)) ||
+ result.documentId ||
+ result.id ||
+ result.nodeId ||
+ (result.document && (result.document.id || result.document.documentId)) ||
+ (result.node && (result.node.id || result.node.documentId)) ||
+ (result.payload && result.payload.documentId)
+ );
+ return String(value || fallback || '').trim();
+ }
+
+ function commandDocumentTitle(result, fallback) {
+ var value = result && (
+ (result.execution && result.execution.title) ||
+ result.title ||
+ (result.document && result.document.title) ||
+ (result.node && result.node.title) ||
+ (result.payload && result.payload.title)
+ );
+ return String(value || fallback || '新页面').trim() || '新页面';
+ }
+
+ function normalizeDocumentRowId(value) {
+ return String(value || '').trim().replace(/^doc:/, '').replace(/^index:/, '');
+ }
+
+ function appendRenderedTreeNode(container, html) {
+ if (!container || !html) return false;
+ var empty = container.querySelector(':scope > .tree-empty');
+ if (empty && empty.parentElement) empty.parentElement.removeChild(empty);
+ var template = document.createElement('template');
+ template.innerHTML = html;
+ var node = template.content.firstElementChild;
+ if (!node) return false;
+ container.appendChild(node);
+ return true;
+ }
+
+ function localPageInsertDepth(parentId) {
+ if (!parentId) return 0;
+ var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
+ var depth = parentRow ? Number(parentRow.getAttribute('data-depth') || 0) : 0;
+ return Number.isFinite(depth) ? depth + 1 : 1;
+ }
+
+ function upsertPageDocumentRow(documentId, parentId, title) {
+ if (!documentId) return false;
+ var existing = document.querySelector(rowSelectorForDocument('page', documentId));
+ if (existing instanceof HTMLElement) {
+ updateTitleEverywhere(documentId, title);
+ moveDocumentRowForMode('page', documentId, parentId || null);
+ return true;
+ }
+ var root = treeRootForMode('page');
+ if (!root) return false;
+ var targetContainer = root;
+ if (parentId) {
+ var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
+ targetContainer = ensureTreeChildren(parentRow);
+ if (!targetContainer) targetContainer = root;
+ }
+ var grouped = new Map();
+ grouped.set(parentId || '', [{
+ id: documentId,
+ nodeId: documentId,
+ documentId: documentId,
+ parentNodeId: parentId || '',
+ rowKind: 'document',
+ title: title,
+ expandable: false,
+ childCount: 0
+ }]);
+ return appendRenderedTreeNode(targetContainer, renderPageRows(parentId || '', grouped, currentDocumentId(), localPageInsertDepth(parentId)));
+ }
+
+ function fileTreeParentContainer(parentId) {
+ var root = treeRootForMode('filetree');
+ if (!root) return null;
+ if (!parentId) return root;
+ var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
+ var children = ensureTreeChildren(parentRow);
+ return children || root;
+ }
+
+ function localFileInsertDepth(parentId) {
+ if (!parentId) return 0;
+ var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
+ var level = parentRow ? Number(parentRow.getAttribute('aria-level') || 1) : 1;
+ return Number.isFinite(level) ? level : 1;
+ }
+
+ function upsertFileDocumentRows(documentId, parentId, title) {
+ if (!documentId) return false;
+ var existing = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
+ if (existing instanceof HTMLElement) {
+ updateTitleEverywhere(documentId, title);
+ moveDocumentRowForMode('filetree', documentId, parentId || null);
+ return true;
+ }
+ var targetContainer = fileTreeParentContainer(parentId);
+ if (!targetContainer) return false;
+ var parentNodeId = parentId ? 'doc:' + parentId : '';
+ var docNodeId = 'doc:' + documentId;
+ var depth = localFileInsertDepth(parentId);
+ var grouped = new Map();
+ grouped.set(parentNodeId, [{
+ id: docNodeId,
+ nodeId: docNodeId,
+ rowId: docNodeId,
+ rowKind: 'document',
+ title: fileTreePageTitle(title),
+ documentId: documentId,
+ parentNodeId: parentNodeId,
+ depth: depth,
+ expandable: false,
+ childCount: 0
+ }]);
+ return appendRenderedTreeNode(targetContainer, renderFileRows(parentNodeId, grouped, currentDocumentId()));
+ }
+
+ function applyCreatedDocumentLocally(result, parentId, fallbackTitle) {
+ var documentId = commandDocumentId(result, null);
+ if (!documentId) return false;
+ var normalizedParentId = normalizeDocumentRowId(parentId);
+ var title = commandDocumentTitle(result, fallbackTitle);
+ var pageChanged = upsertPageDocumentRow(documentId, normalizedParentId, title);
+ var fileChanged = upsertFileDocumentRows(documentId, normalizedParentId, title);
+ if (pageChanged || fileChanged) {
+ document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'create');
+ }
+ return pageChanged || fileChanged;
+ }
+
+ function localCommandNeedsProjectionRefresh(action, result) {
+ if (currentSourceKind() !== 'local_folder') return false;
+ if (action === 'create' || action === 'rename' || action === 'move' || action === 'delete' || action === 'archive' || action === 'trash' || action === 'purge') {
+ return true;
+ }
+ if (result && (result.previousDocumentId || result.previousRelativePath || result.relativePath || result.trashPath)) {
+ return true;
+ }
+ return false;
+ }
+
+ function sortOrderFromDelta(data) {
+ var raw = data && (data.sortOrder ?? data.sort_order);
+ var value = Number(raw);
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
+ }
+
+ function insertTreeNodeAtSortOrder(targetContainer, node, sortOrder) {
+ if (!targetContainer || !node) return false;
+ if (sortOrder === null || sortOrder === undefined) {
+ targetContainer.appendChild(node);
+ return true;
+ }
+ var siblings = Array.from(targetContainer.querySelectorAll(':scope > .tree-node')).filter(function(candidate) {
+ return candidate !== node;
+ });
+ var targetIndex = Math.max(0, Math.min(Number(sortOrder), siblings.length));
+ var referenceNode = siblings[targetIndex] || null;
+ if (referenceNode) targetContainer.insertBefore(node, referenceNode);
+ else targetContainer.appendChild(node);
+ return true;
+ }
+
+ function moveDocumentRowForMode(mode, documentId, parentId, sortOrder) {
+ var row = document.querySelector(rowSelectorForDocument(mode, documentId));
+ var node = row ? row.closest('.tree-node') : null;
+ var root = treeRootForMode(mode);
+ if (!row || !node || !root) return false;
+ var targetContainer = root;
+ if (parentId) {
+ var parentRow = document.querySelector(rowSelectorForDocument(mode, parentId));
+ targetContainer = ensureTreeChildren(parentRow);
+ if (!targetContainer) return false;
+ row.setAttribute('data-parent-id', parentId);
+ } else {
+ row.removeAttribute('data-parent-id');
+ }
+ return insertTreeNodeAtSortOrder(targetContainer, node, sortOrder);
+ }
+
+ function applyMoveDocumentDelta(data) {
+ var documentId = documentIdFromDelta(data);
+ if (!documentId) return false;
+ var parentId = parentIdFromDelta(data);
+ var sortOrder = sortOrderFromDelta(data);
+ var movedPage = moveDocumentRowForMode('page', documentId, parentId, sortOrder);
+ var movedFile = moveDocumentRowForMode('filetree', documentId, parentId, sortOrder);
+ return movedPage || movedFile;
+ }
+
+ function applyRemoveDocumentDelta(data) {
+ var documentId = documentIdFromDelta(data);
+ if (!documentId) return false;
+ var removedPage = removeDocumentRowForMode('page', documentId);
+ var removedFile = removeDocumentRowForMode('filetree', documentId);
+ return removedPage || removedFile;
+ }
+
+ function readProjection(value) {
+ var runtimeFn = fileTreeRuntimeFunction('readProjection');
+ if (runtimeFn) return runtimeFn(value);
+ if (!value || typeof value !== 'object') return null;
+ if (value.result && typeof value.result === 'object') return value.result;
+ if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
+ if (value.data && value.data.tree) return value.data.tree;
+ if (value.tree && typeof value.tree === 'object') return value.tree;
+ return value;
+ }
+
+ function readSidebarDataset(value) {
+ var runtimeFn = fileTreeRuntimeFunction('readSidebarDataset');
+ if (runtimeFn) return runtimeFn(value);
+ if (!value || typeof value !== 'object') return null;
+ if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset;
+ if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset;
+ if (value.dataset && typeof value.dataset === 'object') return value.dataset;
+ if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar;
+ return null;
+ }
+
+ function readDatasetProjection(value, snakeCaseKey, camelCaseKey) {
+ var runtimeFn = fileTreeRuntimeFunction('readDatasetProjection');
+ if (runtimeFn) return runtimeFn(value, snakeCaseKey, camelCaseKey);
+ var dataset = readSidebarDataset(value);
+ if (!dataset || typeof dataset !== 'object') return null;
+ if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey];
+ if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey];
+ return null;
+ }
+
+ function setTreeLiveApplyError(reason) {
+ document.documentElement.setAttribute('data-mnote-tree-live-apply-error', String(reason || 'tree_live_apply_failed'));
+ }
+
+ function projectionItems(projection) {
+ var runtimeFn = fileTreeRuntimeFunction('projectionItems');
+ if (runtimeFn) return runtimeFn(projection);
+ var resolved = readProjection(projection);
+ return resolved && Array.isArray(resolved.items) ? resolved.items : [];
+ }
+
+ function hasProjectionItems(projection) {
+ var runtimeFn = fileTreeRuntimeFunction('hasProjectionItems');
+ if (runtimeFn) return runtimeFn(projection);
+ var resolved = readProjection(projection);
+ return Boolean(resolved && Array.isArray(resolved.items));
+ }
+
+ function nodeIdOf(item) {
+ var runtimeFn = fileTreeRuntimeFunction('nodeIdOf');
+ if (runtimeFn) return runtimeFn(item);
+ return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
+ }
+
+ function rowIdOf(item) {
+ var runtimeFn = fileTreeRuntimeFunction('rowIdOf');
+ if (runtimeFn) return runtimeFn(item);
+ return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
+ }
+
+ function parentIdOf(item) {
+ var runtimeFn = fileTreeRuntimeFunction('parentIdOf');
+ if (runtimeFn) return runtimeFn(item);
+ return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
+ }
+
+ function titleOf(item) {
+ var runtimeFn = fileTreeRuntimeFunction('titleOf');
+ if (runtimeFn) return runtimeFn(item);
+ return String(item && item.title || '无标题').trim() || '无标题';
+ }
+
+ function fileWorkspaceRelativePath(item) {
+ var runtimeFn = fileTreeRuntimeFunction('fileWorkspaceRelativePath');
+ if (runtimeFn) return runtimeFn(item);
+ var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
+ var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {};
+ var fromWorkspacePath = String(workspacePath.relativePath || '').trim();
+ if (fromWorkspacePath) return fromWorkspacePath;
+ var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {};
+ var source = extra.source && typeof extra.source === 'object' ? extra.source : {};
+ var fromSource = String(source.relativePath || '').trim();
+ if (fromSource) return fromSource;
+ return String(item && (item.relativePath || item.rootRelativePath) || '').trim();
+ }
+
+ function groupRowsByParent(rows) {
+ var runtimeFn = fileTreeRuntimeFunction('groupRowsByParent');
+ if (runtimeFn) return runtimeFn(rows);
+ var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
+ var grouped = new Map();
+ rows.forEach(function(item) {
+ var parentId = parentIdOf(item);
+ if (!ids.has(parentId)) parentId = '';
+ if (!grouped.has(parentId)) grouped.set(parentId, []);
+ grouped.get(parentId).push(item);
+ });
+ return grouped;
+ }
+
+ function pageTreeChevronSvg() {
+ return '';
+ }
+
+ function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
+ var computedDepth = Number(inheritedDepth || 0);
+ return (grouped.get(parentId) || []).map(function(item) {
+ var nodeId = nodeIdOf(item);
+ var title = titleOf(item);
+ var depth = computedDepth;
+ var parent = parentIdOf(item);
+ var children = grouped.get(nodeId) || [];
+ var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
+ var expanded = expandable && item.expandedByDefault !== false;
+ var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
+ var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
+ var toggle = expandable
+ ? ''
+ : '';
+ var childHtml = expandable
+ ? '
' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '
'
+ : '';
+ var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
+ return '' + toggle + '
' + childHtml + '';
+ }).join('');
+ }
+
+ function renderPageProjection(projection) {
+ var tree = document.getElementById('sidebar-tree-root');
+ if (!tree) return false;
+ var rows = projectionItems(projection).filter(function(item) {
+ return String(item.rowKind || 'document') === 'document';
+ });
+ var activeId = currentDocumentId();
+ var activeRowId = currentFileTreeActiveRowId();
+ tree.innerHTML = '' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '
';
+ return true;
+ }
+
+ function fileDocumentId(item) {
+ var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
+ if (meta.documentId) return String(meta.documentId).trim();
+ if (item && item.documentId) return String(item.documentId).trim();
+ if (item && item.rowKind === 'document') return nodeIdOf(item);
+ if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
+ return '';
+ }
+
+ function fileAssetId(item) {
+ var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
+ if (meta.assetId) return String(meta.assetId).trim();
+ if (item && item.assetId) return String(item.assetId).trim();
+ if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
+ return '';
+ }
+
+ function fileObjectIdentity(item) {
+ var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
+ // Phase A1: workspacePath 优先于旧猜测逻辑
+ var wp = meta.workspacePath;
+ if (wp && typeof wp === 'object' && wp.objectIdentity && typeof wp.objectIdentity === 'object') {
+ return wp.objectIdentity;
+ }
+ if (meta.objectIdentity && typeof meta.objectIdentity === 'object') return meta.objectIdentity;
+ var rowKind = String(item && item.rowKind || '');
+ var documentId = fileDocumentId(item) || null;
+ var assetId = fileAssetId(item) || null;
+ var iconKind = iconKindOf(item);
+ var objectKind = rowKind === 'document' ? 'page' : rowKind === 'index' ? 'index' : iconKind === 'mindmap' ? 'mindmap' : 'attachment';
+ return { objectKind: objectKind, documentId: documentId, blockId: null, assetId: assetId };
+ }
+
+ function fileOwnerDocumentId(item, fallbackDocumentId, objectIdentity) {
+ var identity = objectIdentity && typeof objectIdentity === 'object' ? objectIdentity : fileObjectIdentity(item);
+ var owner = String(identity && identity.documentId || '').trim();
+ if (owner) return owner;
+ var assetId = fileAssetId(item);
+ var localPath = localFilePathFromAssetId(assetId);
+ if (localPath && localPath.indexOf('/') > 0) {
+ var parts = localPath.split('/');
+ var bundleName = parts[0] || '';
+ if (bundleName) return 'local-md:' + bundleName + '~2F' + bundleName + '.md';
+ }
+ return String(fallbackDocumentId || '').trim();
+ }
+
+ function objectIdentityAttr(identity) {
+ try {
+ return JSON.stringify(identity || {});
+ } catch (_error) {
+ return '';
+ }
+ }
+
+ function iconKindOf(item) {
+ return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
+ }
+
+ function fileCapabilitiesAttr(item) {
+ try {
+ return JSON.stringify(Array.isArray(item && item.capabilities) ? item.capabilities : []);
+ } catch (_error) {
+ return '[]';
+ }
+ }
+
+ function isFileTreeProjectionPageRow(rowKind, assetId) {
+ if (assetId) return false;
+ return rowKind === 'document' || rowKind === 'doc' || rowKind === 'markdown';
+ }
+
+ function normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity) {
+ var title = String(rawTitle || '').trim();
+ var isMindmap = iconKind === 'mindmap' || String(objectIdentity && objectIdentity.objectKind || '') === 'mindmap';
+ if (!isMindmap) return title || '无标题';
+ return title || shortMindmapFileName(assetId);
+ }
+
+ function renderFileRows(parentId, grouped, activeId, activeRowId) {
+ return (grouped.get(parentId) || []).map(function(item) {
+ var nodeId = nodeIdOf(item);
+ var rowId = rowIdOf(item);
+ var rowKind = String(item.rowKind || 'document');
+ var rawTitle = titleOf(item);
+ var depth = Number(item.depth || 0);
+ var parent = parentIdOf(item);
+ var documentId = fileDocumentId(item);
+ var assetId = fileAssetId(item);
+ var relativePath = fileWorkspaceRelativePath(item);
+ var objectIdentity = fileObjectIdentity(item);
+ var ownerDocumentId = fileOwnerDocumentId(item, documentId, objectIdentity);
+ var iconKind = iconKindOf(item);
+ var title = isFileTreeProjectionPageRow(rowKind, assetId)
+ ? fileTreePageTitle(rawTitle)
+ : normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
+ var children = grouped.get(nodeId) || [];
+ var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
+ var expanded = expandable && item.expandedByDefault !== false;
+ var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId;
+ var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
+ var toggle = expandable
+ ? ''
+ : '';
+ var createAction = rowKind === 'document'
+ ? ''
+ : '';
+ var childHtml = expandable
+ ? '' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '
'
+ : '';
+ return '' + toggle + '
' + createAction + '
' + childHtml + '';
+ }).join('');
+ }
+
+ function renderFileProjection(projection) {
+ var tree = document.getElementById('sidebar-file-tree-root');
+ if (!tree) return false;
+ var rows = projectionItems(projection);
+ var activeId = currentDocumentId();
+ var activeRowId = currentFileTreeActiveRowId();
+ tree.innerHTML = '' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '
';
+ return true;
+ }
+
+ function renderSidebarSnapshot(payload) {
+ var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
+ var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
+ var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
+ return renderedPage || renderedFile;
+ }
+
+ function replaceSidebarTreeFromDocument(nextDocument, rootId) {
+ var runtimeFn = fileTreeRuntimeFunction('replaceSidebarTreeFromDocument');
+ if (runtimeFn) return runtimeFn(nextDocument, rootId);
+ var current = document.getElementById(rootId);
+ var next = nextDocument ? nextDocument.getElementById(rootId) : null;
+ if (!(current instanceof HTMLElement) || !(next instanceof HTMLElement)) return false;
+ current.innerHTML = next.innerHTML;
+ Array.from(next.attributes || []).forEach(function(attr) {
+ current.setAttribute(attr.name, attr.value);
+ });
+ return true;
+ }
+
+ function applyLocalFolderSidebarSnapshot(nextDocument, options) {
+ var runtimeFn = fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot');
+ if (runtimeFn) return runtimeFn(nextDocument, options);
+ options = options || {};
+ var pageRootId = String(options.pageRootId || 'sidebar-tree-root');
+ var fileRootId = String(options.fileRootId || 'sidebar-file-tree-root');
+ var appliedPage = replaceSidebarTreeFromDocument(nextDocument, pageRootId);
+ var appliedFile = replaceSidebarTreeFromDocument(nextDocument, fileRootId);
+ var applied = appliedPage || appliedFile;
+ if (applied && typeof options.onApplied === 'function') options.onApplied({
+ pageRootApplied: appliedPage,
+ fileRootApplied: appliedFile
+ });
+ return applied;
+ }
+
+ function markLocalFolderWatchApplied(value) {
+ var runtimeFn = fileTreeRuntimeFunction('markLocalFolderWatchApplied');
+ if (runtimeFn) return runtimeFn(value);
+ var appliedValue = String(value || 'projection');
+ document.documentElement.setAttribute('data-mnote-local-folder-watch-applied', appliedValue);
+ return true;
+ }
+
+ async function refreshLocalFolderSidebarSnapshot() {
+ var workspaceId = currentWorkspaceId();
+ var rootUri = currentRootUri();
+ var currentId = currentDocumentId();
+ if (!workspaceId || !rootUri) return false;
+
+ var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin);
+ sidebarUrl.searchParams.set('workspaceId', workspaceId);
+ sidebarUrl.searchParams.set('sourceKind', 'local_folder');
+ sidebarUrl.searchParams.set('rootUri', rootUri);
+ if (currentId) sidebarUrl.searchParams.set('rootNodeId', currentId);
+ var fileUrl = new URL('/api/tree/projections/file', window.location.origin);
+ fileUrl.searchParams.set('workspaceId', workspaceId);
+ fileUrl.searchParams.set('sourceKind', 'local_folder');
+ fileUrl.searchParams.set('rootUri', rootUri);
+ if (currentId) fileUrl.searchParams.set('rootNodeId', currentId);
+
+ var responses = await Promise.all([
+ fetch(sidebarUrl.toString(), { headers: { accept: 'application/json' } }),
+ fetch(fileUrl.toString(), { headers: { accept: 'application/json' } })
+ ]);
+ if (!responses[0].ok && !responses[1].ok) return false;
+
+ var sidebarPayload = responses[0].ok ? await responses[0].json().catch(function() { return null; }) : null;
+ var filePayload = responses[1].ok ? await responses[1].json().catch(function() { return null; }) : null;
+ var renderedPage = sidebarPayload ? renderSidebarSnapshot(sidebarPayload.result || sidebarPayload) : false;
+ var renderedFile = filePayload ? renderFileProjection(filePayload.result || filePayload) : false;
+ if (!renderedPage && !renderedFile) return false;
+ syncSidebarFileTreeSelection();
+ schedulePendingLocalFolderRestoreFocus();
+ restoreSidebarTreeTab();
+ refreshEditorLocalAttachmentExistence();
+ markLocalFolderWatchApplied('projection');
+ return true;
+ }
+
+ function startLocalFolderSidebarWatch() {
+ if (currentSourceKind() !== 'local_folder') return;
+ var rootUri = currentRootUri();
+ if (!rootUri) return;
+ var revision = '';
+ var refreshTimer = 0;
+ var scheduleRefresh = function() {
+ if (refreshTimer) return;
+ refreshTimer = window.setTimeout(function() {
+ refreshTimer = 0;
+ void refreshLocalFolderSidebarSnapshot();
+ }, 180);
+ };
+ var poll = async function() {
+ if (document.hidden) return;
+ // If tree live SSE transport is active for local_folder, skip polling (fallback)
+ var treeTransport = document.documentElement.getAttribute('data-mnote-tree-live-transport') || '';
+ if (treeTransport === 'local-folder-events') return;
+ var url = new URL('/api/tree/local-folder-watch', window.location.origin);
+ url.searchParams.set('rootUri', rootUri);
+ var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
+ if (!response.ok) return;
+ var payload = await response.json();
+ var nextRevision = payload && payload.result && typeof payload.result.revision === 'string'
+ ? payload.result.revision
+ : '';
+ if (!nextRevision) return;
+ if (!revision) {
+ revision = nextRevision;
+ return;
+ }
+ if (nextRevision !== revision) {
+ revision = nextRevision;
+ scheduleRefresh();
+ }
+ };
+ window.setInterval(function() {
+ void poll();
+ }, 1200);
+ void poll();
+ }
+
+ function isTitleOnlyDocumentPatch(candidate) {
+ if (!candidate || typeof candidate !== 'object') return false;
+ var allowedKeys = {
+ id: true,
+ documentId: true,
+ title: true,
+ updatedAt: true,
+ updated_at: true
+ };
+ return Object.keys(candidate).every(function(key) {
+ return allowedKeys[key] === true;
+ });
+ }
+
+ function deltaNeedsProjectionRefresh(payload) {
+ var data = payload && (payload.data || payload.delta || payload);
+ var op = data && typeof data.op === 'string' ? String(data.op).trim() : '';
+ if (!op || op === 'noop') return false;
+ if (op === 'upsert_document') {
+ return !isTitleOnlyDocumentPatch(data.node || data.document || null);
+ }
+ if (op === 'upsert_documents') {
+ var documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
+ if (documents.length > 0 && documents.every(isTitleOnlyDocumentPatch)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function toggleChildren(row, button) {
+ var li = row && row.parentElement;
+ var children = li ? li.querySelector(':scope > .tree-children') : null;
+ if (!children) return;
+ children.classList.toggle('tree-children--collapsed');
+ var collapsed = children.classList.contains('tree-children--collapsed');
+ row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
+ if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
+ button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
+ } else if (button) {
+ button.textContent = collapsed ? '▸' : '▾';
+ }
+ }
+
+
+ function installTreeLiveApplyEventListeners() {
+ window.addEventListener('tree:title-updated', function(event) {
+ var detail = event.detail || {};
+ var previousDocumentId = detail.previousDocumentId || (detail.payload && detail.payload.result && detail.payload.result.previousDocumentId) || '';
+ if (previousDocumentId && previousDocumentId !== detail.documentId) {
+ removeDocumentRowForMode('page', previousDocumentId);
+ removeDocumentRowForMode('filetree', previousDocumentId);
+ if (currentDocumentId() === previousDocumentId) {
+ navigateToDocument(detail.documentId, detail.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() });
+ }
+ }
+ updateTitleEverywhere(detail.documentId, detail.title);
+ if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
+ document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
+ });
+
+ window.addEventListener('tree:local-command', function(event) {
+ var detail = event.detail || {};
+ var body = detail.body || {};
+ var action = String(body.action || '').trim();
+ var result = detail.result || {};
+ if (action === 'create') {
+ applyCreatedDocumentLocally(result, body.parentId || null, body.title || '新页面');
+ if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
+ return;
+ }
+ if (action === 'rename' && body.documentId && body.title) {
+ var newDocumentId = commandDocumentId(result, body.documentId);
+ if (newDocumentId && newDocumentId !== body.documentId) {
+ removeDocumentRowForMode('page', body.documentId);
+ removeDocumentRowForMode('filetree', body.documentId);
+ if (currentDocumentId() === body.documentId) {
+ navigateToDocument(newDocumentId, body.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() });
+ }
+ } else {
+ updateTitleEverywhere(body.documentId, body.title);
+ }
+ if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
+ document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'rename');
+ return;
+ }
+ if (action === 'move' && body.documentId) {
+ if (applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })) {
+ document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'move');
+ }
+ if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
+ return;
+ }
+ if ((action === 'purge' || action === 'delete' || action === 'archive') && body.documentId) {
+ if (applyRemoveDocumentDelta({ documentId: body.documentId })) {
+ document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'remove');
+ }
+ if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
+ }
+ });
+
+ window.addEventListener('tree:snapshot', function(event) {
+ var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
+ if (renderSidebarSnapshot(payload)) {
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
+ return;
+ }
+ setTreeLiveApplyError('tree_snapshot_missing_projection_payload');
+ });
+
+ window.addEventListener('tree:delta', function(event) {
+ var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
+ var data = payload && (payload.data || payload.delta || payload);
+ var documentPatch = data && (data.document || data.node);
+ if (data && data.op === 'upsert_document' && documentPatch) {
+ updateTitleEverywhere(documentPatch.id || documentPatch.documentId, documentPatch.title || '无标题');
+ }
+ if (data && data.op === 'move_document' && applyMoveDocumentDelta(data)) {
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
+ return;
+ }
+ if (data && data.op === 'remove_document' && applyRemoveDocumentDelta(data)) {
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
+ return;
+ }
+ var documents = data && (data.upsertDocuments || data.upsert_documents);
+ if (Array.isArray(documents)) {
+ documents.forEach(function(doc) {
+ updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
+ });
+ }
+ if (renderSidebarSnapshot(payload)) {
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
+ return;
+ }
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
+ if (deltaNeedsProjectionRefresh(payload)) {
+ setTreeLiveApplyError('tree_delta_missing_projection_payload');
+ }
+ });
+
+ window.addEventListener('tree:resync', function(event) {
+ var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
+ if (renderSidebarSnapshot(payload)) {
+ refreshEditorLocalAttachmentExistence();
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
+ return;
+ }
+ document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
+ setTreeLiveApplyError('tree_resync_missing_projection_payload');
+ });
+ }
+
+ return {
+ applyCreatedDocumentLocally,
+ applyMoveDocumentDelta,
+ applyRemoveDocumentDelta,
+ commandDocumentId,
+ commandDocumentTitle,
+ deltaNeedsProjectionRefresh,
+ fileTreePageTitle,
+ installTreeLiveApplyEventListeners,
+ isFileTreePageRow,
+ localCommandNeedsProjectionRefresh,
+ normalizeFileTreePageRenameTitle,
+ objectIdentityAttr,
+ refreshLocalFolderSidebarSnapshot,
+ removeDocumentRowForMode,
+ renderSidebarSnapshot,
+ setTreeLiveApplyError,
+ startLocalFolderSidebarWatch,
+ toggleChildren,
+ updateTitleEverywhere,
+ validateFileTreeRename,
+ };
+};
diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
index 258160e5..b93a662b 100644
--- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
+++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
@@ -1,4 +1,5 @@
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
+import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js';
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
@@ -805,763 +806,43 @@ import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
if (runtimeFn) return runtimeFn();
}
- function updateTitleEverywhere(documentId, title) {
- if (!documentId) return;
- var escaped = cssEscape(documentId);
- var escapedDocRowId = cssEscape('doc:' + documentId);
- var isCurrentDocument = currentDocumentId() === documentId;
- var pageSelectors = [
- '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
- '.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
- 'a[href="/documents/' + escaped + '"] > .wolai-row-title',
- 'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
- ];
- pageSelectors.forEach(function(selector) {
- document.querySelectorAll(selector).forEach(function(node) {
- if (node instanceof HTMLElement) node.textContent = title;
- });
- });
- document.querySelectorAll('.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title').forEach(function(node) {
- if (node instanceof HTMLElement) node.textContent = fileTreePageTitle(title);
- });
- document.querySelectorAll('[data-page-title-input="true"][data-document-id="' + escaped + '"]').forEach(function(node) {
- if (!(node instanceof HTMLTextAreaElement)) return;
- node.value = title;
- node.setAttribute('data-title-last-saved', title);
- node.setAttribute('data-title-save-status', 'saved');
- node.style.height = 'auto';
- node.style.height = Math.max(48, node.scrollHeight) + 'px';
- });
- document.querySelectorAll('[data-document-pane="true"][data-pane-document-id="' + escaped + '"] [data-page-title-current="true"]').forEach(function(node) {
- if (node instanceof HTMLElement) node.textContent = title;
- });
- if (isCurrentDocument) {
- document.title = title;
- var topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
- if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
- }
- }
-
- function fileTreePageTitle(title) {
- var normalized = String(title || '无标题').trim() || '无标题';
- return normalized.endsWith('.md') ? normalized : normalized + '.md';
- }
-
- function isFileTreePageRow(row) {
- if (!(row instanceof HTMLElement)) return false;
- if (row.getAttribute('data-asset-id')) return false;
- var rowKind = row.getAttribute('data-row-kind') || '';
- return rowKind === 'document' || rowKind === 'doc' || rowKind === 'index' || rowKind === 'markdown';
- }
-
- function normalizeFileTreePageRenameTitle(value) {
- var normalized = String(value || '').trim();
- if (/\.md$/i.test(normalized)) normalized = normalized.slice(0, -3).trim();
- return normalized;
- }
-
- function validateFileTreeRename(row, draft) {
- var raw = String(draft || '').trim();
- if (!raw) return '名称不能为空';
- if (/[\\/:*?"<>|]/.test(raw)) return '文件名不能包含 / \\ : * ? " < > |';
- if (!isFileTreePageRow(row)) return '';
- var pageTitle = normalizeFileTreePageRenameTitle(raw);
- if (!pageTitle) return '名称不能为空';
- var expectedFileName = fileTreePageTitle(pageTitle).toLocaleLowerCase();
- var rowId = row.getAttribute('data-row-id') || '';
- var parentId = row.getAttribute('data-parent-id') || '';
- var siblings = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="document"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="doc"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="markdown"]'));
- var duplicate = siblings.some(function(sibling) {
- if (!(sibling instanceof HTMLElement)) return false;
- if ((sibling.getAttribute('data-row-id') || '') === rowId) return false;
- if ((sibling.getAttribute('data-parent-id') || '') !== parentId) return false;
- return fileTreePageTitle(normalizeFileTreePageRenameTitle(rowTitle(sibling))).toLocaleLowerCase() === expectedFileName;
- });
- return duplicate ? '同级已存在同名页面' : '';
- }
-
- function documentIdFromDelta(data) {
- return String(data && (data.documentId || data.id || (data.document && data.document.id) || (data.node && data.node.id) || (data.args && data.args.documentId)) || '').trim();
- }
-
- function parentIdFromDelta(data) {
- if (!data || typeof data !== 'object') return null;
- if (Object.prototype.hasOwnProperty.call(data, 'parentId')) return data.parentId ? String(data.parentId).trim() : null;
- if (Object.prototype.hasOwnProperty.call(data, 'parent_id')) return data.parent_id ? String(data.parent_id).trim() : null;
- if (data.args && Object.prototype.hasOwnProperty.call(data.args, 'parentId')) return data.args.parentId ? String(data.args.parentId).trim() : null;
- return null;
- }
-
- function treeRootForMode(mode) {
- var id = mode === 'filetree' ? 'sidebar-file-tree-root' : 'sidebar-tree-root';
- return document.querySelector('#' + id + ' .tree-root');
- }
-
- function rowSelectorForDocument(mode, documentId) {
- var escaped = cssEscape(documentId);
- if (mode === 'filetree') {
- return '.tree-row[data-shell-mode="filetree"][data-doc-id="' + escaped + '"], .tree-row[data-shell-mode="filetree"][data-document-id="' + escaped + '"]';
- }
- return '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"]';
- }
-
- function ensureTreeChildren(parentRow) {
- var parentNode = parentRow ? parentRow.closest('.tree-node') : null;
- if (!parentNode) return null;
- var children = parentNode.querySelector(':scope > .tree-children');
- if (!children) {
- children = document.createElement('ul');
- children.className = 'tree-children';
- parentNode.appendChild(children);
- }
- children.classList.remove('tree-children--collapsed');
- parentRow.setAttribute('aria-expanded', 'true');
- var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
- if (toggle) toggle.setAttribute('aria-expanded', 'true');
- return children;
- }
-
- function removeDocumentRowForMode(mode, documentId) {
- var row = document.querySelector(rowSelectorForDocument(mode, documentId));
- var node = row ? row.closest('.tree-node') : null;
- if (!node || !node.parentElement) return false;
- node.parentElement.removeChild(node);
- return true;
- }
-
- function commandDocumentId(result, fallback) {
- var value = result && (
- (result.execution && (result.execution.documentId || result.execution.id || result.execution.nodeId)) ||
- result.documentId ||
- result.id ||
- result.nodeId ||
- (result.document && (result.document.id || result.document.documentId)) ||
- (result.node && (result.node.id || result.node.documentId)) ||
- (result.payload && result.payload.documentId)
- );
- return String(value || fallback || '').trim();
- }
-
- function commandDocumentTitle(result, fallback) {
- var value = result && (
- (result.execution && result.execution.title) ||
- result.title ||
- (result.document && result.document.title) ||
- (result.node && result.node.title) ||
- (result.payload && result.payload.title)
- );
- return String(value || fallback || '新页面').trim() || '新页面';
- }
-
- function normalizeDocumentRowId(value) {
- return String(value || '').trim().replace(/^doc:/, '').replace(/^index:/, '');
- }
-
- function appendRenderedTreeNode(container, html) {
- if (!container || !html) return false;
- var empty = container.querySelector(':scope > .tree-empty');
- if (empty && empty.parentElement) empty.parentElement.removeChild(empty);
- var template = document.createElement('template');
- template.innerHTML = html;
- var node = template.content.firstElementChild;
- if (!node) return false;
- container.appendChild(node);
- return true;
- }
-
- function localPageInsertDepth(parentId) {
- if (!parentId) return 0;
- var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
- var depth = parentRow ? Number(parentRow.getAttribute('data-depth') || 0) : 0;
- return Number.isFinite(depth) ? depth + 1 : 1;
- }
-
- function upsertPageDocumentRow(documentId, parentId, title) {
- if (!documentId) return false;
- var existing = document.querySelector(rowSelectorForDocument('page', documentId));
- if (existing instanceof HTMLElement) {
- updateTitleEverywhere(documentId, title);
- moveDocumentRowForMode('page', documentId, parentId || null);
- return true;
- }
- var root = treeRootForMode('page');
- if (!root) return false;
- var targetContainer = root;
- if (parentId) {
- var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
- targetContainer = ensureTreeChildren(parentRow);
- if (!targetContainer) targetContainer = root;
- }
- var grouped = new Map();
- grouped.set(parentId || '', [{
- id: documentId,
- nodeId: documentId,
- documentId: documentId,
- parentNodeId: parentId || '',
- rowKind: 'document',
- title: title,
- expandable: false,
- childCount: 0
- }]);
- return appendRenderedTreeNode(targetContainer, renderPageRows(parentId || '', grouped, currentDocumentId(), localPageInsertDepth(parentId)));
- }
-
- function fileTreeParentContainer(parentId) {
- var root = treeRootForMode('filetree');
- if (!root) return null;
- if (!parentId) return root;
- var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
- var children = ensureTreeChildren(parentRow);
- return children || root;
- }
-
- function localFileInsertDepth(parentId) {
- if (!parentId) return 0;
- var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
- var level = parentRow ? Number(parentRow.getAttribute('aria-level') || 1) : 1;
- return Number.isFinite(level) ? level : 1;
- }
-
- function upsertFileDocumentRows(documentId, parentId, title) {
- if (!documentId) return false;
- var existing = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
- if (existing instanceof HTMLElement) {
- updateTitleEverywhere(documentId, title);
- moveDocumentRowForMode('filetree', documentId, parentId || null);
- return true;
- }
- var targetContainer = fileTreeParentContainer(parentId);
- if (!targetContainer) return false;
- var parentNodeId = parentId ? 'doc:' + parentId : '';
- var docNodeId = 'doc:' + documentId;
- var depth = localFileInsertDepth(parentId);
- var grouped = new Map();
- grouped.set(parentNodeId, [{
- id: docNodeId,
- nodeId: docNodeId,
- rowId: docNodeId,
- rowKind: 'document',
- title: fileTreePageTitle(title),
- documentId: documentId,
- parentNodeId: parentNodeId,
- depth: depth,
- expandable: false,
- childCount: 0
- }]);
- return appendRenderedTreeNode(targetContainer, renderFileRows(parentNodeId, grouped, currentDocumentId()));
- }
-
- function applyCreatedDocumentLocally(result, parentId, fallbackTitle) {
- var documentId = commandDocumentId(result, null);
- if (!documentId) return false;
- var normalizedParentId = normalizeDocumentRowId(parentId);
- var title = commandDocumentTitle(result, fallbackTitle);
- var pageChanged = upsertPageDocumentRow(documentId, normalizedParentId, title);
- var fileChanged = upsertFileDocumentRows(documentId, normalizedParentId, title);
- if (pageChanged || fileChanged) {
- document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'create');
- }
- return pageChanged || fileChanged;
- }
-
- function localCommandNeedsProjectionRefresh(action, result) {
- if (currentSourceKind() !== 'local_folder') return false;
- if (action === 'create' || action === 'rename' || action === 'move' || action === 'delete' || action === 'archive' || action === 'trash' || action === 'purge') {
- return true;
- }
- if (result && (result.previousDocumentId || result.previousRelativePath || result.relativePath || result.trashPath)) {
- return true;
- }
- return false;
- }
-
- function sortOrderFromDelta(data) {
- var raw = data && (data.sortOrder ?? data.sort_order);
- var value = Number(raw);
- return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
- }
-
- function insertTreeNodeAtSortOrder(targetContainer, node, sortOrder) {
- if (!targetContainer || !node) return false;
- if (sortOrder === null || sortOrder === undefined) {
- targetContainer.appendChild(node);
- return true;
- }
- var siblings = Array.from(targetContainer.querySelectorAll(':scope > .tree-node')).filter(function(candidate) {
- return candidate !== node;
- });
- var targetIndex = Math.max(0, Math.min(Number(sortOrder), siblings.length));
- var referenceNode = siblings[targetIndex] || null;
- if (referenceNode) targetContainer.insertBefore(node, referenceNode);
- else targetContainer.appendChild(node);
- return true;
- }
-
- function moveDocumentRowForMode(mode, documentId, parentId, sortOrder) {
- var row = document.querySelector(rowSelectorForDocument(mode, documentId));
- var node = row ? row.closest('.tree-node') : null;
- var root = treeRootForMode(mode);
- if (!row || !node || !root) return false;
- var targetContainer = root;
- if (parentId) {
- var parentRow = document.querySelector(rowSelectorForDocument(mode, parentId));
- targetContainer = ensureTreeChildren(parentRow);
- if (!targetContainer) return false;
- row.setAttribute('data-parent-id', parentId);
- } else {
- row.removeAttribute('data-parent-id');
- }
- return insertTreeNodeAtSortOrder(targetContainer, node, sortOrder);
- }
-
- function applyMoveDocumentDelta(data) {
- var documentId = documentIdFromDelta(data);
- if (!documentId) return false;
- var parentId = parentIdFromDelta(data);
- var sortOrder = sortOrderFromDelta(data);
- var movedPage = moveDocumentRowForMode('page', documentId, parentId, sortOrder);
- var movedFile = moveDocumentRowForMode('filetree', documentId, parentId, sortOrder);
- return movedPage || movedFile;
- }
-
- function applyRemoveDocumentDelta(data) {
- var documentId = documentIdFromDelta(data);
- if (!documentId) return false;
- var removedPage = removeDocumentRowForMode('page', documentId);
- var removedFile = removeDocumentRowForMode('filetree', documentId);
- return removedPage || removedFile;
- }
-
- function readProjection(value) {
- var runtimeFn = fileTreeRuntimeFunction('readProjection');
- if (runtimeFn) return runtimeFn(value);
- if (!value || typeof value !== 'object') return null;
- if (value.result && typeof value.result === 'object') return value.result;
- if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
- if (value.data && value.data.tree) return value.data.tree;
- if (value.tree && typeof value.tree === 'object') return value.tree;
- return value;
- }
-
- function readSidebarDataset(value) {
- var runtimeFn = fileTreeRuntimeFunction('readSidebarDataset');
- if (runtimeFn) return runtimeFn(value);
- if (!value || typeof value !== 'object') return null;
- if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset;
- if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset;
- if (value.dataset && typeof value.dataset === 'object') return value.dataset;
- if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar;
- return null;
- }
-
- function readDatasetProjection(value, snakeCaseKey, camelCaseKey) {
- var runtimeFn = fileTreeRuntimeFunction('readDatasetProjection');
- if (runtimeFn) return runtimeFn(value, snakeCaseKey, camelCaseKey);
- var dataset = readSidebarDataset(value);
- if (!dataset || typeof dataset !== 'object') return null;
- if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey];
- if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey];
- return null;
- }
-
- function setTreeLiveApplyError(reason) {
- document.documentElement.setAttribute('data-mnote-tree-live-apply-error', String(reason || 'tree_live_apply_failed'));
- }
-
- function projectionItems(projection) {
- var runtimeFn = fileTreeRuntimeFunction('projectionItems');
- if (runtimeFn) return runtimeFn(projection);
- var resolved = readProjection(projection);
- return resolved && Array.isArray(resolved.items) ? resolved.items : [];
- }
-
- function hasProjectionItems(projection) {
- var runtimeFn = fileTreeRuntimeFunction('hasProjectionItems');
- if (runtimeFn) return runtimeFn(projection);
- var resolved = readProjection(projection);
- return Boolean(resolved && Array.isArray(resolved.items));
- }
-
- function nodeIdOf(item) {
- var runtimeFn = fileTreeRuntimeFunction('nodeIdOf');
- if (runtimeFn) return runtimeFn(item);
- return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
- }
-
- function rowIdOf(item) {
- var runtimeFn = fileTreeRuntimeFunction('rowIdOf');
- if (runtimeFn) return runtimeFn(item);
- return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
- }
-
- function parentIdOf(item) {
- var runtimeFn = fileTreeRuntimeFunction('parentIdOf');
- if (runtimeFn) return runtimeFn(item);
- return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
- }
-
- function titleOf(item) {
- var runtimeFn = fileTreeRuntimeFunction('titleOf');
- if (runtimeFn) return runtimeFn(item);
- return String(item && item.title || '无标题').trim() || '无标题';
- }
-
- function fileWorkspaceRelativePath(item) {
- var runtimeFn = fileTreeRuntimeFunction('fileWorkspaceRelativePath');
- if (runtimeFn) return runtimeFn(item);
- var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
- var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {};
- var fromWorkspacePath = String(workspacePath.relativePath || '').trim();
- if (fromWorkspacePath) return fromWorkspacePath;
- var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {};
- var source = extra.source && typeof extra.source === 'object' ? extra.source : {};
- var fromSource = String(source.relativePath || '').trim();
- if (fromSource) return fromSource;
- return String(item && (item.relativePath || item.rootRelativePath) || '').trim();
- }
-
- function groupRowsByParent(rows) {
- var runtimeFn = fileTreeRuntimeFunction('groupRowsByParent');
- if (runtimeFn) return runtimeFn(rows);
- var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
- var grouped = new Map();
- rows.forEach(function(item) {
- var parentId = parentIdOf(item);
- if (!ids.has(parentId)) parentId = '';
- if (!grouped.has(parentId)) grouped.set(parentId, []);
- grouped.get(parentId).push(item);
- });
- return grouped;
- }
-
- function pageTreeChevronSvg() {
- return '';
- }
-
- function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
- var computedDepth = Number(inheritedDepth || 0);
- return (grouped.get(parentId) || []).map(function(item) {
- var nodeId = nodeIdOf(item);
- var title = titleOf(item);
- var depth = computedDepth;
- var parent = parentIdOf(item);
- var children = grouped.get(nodeId) || [];
- var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
- var expanded = expandable && item.expandedByDefault !== false;
- var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
- var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
- var toggle = expandable
- ? ''
- : '';
- var childHtml = expandable
- ? '' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '
'
- : '';
- var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
- return '' + toggle + '
' + childHtml + '';
- }).join('');
- }
-
- function renderPageProjection(projection) {
- var tree = document.getElementById('sidebar-tree-root');
- if (!tree) return false;
- var rows = projectionItems(projection).filter(function(item) {
- return String(item.rowKind || 'document') === 'document';
- });
- var activeId = currentDocumentId();
- var activeRowId = currentFileTreeActiveRowId();
- tree.innerHTML = '' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '
';
- return true;
- }
-
- function fileDocumentId(item) {
- var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
- if (meta.documentId) return String(meta.documentId).trim();
- if (item && item.documentId) return String(item.documentId).trim();
- if (item && item.rowKind === 'document') return nodeIdOf(item);
- if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
- return '';
- }
-
- function fileAssetId(item) {
- var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
- if (meta.assetId) return String(meta.assetId).trim();
- if (item && item.assetId) return String(item.assetId).trim();
- if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
- return '';
- }
-
- function fileObjectIdentity(item) {
- var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
- // Phase A1: workspacePath 优先于旧猜测逻辑
- var wp = meta.workspacePath;
- if (wp && typeof wp === 'object' && wp.objectIdentity && typeof wp.objectIdentity === 'object') {
- return wp.objectIdentity;
- }
- if (meta.objectIdentity && typeof meta.objectIdentity === 'object') return meta.objectIdentity;
- var rowKind = String(item && item.rowKind || '');
- var documentId = fileDocumentId(item) || null;
- var assetId = fileAssetId(item) || null;
- var iconKind = iconKindOf(item);
- var objectKind = rowKind === 'document' ? 'page' : rowKind === 'index' ? 'index' : iconKind === 'mindmap' ? 'mindmap' : 'attachment';
- return { objectKind: objectKind, documentId: documentId, blockId: null, assetId: assetId };
- }
-
- function fileOwnerDocumentId(item, fallbackDocumentId, objectIdentity) {
- var identity = objectIdentity && typeof objectIdentity === 'object' ? objectIdentity : fileObjectIdentity(item);
- var owner = String(identity && identity.documentId || '').trim();
- if (owner) return owner;
- var assetId = fileAssetId(item);
- var localPath = localFilePathFromAssetId(assetId);
- if (localPath && localPath.indexOf('/') > 0) {
- var parts = localPath.split('/');
- var bundleName = parts[0] || '';
- if (bundleName) return 'local-md:' + bundleName + '~2F' + bundleName + '.md';
- }
- return String(fallbackDocumentId || '').trim();
- }
-
- function objectIdentityAttr(identity) {
- try {
- return JSON.stringify(identity || {});
- } catch (_error) {
- return '';
- }
- }
-
- function iconKindOf(item) {
- return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
- }
-
- function fileCapabilitiesAttr(item) {
- try {
- return JSON.stringify(Array.isArray(item && item.capabilities) ? item.capabilities : []);
- } catch (_error) {
- return '[]';
- }
- }
-
- function isFileTreeProjectionPageRow(rowKind, assetId) {
- if (assetId) return false;
- return rowKind === 'document' || rowKind === 'doc' || rowKind === 'markdown';
- }
-
- function normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity) {
- var title = String(rawTitle || '').trim();
- var isMindmap = iconKind === 'mindmap' || String(objectIdentity && objectIdentity.objectKind || '') === 'mindmap';
- if (!isMindmap) return title || '无标题';
- return title || shortMindmapFileName(assetId);
- }
-
- function renderFileRows(parentId, grouped, activeId, activeRowId) {
- return (grouped.get(parentId) || []).map(function(item) {
- var nodeId = nodeIdOf(item);
- var rowId = rowIdOf(item);
- var rowKind = String(item.rowKind || 'document');
- var rawTitle = titleOf(item);
- var depth = Number(item.depth || 0);
- var parent = parentIdOf(item);
- var documentId = fileDocumentId(item);
- var assetId = fileAssetId(item);
- var relativePath = fileWorkspaceRelativePath(item);
- var objectIdentity = fileObjectIdentity(item);
- var ownerDocumentId = fileOwnerDocumentId(item, documentId, objectIdentity);
- var iconKind = iconKindOf(item);
- var title = isFileTreeProjectionPageRow(rowKind, assetId)
- ? fileTreePageTitle(rawTitle)
- : normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
- var children = grouped.get(nodeId) || [];
- var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
- var expanded = expandable && item.expandedByDefault !== false;
- var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId;
- var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
- var toggle = expandable
- ? ''
- : '';
- var createAction = rowKind === 'document'
- ? ''
- : '';
- var childHtml = expandable
- ? '' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '
'
- : '';
- return '' + toggle + '
' + createAction + '
' + childHtml + '';
- }).join('');
- }
-
- function renderFileProjection(projection) {
- var tree = document.getElementById('sidebar-file-tree-root');
- if (!tree) return false;
- var rows = projectionItems(projection);
- var activeId = currentDocumentId();
- var activeRowId = currentFileTreeActiveRowId();
- tree.innerHTML = '' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '
';
- return true;
- }
-
- function renderSidebarSnapshot(payload) {
- var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
- var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
- var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
- return renderedPage || renderedFile;
- }
-
- function replaceSidebarTreeFromDocument(nextDocument, rootId) {
- var runtimeFn = fileTreeRuntimeFunction('replaceSidebarTreeFromDocument');
- if (runtimeFn) return runtimeFn(nextDocument, rootId);
- var current = document.getElementById(rootId);
- var next = nextDocument ? nextDocument.getElementById(rootId) : null;
- if (!(current instanceof HTMLElement) || !(next instanceof HTMLElement)) return false;
- current.innerHTML = next.innerHTML;
- Array.from(next.attributes || []).forEach(function(attr) {
- current.setAttribute(attr.name, attr.value);
- });
- return true;
- }
-
- function applyLocalFolderSidebarSnapshot(nextDocument, options) {
- var runtimeFn = fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot');
- if (runtimeFn) return runtimeFn(nextDocument, options);
- options = options || {};
- var pageRootId = String(options.pageRootId || 'sidebar-tree-root');
- var fileRootId = String(options.fileRootId || 'sidebar-file-tree-root');
- var appliedPage = replaceSidebarTreeFromDocument(nextDocument, pageRootId);
- var appliedFile = replaceSidebarTreeFromDocument(nextDocument, fileRootId);
- var applied = appliedPage || appliedFile;
- if (applied && typeof options.onApplied === 'function') options.onApplied({
- pageRootApplied: appliedPage,
- fileRootApplied: appliedFile
- });
- return applied;
- }
-
- function markLocalFolderWatchApplied(value) {
- var runtimeFn = fileTreeRuntimeFunction('markLocalFolderWatchApplied');
- if (runtimeFn) return runtimeFn(value);
- var appliedValue = String(value || 'projection');
- document.documentElement.setAttribute('data-mnote-local-folder-watch-applied', appliedValue);
- return true;
- }
-
- async function refreshLocalFolderSidebarSnapshot() {
- var workspaceId = currentWorkspaceId();
- var rootUri = currentRootUri();
- var currentId = currentDocumentId();
- if (!workspaceId || !rootUri) return false;
-
- var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin);
- sidebarUrl.searchParams.set('workspaceId', workspaceId);
- sidebarUrl.searchParams.set('sourceKind', 'local_folder');
- sidebarUrl.searchParams.set('rootUri', rootUri);
- if (currentId) sidebarUrl.searchParams.set('rootNodeId', currentId);
- var fileUrl = new URL('/api/tree/projections/file', window.location.origin);
- fileUrl.searchParams.set('workspaceId', workspaceId);
- fileUrl.searchParams.set('sourceKind', 'local_folder');
- fileUrl.searchParams.set('rootUri', rootUri);
- if (currentId) fileUrl.searchParams.set('rootNodeId', currentId);
-
- var responses = await Promise.all([
- fetch(sidebarUrl.toString(), { headers: { accept: 'application/json' } }),
- fetch(fileUrl.toString(), { headers: { accept: 'application/json' } })
- ]);
- if (!responses[0].ok && !responses[1].ok) return false;
-
- var sidebarPayload = responses[0].ok ? await responses[0].json().catch(function() { return null; }) : null;
- var filePayload = responses[1].ok ? await responses[1].json().catch(function() { return null; }) : null;
- var renderedPage = sidebarPayload ? renderSidebarSnapshot(sidebarPayload.result || sidebarPayload) : false;
- var renderedFile = filePayload ? renderFileProjection(filePayload.result || filePayload) : false;
- if (!renderedPage && !renderedFile) return false;
- syncSidebarFileTreeSelection();
- schedulePendingLocalFolderRestoreFocus();
- restoreSidebarTreeTab();
- refreshEditorLocalAttachmentExistence();
- markLocalFolderWatchApplied('projection');
- return true;
- }
-
- function startLocalFolderSidebarWatch() {
- if (currentSourceKind() !== 'local_folder') return;
- var rootUri = currentRootUri();
- if (!rootUri) return;
- var revision = '';
- var refreshTimer = 0;
- var scheduleRefresh = function() {
- if (refreshTimer) return;
- refreshTimer = window.setTimeout(function() {
- refreshTimer = 0;
- void refreshLocalFolderSidebarSnapshot();
- }, 180);
- };
- var poll = async function() {
- if (document.hidden) return;
- // If tree live SSE transport is active for local_folder, skip polling (fallback)
- var treeTransport = document.documentElement.getAttribute('data-mnote-tree-live-transport') || '';
- if (treeTransport === 'local-folder-events') return;
- var url = new URL('/api/tree/local-folder-watch', window.location.origin);
- url.searchParams.set('rootUri', rootUri);
- var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
- if (!response.ok) return;
- var payload = await response.json();
- var nextRevision = payload && payload.result && typeof payload.result.revision === 'string'
- ? payload.result.revision
- : '';
- if (!nextRevision) return;
- if (!revision) {
- revision = nextRevision;
- return;
- }
- if (nextRevision !== revision) {
- revision = nextRevision;
- scheduleRefresh();
- }
- };
- window.setInterval(function() {
- void poll();
- }, 1200);
- void poll();
- }
-
- function isTitleOnlyDocumentPatch(candidate) {
- if (!candidate || typeof candidate !== 'object') return false;
- var allowedKeys = {
- id: true,
- documentId: true,
- title: true,
- updatedAt: true,
- updated_at: true
- };
- return Object.keys(candidate).every(function(key) {
- return allowedKeys[key] === true;
- });
- }
-
- function deltaNeedsProjectionRefresh(payload) {
- var data = payload && (payload.data || payload.delta || payload);
- var op = data && typeof data.op === 'string' ? String(data.op).trim() : '';
- if (!op || op === 'noop') return false;
- if (op === 'upsert_document') {
- return !isTitleOnlyDocumentPatch(data.node || data.document || null);
- }
- if (op === 'upsert_documents') {
- var documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
- if (documents.length > 0 && documents.every(isTitleOnlyDocumentPatch)) {
- return false;
- }
- }
- return true;
- }
-
- function toggleChildren(row, button) {
- var li = row && row.parentElement;
- var children = li ? li.querySelector(':scope > .tree-children') : null;
- if (!children) return;
- children.classList.toggle('tree-children--collapsed');
- var collapsed = children.classList.contains('tree-children--collapsed');
- row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
- if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
- button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
- } else if (button) {
- button.textContent = collapsed ? '▸' : '▾';
- }
- }
+ const sidebarTreeLiveApply = createSidebarTreeLiveApplyRuntime({
+ activeSidebarTreeMode,
+ cssEscape,
+ currentDocumentId: (...args) => currentDocumentId(...args),
+ currentFileTreeActiveRowId: (...args) => currentFileTreeActiveRowId(...args),
+ currentRootUri,
+ currentSourceKind,
+ currentWorkspaceId,
+ escapeHtml,
+ fileTreeRuntimeFunction: (...args) => fileTreeRuntimeFunction(...args),
+ localFilePathFromAssetId: (...args) => localFilePathFromAssetId(...args),
+ navigateToDocument,
+ refreshEditorLocalAttachmentExistence: (...args) => refreshEditorLocalAttachmentExistence(...args),
+ restoreSidebarTreeTab,
+ rowTitle: (...args) => rowTitle(...args),
+ schedulePendingLocalFolderRestoreFocus: (...args) => schedulePendingLocalFolderRestoreFocus(...args),
+ shortMindmapFileName: (...args) => shortMindmapFileName(...args),
+ syncSidebarFileTreeSelection: (...args) => syncSidebarFileTreeSelection(...args),
+ });
+ const updateTitleEverywhere = (...args) => sidebarTreeLiveApply.updateTitleEverywhere(...args);
+ const fileTreePageTitle = (...args) => sidebarTreeLiveApply.fileTreePageTitle(...args);
+ const isFileTreePageRow = (...args) => sidebarTreeLiveApply.isFileTreePageRow(...args);
+ const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args);
+ const validateFileTreeRename = (...args) => sidebarTreeLiveApply.validateFileTreeRename(...args);
+ const removeDocumentRowForMode = (...args) => sidebarTreeLiveApply.removeDocumentRowForMode(...args);
+ const commandDocumentId = (...args) => sidebarTreeLiveApply.commandDocumentId(...args);
+ const applyCreatedDocumentLocally = (...args) => sidebarTreeLiveApply.applyCreatedDocumentLocally(...args);
+ const localCommandNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.localCommandNeedsProjectionRefresh(...args);
+ const applyMoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyMoveDocumentDelta(...args);
+ const applyRemoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyRemoveDocumentDelta(...args);
+ const setTreeLiveApplyError = (...args) => sidebarTreeLiveApply.setTreeLiveApplyError(...args);
+ const objectIdentityAttr = (...args) => sidebarTreeLiveApply.objectIdentityAttr(...args);
+ const renderSidebarSnapshot = (...args) => sidebarTreeLiveApply.renderSidebarSnapshot(...args);
+ const refreshLocalFolderSidebarSnapshot = (...args) => sidebarTreeLiveApply.refreshLocalFolderSidebarSnapshot(...args);
+ const startLocalFolderSidebarWatch = (...args) => sidebarTreeLiveApply.startLocalFolderSidebarWatch(...args);
+ const deltaNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.deltaNeedsProjectionRefresh(...args);
+ const toggleChildren = (...args) => sidebarTreeLiveApply.toggleChildren(...args);
function dispatchSidebarEvent(name, detail) {
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
@@ -9519,111 +8800,7 @@ import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
}
});
- window.addEventListener('tree:title-updated', function(event) {
- var detail = event.detail || {};
- var previousDocumentId = detail.previousDocumentId || (detail.payload && detail.payload.result && detail.payload.result.previousDocumentId) || '';
- if (previousDocumentId && previousDocumentId !== detail.documentId) {
- removeDocumentRowForMode('page', previousDocumentId);
- removeDocumentRowForMode('filetree', previousDocumentId);
- if (currentDocumentId() === previousDocumentId) {
- navigateToDocument(detail.documentId, detail.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() });
- }
- }
- updateTitleEverywhere(detail.documentId, detail.title);
- if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
- document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
- });
-
- window.addEventListener('tree:local-command', function(event) {
- var detail = event.detail || {};
- var body = detail.body || {};
- var action = String(body.action || '').trim();
- var result = detail.result || {};
- if (action === 'create') {
- applyCreatedDocumentLocally(result, body.parentId || null, body.title || '新页面');
- if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
- return;
- }
- if (action === 'rename' && body.documentId && body.title) {
- var newDocumentId = commandDocumentId(result, body.documentId);
- if (newDocumentId && newDocumentId !== body.documentId) {
- removeDocumentRowForMode('page', body.documentId);
- removeDocumentRowForMode('filetree', body.documentId);
- if (currentDocumentId() === body.documentId) {
- navigateToDocument(newDocumentId, body.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() });
- }
- } else {
- updateTitleEverywhere(body.documentId, body.title);
- }
- if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
- document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'rename');
- return;
- }
- if (action === 'move' && body.documentId) {
- if (applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })) {
- document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'move');
- }
- if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
- return;
- }
- if ((action === 'purge' || action === 'delete' || action === 'archive') && body.documentId) {
- if (applyRemoveDocumentDelta({ documentId: body.documentId })) {
- document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'remove');
- }
- if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
- }
- });
-
- window.addEventListener('tree:snapshot', function(event) {
- var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
- if (renderSidebarSnapshot(payload)) {
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
- return;
- }
- setTreeLiveApplyError('tree_snapshot_missing_projection_payload');
- });
-
- window.addEventListener('tree:delta', function(event) {
- var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
- var data = payload && (payload.data || payload.delta || payload);
- var documentPatch = data && (data.document || data.node);
- if (data && data.op === 'upsert_document' && documentPatch) {
- updateTitleEverywhere(documentPatch.id || documentPatch.documentId, documentPatch.title || '无标题');
- }
- if (data && data.op === 'move_document' && applyMoveDocumentDelta(data)) {
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
- return;
- }
- if (data && data.op === 'remove_document' && applyRemoveDocumentDelta(data)) {
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
- return;
- }
- var documents = data && (data.upsertDocuments || data.upsert_documents);
- if (Array.isArray(documents)) {
- documents.forEach(function(doc) {
- updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
- });
- }
- if (renderSidebarSnapshot(payload)) {
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
- return;
- }
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
- if (deltaNeedsProjectionRefresh(payload)) {
- setTreeLiveApplyError('tree_delta_missing_projection_payload');
- }
- });
-
- window.addEventListener('tree:resync', function(event) {
- var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
- if (renderSidebarSnapshot(payload)) {
- refreshEditorLocalAttachmentExistence();
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
- return;
- }
- document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
- setTreeLiveApplyError('tree_resync_missing_projection_payload');
- });
+ sidebarTreeLiveApply.installTreeLiveApplyEventListeners();
var tree = document.getElementById('sidebar-tree-root');
var activeId = currentDocumentId();
diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs
index fc93340b..d3ce4821 100644
--- a/rust/crates/mnote-web/src/routes/mod.rs
+++ b/rust/crates/mnote-web/src/routes/mod.rs
@@ -130,6 +130,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
get(web_shell::sidebar_workspace_runtime_asset),
)
+ .route(
+ "/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js",
+ get(web_shell::sidebar_tree_live_apply_runtime_asset),
+ )
.route(
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
get(web_shell::sidebar_tree_runtime_asset),
@@ -584,6 +588,7 @@ mod tests {
"/api/mnote-browser-runtime/filetree-keyboard-runtime.js",
"/api/mnote-browser-runtime/sidebar-shell-runtime.js",
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
+ "/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs
index cb711a00..29d1b11a 100644
--- a/rust/crates/mnote-web/src/routes/web_shell.rs
+++ b/rust/crates/mnote-web/src/routes/web_shell.rs
@@ -756,6 +756,20 @@ pub async fn sidebar_workspace_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
+pub async fn sidebar_tree_live_apply_runtime_asset() -> Response {
+ const JS: &str = include_str!("../../browser/sidebar-tree-live-apply-runtime.js");
+ Response::builder()
+ .status(StatusCode::OK)
+ .header(
+ header::CONTENT_TYPE,
+ "application/javascript; charset=utf-8",
+ )
+ .header(header::CACHE_CONTROL, "no-store")
+ .header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
+ .body(Body::from(JS))
+ .unwrap_or_else(|_| Response::new(Body::empty()))
+}
+
pub async fn filetree_keyboard_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
Response::builder()
diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs
index 375fbe50..f7942cf5 100644
--- a/rust/crates/mnote-web/src/ssr/pages/layout.rs
+++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs
@@ -199,6 +199,8 @@ pub fn PageLayout(
#[cfg(test)]
mod tests {
const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js");
+ const SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS: &str =
+ include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
const SIDEBAR_SHELL_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-shell-runtime.js");
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
@@ -248,14 +250,18 @@ mod tests {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'create"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'rename"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'move"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("data-mnote-tree-local-command-applied', 'create"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("data-mnote-tree-local-command-applied', 'remove"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("data-mnote-tree-local-command-applied', 'rename"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("data-mnote-tree-local-command-applied', 'move"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:assets-changed"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyAssetsChangedToFileTree"));
@@ -474,20 +480,22 @@ mod tests {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("startLocalFolderSidebarWatch"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/file"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileProjection(filePayload.result || filePayload)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS
+ assert!(
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
+ );
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fetch(window.location.href, { headers: { accept: 'text/html' } })"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root')"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("window.location.reload"));
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("window.location.reload"));
}
#[test]
@@ -589,25 +597,31 @@ mod tests {
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readProjection')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readSidebarDataset')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('readProjection')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('readSidebarDataset')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('readDatasetProjection')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('projectionItems')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('hasProjectionItems')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')"));
assert!(
- SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readDatasetProjection')")
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')")
);
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('projectionItems')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('hasProjectionItems')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
+ .contains("fileTreeRuntimeFunction('groupRowsByParent')"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId");
assert!(
@@ -615,15 +629,19 @@ mod tests {
&& document_id_body.contains("data-doc-id"),
"inline fallback 必须保留旧 data attribute 解析"
);
- let replace_body =
- js_function_body(SIDEBAR_TREE_RUNTIME_JS, "replaceSidebarTreeFromDocument");
+ let replace_body = js_function_body(
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
+ "replaceSidebarTreeFromDocument",
+ );
assert!(
replace_body.contains("current.innerHTML = next.innerHTML")
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
"inline fallback 必须保留 DOM copy 行为"
);
- let apply_body =
- js_function_body(SIDEBAR_TREE_RUNTIME_JS, "applyLocalFolderSidebarSnapshot");
+ let apply_body = js_function_body(
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
+ "applyLocalFolderSidebarSnapshot",
+ );
assert!(
apply_body.contains("options.pageRootId || 'sidebar-tree-root'")
&& apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'")
@@ -631,8 +649,10 @@ mod tests {
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"),
"inline fallback 必须保留 sidebar/page tree DOM 应用行为"
);
- let watch_applied_body =
- js_function_body(SIDEBAR_TREE_RUNTIME_JS, "markLocalFolderWatchApplied");
+ let watch_applied_body = js_function_body(
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
+ "markLocalFolderWatchApplied",
+ );
assert!(
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
&& watch_applied_body.contains("String(value || 'projection')"),
@@ -826,8 +846,10 @@ mod tests {
assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime"));
assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#)
+ );
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#));
}
@@ -891,55 +913,55 @@ mod tests {
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);")
);
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
- assert!(!SIDEBAR_TREE_RUNTIME_JS.contains(
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
- assert!(!SIDEBAR_TREE_RUNTIME_JS
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("renderSidebarSnapshot"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("kernel_file_tree_projection"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("upsert_documents"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("renderSidebarSnapshot"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("kernel_file_tree_projection"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/file"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
}
#[test]
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreePageTitle(title)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("validateFileTreeRename"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("同级已存在同名页面"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("文件名不能包含"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreePageTitle(title)"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("validateFileTreeRename"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("同级已存在同名页面"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("文件名不能包含"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId"
));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
- assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
- assert!(SIDEBAR_TREE_RUNTIME_JS
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
+ assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("title: 'index.md'"));
- assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("title: 'index.md'"));
+ assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
}
#[test]
@@ -1064,12 +1086,12 @@ mod tests {
"tree.asset.open detail 应携带资源 owner document"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
"SSR filetree 行应输出 data-owner-document-id"
);
assert!(
- SIDEBAR_TREE_RUNTIME_JS
+ SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
);