refactor: split sidebar tree live apply runtime
This commit is contained in:
+1
-1
@@ -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。
|
- [ ] 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。
|
- [ ] 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。
|
- [ ] 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 行。
|
- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
|
||||||
|
|
||||||
验收命令:
|
验收命令:
|
||||||
|
|||||||
@@ -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 '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
|
||||||
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||||||
|
var childHtml = expandable
|
||||||
|
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
||||||
|
: '';
|
||||||
|
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
|
||||||
|
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-page-openable="' + String(openable) + '" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '" data-page-openable="' + String(openable) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
|
||||||
|
}).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 = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
|
||||||
|
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
|
||||||
|
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
||||||
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||||||
|
var createAction = rowKind === 'document'
|
||||||
|
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
|
||||||
|
: '';
|
||||||
|
var childHtml = expandable
|
||||||
|
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
|
||||||
|
: '';
|
||||||
|
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
||||||
|
}).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 = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '</ul>';
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
||||||
|
import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js';
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
||||||
@@ -805,763 +806,43 @@ import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
|||||||
if (runtimeFn) return runtimeFn();
|
if (runtimeFn) return runtimeFn();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTitleEverywhere(documentId, title) {
|
const sidebarTreeLiveApply = createSidebarTreeLiveApplyRuntime({
|
||||||
if (!documentId) return;
|
activeSidebarTreeMode,
|
||||||
var escaped = cssEscape(documentId);
|
cssEscape,
|
||||||
var escapedDocRowId = cssEscape('doc:' + documentId);
|
currentDocumentId: (...args) => currentDocumentId(...args),
|
||||||
var isCurrentDocument = currentDocumentId() === documentId;
|
currentFileTreeActiveRowId: (...args) => currentFileTreeActiveRowId(...args),
|
||||||
var pageSelectors = [
|
currentRootUri,
|
||||||
'.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
currentSourceKind,
|
||||||
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
|
currentWorkspaceId,
|
||||||
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
|
escapeHtml,
|
||||||
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
|
fileTreeRuntimeFunction: (...args) => fileTreeRuntimeFunction(...args),
|
||||||
];
|
localFilePathFromAssetId: (...args) => localFilePathFromAssetId(...args),
|
||||||
pageSelectors.forEach(function(selector) {
|
navigateToDocument,
|
||||||
document.querySelectorAll(selector).forEach(function(node) {
|
refreshEditorLocalAttachmentExistence: (...args) => refreshEditorLocalAttachmentExistence(...args),
|
||||||
if (node instanceof HTMLElement) node.textContent = title;
|
restoreSidebarTreeTab,
|
||||||
});
|
rowTitle: (...args) => rowTitle(...args),
|
||||||
});
|
schedulePendingLocalFolderRestoreFocus: (...args) => schedulePendingLocalFolderRestoreFocus(...args),
|
||||||
document.querySelectorAll('.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title').forEach(function(node) {
|
shortMindmapFileName: (...args) => shortMindmapFileName(...args),
|
||||||
if (node instanceof HTMLElement) node.textContent = fileTreePageTitle(title);
|
syncSidebarFileTreeSelection: (...args) => syncSidebarFileTreeSelection(...args),
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[data-page-title-input="true"][data-document-id="' + escaped + '"]').forEach(function(node) {
|
const updateTitleEverywhere = (...args) => sidebarTreeLiveApply.updateTitleEverywhere(...args);
|
||||||
if (!(node instanceof HTMLTextAreaElement)) return;
|
const fileTreePageTitle = (...args) => sidebarTreeLiveApply.fileTreePageTitle(...args);
|
||||||
node.value = title;
|
const isFileTreePageRow = (...args) => sidebarTreeLiveApply.isFileTreePageRow(...args);
|
||||||
node.setAttribute('data-title-last-saved', title);
|
const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args);
|
||||||
node.setAttribute('data-title-save-status', 'saved');
|
const validateFileTreeRename = (...args) => sidebarTreeLiveApply.validateFileTreeRename(...args);
|
||||||
node.style.height = 'auto';
|
const removeDocumentRowForMode = (...args) => sidebarTreeLiveApply.removeDocumentRowForMode(...args);
|
||||||
node.style.height = Math.max(48, node.scrollHeight) + 'px';
|
const commandDocumentId = (...args) => sidebarTreeLiveApply.commandDocumentId(...args);
|
||||||
});
|
const applyCreatedDocumentLocally = (...args) => sidebarTreeLiveApply.applyCreatedDocumentLocally(...args);
|
||||||
document.querySelectorAll('[data-document-pane="true"][data-pane-document-id="' + escaped + '"] [data-page-title-current="true"]').forEach(function(node) {
|
const localCommandNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.localCommandNeedsProjectionRefresh(...args);
|
||||||
if (node instanceof HTMLElement) node.textContent = title;
|
const applyMoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyMoveDocumentDelta(...args);
|
||||||
});
|
const applyRemoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyRemoveDocumentDelta(...args);
|
||||||
if (isCurrentDocument) {
|
const setTreeLiveApplyError = (...args) => sidebarTreeLiveApply.setTreeLiveApplyError(...args);
|
||||||
document.title = title;
|
const objectIdentityAttr = (...args) => sidebarTreeLiveApply.objectIdentityAttr(...args);
|
||||||
var topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
|
const renderSidebarSnapshot = (...args) => sidebarTreeLiveApply.renderSidebarSnapshot(...args);
|
||||||
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
|
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 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 '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
|
|
||||||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
||||||
var childHtml = expandable
|
|
||||||
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
|
||||||
: '';
|
|
||||||
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
|
|
||||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-page-openable="' + String(openable) + '" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '" data-page-openable="' + String(openable) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
|
|
||||||
}).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 = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
|
|
||||||
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
|
|
||||||
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
|
||||||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
||||||
var createAction = rowKind === 'document'
|
|
||||||
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
|
|
||||||
: '';
|
|
||||||
var childHtml = expandable
|
|
||||||
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
|
|
||||||
: '';
|
|
||||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
|
||||||
}).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 = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '</ul>';
|
|
||||||
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 dispatchSidebarEvent(name, detail) {
|
function dispatchSidebarEvent(name, detail) {
|
||||||
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
|
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) {
|
sidebarTreeLiveApply.installTreeLiveApplyEventListeners();
|
||||||
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');
|
|
||||||
});
|
|
||||||
|
|
||||||
var tree = document.getElementById('sidebar-tree-root');
|
var tree = document.getElementById('sidebar-tree-root');
|
||||||
var activeId = currentDocumentId();
|
var activeId = currentDocumentId();
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-workspace-runtime.js",
|
||||||
get(web_shell::sidebar_workspace_runtime_asset),
|
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(
|
.route(
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||||
get(web_shell::sidebar_tree_runtime_asset),
|
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/filetree-keyboard-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-shell-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-shell-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-workspace-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/sidebar-tree-runtime.js",
|
||||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
|
|||||||
@@ -756,6 +756,20 @@ pub async fn sidebar_workspace_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.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 {
|
pub async fn filetree_keyboard_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
|
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ pub fn PageLayout(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js");
|
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 =
|
const SIDEBAR_SHELL_RUNTIME_JS: &str =
|
||||||
include_str!("../../../browser/sidebar-shell-runtime.js");
|
include_str!("../../../browser/sidebar-shell-runtime.js");
|
||||||
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
|
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("treeView"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'create"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
|
.contains("data-mnote-tree-local-command-applied', 'create"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'rename"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-tree-local-command-applied', 'move"));
|
.contains("data-mnote-tree-local-command-applied', 'remove"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
|
.contains("data-mnote-tree-local-command-applied', 'rename"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_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 })"));
|
.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("wolai:assets-changed"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyAssetsChangedToFileTree"));
|
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("startLocalFolderSidebarWatch"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
||||||
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
|
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_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/file"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
|
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("renderFileProjection(filePayload.result || filePayload)"));
|
.contains("renderFileProjection(filePayload.result || filePayload)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied"));
|
assert!(
|
||||||
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
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' } })"));
|
.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')"));
|
.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]
|
#[test]
|
||||||
@@ -589,25 +597,31 @@ mod tests {
|
|||||||
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
|
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||||
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
|
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readProjection')"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('readSidebarDataset')"));
|
.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!(
|
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_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('hasProjectionItems')"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
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
|
|
||||||
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
|
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
|
||||||
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
|
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
|
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
|
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
|
||||||
let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId");
|
let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -615,15 +629,19 @@ mod tests {
|
|||||||
&& document_id_body.contains("data-doc-id"),
|
&& document_id_body.contains("data-doc-id"),
|
||||||
"inline fallback 必须保留旧 data attribute 解析"
|
"inline fallback 必须保留旧 data attribute 解析"
|
||||||
);
|
);
|
||||||
let replace_body =
|
let replace_body = js_function_body(
|
||||||
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "replaceSidebarTreeFromDocument");
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
||||||
|
"replaceSidebarTreeFromDocument",
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
replace_body.contains("current.innerHTML = next.innerHTML")
|
replace_body.contains("current.innerHTML = next.innerHTML")
|
||||||
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
|
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
|
||||||
"inline fallback 必须保留 DOM copy 行为"
|
"inline fallback 必须保留 DOM copy 行为"
|
||||||
);
|
);
|
||||||
let apply_body =
|
let apply_body = js_function_body(
|
||||||
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "applyLocalFolderSidebarSnapshot");
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
||||||
|
"applyLocalFolderSidebarSnapshot",
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
apply_body.contains("options.pageRootId || 'sidebar-tree-root'")
|
apply_body.contains("options.pageRootId || 'sidebar-tree-root'")
|
||||||
&& apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'")
|
&& apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'")
|
||||||
@@ -631,8 +649,10 @@ mod tests {
|
|||||||
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"),
|
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"),
|
||||||
"inline fallback 必须保留 sidebar/page tree DOM 应用行为"
|
"inline fallback 必须保留 sidebar/page tree DOM 应用行为"
|
||||||
);
|
);
|
||||||
let watch_applied_body =
|
let watch_applied_body = js_function_body(
|
||||||
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "markLocalFolderWatchApplied");
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
||||||
|
"markLocalFolderWatchApplied",
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
|
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
|
||||||
&& watch_applied_body.contains("String(value || 'projection')"),
|
&& watch_applied_body.contains("String(value || 'projection')"),
|
||||||
@@ -826,8 +846,10 @@ mod tests {
|
|||||||
assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime"));
|
assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime"));
|
||||||
assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime"));
|
assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#));
|
assert!(
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
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) + '""#));
|
.contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,55 +913,55 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
SIDEBAR_TREE_RUNTIME_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);")
|
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"#
|
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"#
|
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 + '"]"#));
|
.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]"#));
|
.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"#
|
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"#));
|
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("renderSidebarSnapshot"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("renderSidebarSnapshot"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("kernel_file_tree_projection"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("kernel_file_tree_projection"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("upsert_documents"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("upsert_documents"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
|
||||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_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/file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
|
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreePageTitle(title)"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreePageTitle(title)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("validateFileTreeRename"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("validateFileTreeRename"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("同级已存在同名页面"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("同级已存在同名页面"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("文件名不能包含"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("文件名不能包含"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||||
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
|
.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"
|
"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)"));
|
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
|
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
|
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
|
||||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("title: 'index.md'"));
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("title: 'index.md'"));
|
||||||
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1064,12 +1086,12 @@ mod tests {
|
|||||||
"tree.asset.open detail 应携带资源 owner document"
|
"tree.asset.open detail 应携带资源 owner document"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
SIDEBAR_TREE_RUNTIME_JS
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
|
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
|
||||||
"SSR filetree 行应输出 data-owner-document-id"
|
"SSR filetree 行应输出 data-owner-document-id"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
SIDEBAR_TREE_RUNTIME_JS
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||||
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
|
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
|
||||||
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
|
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user