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 + ? '' + : ''; + 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 = ''; + 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 + ? '' + : ''; + 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 = ''; + 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 - ? '' - : ''; - 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 = ''; - 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 - ? '' - : ''; - 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 = ''; - 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" );