chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -441,11 +441,6 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
|
||||
function applyLocalFolderWatchBatch(payload) {
|
||||
var batch = payload && (payload.payload || payload);
|
||||
if (batch && (batch.fallbackResync === true || batch.requiresResync === true)) {
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-fallback', String(batch.revision || 'resync'));
|
||||
void refreshLocalFolderSidebarSnapshot();
|
||||
return true;
|
||||
}
|
||||
var changedPaths = Array.isArray(batch && batch.changedPaths)
|
||||
? batch.changedPaths
|
||||
: Array.isArray(batch && batch.changed_paths)
|
||||
@@ -456,6 +451,31 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
: Array.isArray(batch && batch.affected_parents)
|
||||
? batch.affected_parents
|
||||
: [];
|
||||
if (batch && (batch.fallbackResync === true || batch.requiresResync === true)) {
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-fallback', String(batch.revision || 'resync'));
|
||||
var fallbackParents = new Set();
|
||||
affectedParents.forEach(function(parent) {
|
||||
addCommandRefreshParent(fallbackParents, parent && (parent.relativePath || parent.relative_path));
|
||||
});
|
||||
changedPaths.forEach(function(item) {
|
||||
var relativePath = String(item && (item.relativePath || item.relative_path) || '').trim();
|
||||
addCommandRefreshParent(fallbackParents, parentRelativePathForPath(relativePath));
|
||||
});
|
||||
if (fallbackParents.size) {
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-fallback-scoped', String(fallbackParents.size));
|
||||
void Promise.all(Array.from(fallbackParents).map(function(parentRelativePath) {
|
||||
return refreshFileTreeParent(parentRelativePath).catch(function(error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树 fallback scoped 刷新失败');
|
||||
return false;
|
||||
});
|
||||
})).then(function() {
|
||||
markLocalFolderWatchApplied('watch_batch_fallback_scoped');
|
||||
});
|
||||
} else {
|
||||
void refreshLocalFolderSidebarSnapshot();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!affectedParents.length) {
|
||||
affectedParents = changedPaths.map(function(item) {
|
||||
var relativePath = String(item && (item.relativePath || item.relative_path) || '').trim();
|
||||
@@ -714,7 +734,13 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
function sidebarTreeViewScope(treeKind) {
|
||||
var normalizedTreeKind = String(treeKind || '').trim() === 'pagetree' ? 'pagetree' : 'filetree';
|
||||
var rootUri = String(currentRootUri() || '').trim();
|
||||
var scope = currentFileTreeScope() || 'root';
|
||||
// Sidex stores explorer view-state per workspace, not per current folder
|
||||
// reveal path. PageTree must stay on a stable "root" scope so expand state
|
||||
// survives refresh / navigation even when fileTreeScope changes.
|
||||
// FileTree keeps scope = current fileTreeScope (folder-scoped projection).
|
||||
var scope = normalizedTreeKind === 'pagetree'
|
||||
? 'root'
|
||||
: (currentFileTreeScope() || 'root');
|
||||
return {
|
||||
userId: currentActorStorageId(),
|
||||
workspaceId: String(currentWorkspaceId() || '').trim() || (rootUri ? 'local:' + rootUri.replace(/^file:\/\//, '').replace(/[^A-Za-z0-9]+/g, '_') : 'default'),
|
||||
@@ -797,6 +823,11 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}
|
||||
state = normalizeSidebarTreeViewState(scope.treeKind, localValue, localValue ? 'local' : 'default');
|
||||
sidebarTreeViewStates.set(key, state);
|
||||
// Sidex applies stored view-state on setInput; apply local snapshot immediately
|
||||
// so first paint / refresh does not wait for the control-plane GET.
|
||||
if (state.hasUserState) {
|
||||
applySidebarTreeViewState(scope.treeKind, state);
|
||||
}
|
||||
void loadSidebarTreeViewState(scope.treeKind);
|
||||
}
|
||||
if (scope.treeKind === 'filetree') {
|
||||
@@ -827,9 +858,36 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (!result || !result.state) return null;
|
||||
var latestScope = sidebarTreeViewScope(scope.treeKind);
|
||||
if (sidebarTreeViewStateMapKey(latestScope) !== key) return null;
|
||||
var loaded = normalizeSidebarTreeViewState(scope.treeKind, result.state, result.source === 'sqlite' ? 'sqlite' : 'default');
|
||||
var sourceTag = result.source === 'sqlite' || result.source === 'control-plane' ? 'sqlite' : 'default';
|
||||
var loaded = normalizeSidebarTreeViewState(scope.treeKind, result.state, sourceTag);
|
||||
// Privacy / cold boot: async view-state must not clobber expand that the
|
||||
// user already performed while this request was in flight (first-click
|
||||
// expand would snap shut when default/empty state lands).
|
||||
var current = sidebarTreeViewStates.get(key);
|
||||
if (current && current.hasUserState) {
|
||||
var currentUpdated = Number(current.updatedAtMs || 0) || 0;
|
||||
var loadedUpdated = Number(loaded.updatedAtMs || 0) || 0;
|
||||
if (currentUpdated > loadedUpdated) {
|
||||
current.expandedIds.forEach(function(id) { loaded.expandedIds.add(id); });
|
||||
current.expandedRelativePaths.forEach(function(path) { loaded.expandedRelativePaths.add(path); });
|
||||
loaded.hasUserState = true;
|
||||
loaded.updatedAtMs = Math.max(currentUpdated, loadedUpdated);
|
||||
if (current.selectedId && !loaded.selectedId) loaded.selectedId = current.selectedId;
|
||||
if (current.focusedId && !loaded.focusedId) loaded.focusedId = current.focusedId;
|
||||
if (current.activeId && !loaded.activeId) loaded.activeId = current.activeId;
|
||||
} else {
|
||||
// Server is newer: still union in-session expands so a late GET never
|
||||
// collapses a node the user just opened.
|
||||
current.expandedIds.forEach(function(id) { loaded.expandedIds.add(id); });
|
||||
current.expandedRelativePaths.forEach(function(path) { loaded.expandedRelativePaths.add(path); });
|
||||
loaded.hasUserState = true;
|
||||
}
|
||||
}
|
||||
sidebarTreeViewStates.set(key, loaded);
|
||||
applySidebarTreeViewState(scope.treeKind, loaded);
|
||||
if (scope.treeKind === 'pagetree') {
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
}
|
||||
return loaded;
|
||||
});
|
||||
}).catch(function(error) {
|
||||
@@ -919,16 +977,30 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
var node = row.closest('.tree-node');
|
||||
var children = node ? node.querySelector(':scope > .tree-children') : null;
|
||||
var children = pageTreeChildrenContainer(row);
|
||||
if (!nodeId || !button || !(children instanceof HTMLElement)) return;
|
||||
var expanded = state.expandedIds.has(nodeId);
|
||||
row.setAttribute('aria-expanded', String(expanded));
|
||||
button.setAttribute('aria-expanded', String(expanded));
|
||||
children.classList.toggle('tree-children--collapsed', !expanded);
|
||||
// Keep rows the user already opened this session (and that are actually
|
||||
// showing children) even if a stale view-state snapshot omitted the id.
|
||||
if (!expanded && pageTreeRowIsOpen(row)) {
|
||||
state.expandedIds.add(nodeId);
|
||||
expanded = true;
|
||||
}
|
||||
var hasChildRows = children.children.length > 0;
|
||||
// Only open presentation when children exist in DOM. Empty bodies stay
|
||||
// collapsed (icon closed); restorePersistedPageTreeExpansionState hydrates.
|
||||
var openNow = expanded && hasChildRows;
|
||||
setPageTreeExpandPresentation(row, button, openNow, {
|
||||
children: children,
|
||||
forceCollapsed: !openNow
|
||||
});
|
||||
if (openNow) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
}
|
||||
changed = true;
|
||||
});
|
||||
if (changed) document.documentElement.setAttribute('data-mnote-page-tree-view-state-applied', state.scope || 'root');
|
||||
syncPageTreeExpandVisualState();
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -1016,6 +1088,52 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
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 pageTreeRelativePathFromItem(item) {
|
||||
if (!item || typeof item !== 'object') return '';
|
||||
var direct = String(item.expandRelativePath || item.relativePath || '').trim();
|
||||
if (direct) return direct.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
var meta = item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||||
var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {};
|
||||
var fromWorkspace = String(workspacePath.relativePath || '').trim();
|
||||
if (fromWorkspace) return fromWorkspace.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {};
|
||||
var source = extra.source && typeof extra.source === 'object' ? extra.source : {};
|
||||
return String(source.relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
function pageTreeExpandRelativePath(relativePath) {
|
||||
var rp = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
if (!rp) return '';
|
||||
if (!/\.md$/i.test(rp)) return rp;
|
||||
var stem = rp.slice(0, -3);
|
||||
var slash = rp.lastIndexOf('/');
|
||||
if (slash >= 0) {
|
||||
var parent = rp.slice(0, slash);
|
||||
var file = rp.slice(slash + 1);
|
||||
var fileStem = file.replace(/\.md$/i, '');
|
||||
var parentName = parent.indexOf('/') >= 0 ? parent.slice(parent.lastIndexOf('/') + 1) : parent;
|
||||
if (parentName === fileStem) return parent;
|
||||
return stem;
|
||||
}
|
||||
return stem;
|
||||
}
|
||||
|
||||
function pageTreeExpandPathFromRow(row) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
||||
if (direct) return direct.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
// local-md:<encoded path> is not human path; fall back empty and let API skip.
|
||||
if (nodeId.indexOf('local-dir:') === 0) {
|
||||
try {
|
||||
return decodeURIComponent(nodeId.slice('local-dir:'.length).replace(/~/g, '%'));
|
||||
} catch (_) {
|
||||
return nodeId.slice('local-dir:'.length);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
|
||||
var computedDepth = Number(inheritedDepth || 0);
|
||||
var pageTreeStateView = sidebarTreeViewStateFor('pagetree');
|
||||
@@ -1028,20 +1146,37 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var parent = parentIdOf(item);
|
||||
var children = grouped.get(nodeId) || [];
|
||||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
||||
var expanded = expandable && (
|
||||
// Align with SSR (expandedByDefault defaults false) and Sidex view-state:
|
||||
// only expand when user state lists the id, or (no user state yet) the
|
||||
// active ancestor / explicit expandedByDefault === true.
|
||||
// IMPORTANT: never paint aria/data-expanded=true for an empty children UL.
|
||||
// Lazy scope hydrate only has one level of rows; nested expandedIds are
|
||||
// restored by restorePersistedPageTreeExpansionState cascade, not by
|
||||
// faking an open empty container (that caused ▼ with no children after refresh).
|
||||
var wantExpanded = expandable && (
|
||||
pageTreeExpandedIds.has(nodeId) ||
|
||||
(!pageTreeStateView.hasUserState && (activeParentIds.has(nodeId) || item.expandedByDefault !== false))
|
||||
(!pageTreeStateView.hasUserState && (
|
||||
activeParentIds.has(nodeId) ||
|
||||
item.expandedByDefault === true
|
||||
))
|
||||
);
|
||||
var expanded = wantExpanded && children.length > 0;
|
||||
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||||
var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
|
||||
var expandPath = pageTreeExpandRelativePath(pageTreeRelativePathFromItem(item));
|
||||
if (!expandPath && expandable) {
|
||||
expandPath = pageTreeExpandRelativePath(String(item.expandRelativePath || item.relativePath || ''));
|
||||
}
|
||||
var pathAttr = expandPath ? ' data-local-relative-path="' + escapeHtml(expandPath) + '"' : '';
|
||||
var expandedAttr = String(Boolean(expandable && expanded));
|
||||
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>'
|
||||
? '<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="' + expandedAttr + '" data-expanded="' + expandedAttr + '">' + 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>';
|
||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + expandedAttr + '" data-expanded="' + expandedAttr + '" 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) + '"' : '') + pathAttr + ' 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('');
|
||||
}
|
||||
|
||||
@@ -1065,6 +1200,14 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return false;
|
||||
}
|
||||
tree.replaceChildren(template.content.cloneNode(true));
|
||||
// Re-apply persisted expand after full projection replace (Sidex setInput + viewState).
|
||||
var pageState = sidebarTreeViewStateFor('pagetree');
|
||||
if (pageState.hasUserState) {
|
||||
applyPageTreeExpansionState(pageState);
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
} else {
|
||||
syncPageTreeExpandVisualState();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1165,10 +1308,19 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var iconKind = iconKindOf(item);
|
||||
var indexStatus = String((item && item.indexStatus) || (item && item.resourceMeta && item.resourceMeta.indexStatus) || '').trim();
|
||||
if (!['indexed', 'indexing', 'failed'].includes(indexStatus)) indexStatus = '';
|
||||
// S5: FileTree tooltip shows RAG index status when known.
|
||||
var indexStatusLabel = indexStatus === 'indexed'
|
||||
? '已索引'
|
||||
: indexStatus === 'indexing'
|
||||
? '索引中'
|
||||
: indexStatus === 'failed'
|
||||
? '索引失败'
|
||||
: '';
|
||||
var cachedChildren = relativePath ? cachedFileTreeRows(relativePath) : [];
|
||||
var title = isFileTreeProjectionPageRow(rowKind, assetId)
|
||||
? fileTreePageTitle(rawTitle)
|
||||
: normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
|
||||
var linkTitle = indexStatusLabel ? (title + ' · ' + indexStatusLabel) : title;
|
||||
var children = grouped.get(nodeId) || [];
|
||||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length || cachedChildren.length);
|
||||
var expanded = expandable && (expandedRelativePaths.has(relativePath) || (!fileTreeStateView.hasUserState && item.expandedByDefault !== false));
|
||||
@@ -1188,7 +1340,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var childHtml = expandable && expanded && childRowsHtml
|
||||
? '<ul class="tree-children">' + childRowsHtml + '</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 || '')) + '"' + (indexStatus ? ' data-index-status="' + escapeHtml(indexStatus) + '"' : '') + ' 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>';
|
||||
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 || '')) + '"' + (indexStatus ? ' data-index-status="' + escapeHtml(indexStatus) + '"' : '') + ' 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(linkTitle) + '"><span class="tree-link-title" title="' + escapeHtml(linkTitle) + '">' + 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('');
|
||||
}
|
||||
|
||||
@@ -1402,6 +1554,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '</ul>';
|
||||
tree.replaceChildren(template.content.cloneNode(true));
|
||||
// Root hydration replaces pending shell; mark FileTree ready for shell-first home.
|
||||
tree.setAttribute('data-filetree-ssr', 'ready');
|
||||
tree.removeAttribute('aria-busy');
|
||||
reprojectFileTreeSelectionState();
|
||||
scheduleRestorePersistedFileTreeExpansionState();
|
||||
scheduleHydrateVisibleExpandedFileTreeRows('render');
|
||||
@@ -1534,6 +1689,42 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return readProjection(payload && (payload.result || payload));
|
||||
}
|
||||
|
||||
function isPendingFileTreeShell() {
|
||||
var root = document.getElementById('sidebar-file-tree-root');
|
||||
if (!(root instanceof HTMLElement)) return false;
|
||||
if (root.getAttribute('data-filetree-ssr') === 'pending') return true;
|
||||
return !!root.querySelector('[data-filetree-ssr="pending"], [data-rust-filetree-renderer="pending_shell_v1"]');
|
||||
}
|
||||
|
||||
async function hydratePendingFileTreeShell(reason) {
|
||||
if (!isPendingFileTreeShell()) return false;
|
||||
var root = document.getElementById('sidebar-file-tree-root');
|
||||
if (root instanceof HTMLElement) {
|
||||
root.setAttribute('data-filetree-ssr', 'hydrating');
|
||||
root.setAttribute('aria-busy', 'true');
|
||||
}
|
||||
var fileTreeScope = currentFileTreeScope();
|
||||
var rendered = false;
|
||||
try {
|
||||
rendered = await refreshFileTreeParent(fileTreeScope);
|
||||
} catch (error) {
|
||||
rendered = false;
|
||||
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||
console.warn('[mnote] hydratePendingFileTreeShell failed', reason || 'boot', error);
|
||||
}
|
||||
}
|
||||
if (root instanceof HTMLElement) {
|
||||
if (rendered) {
|
||||
root.setAttribute('data-filetree-ssr', 'ready');
|
||||
root.removeAttribute('aria-busy');
|
||||
} else {
|
||||
root.setAttribute('data-filetree-ssr', 'pending');
|
||||
root.setAttribute('aria-busy', 'true');
|
||||
}
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
async function refreshFileTreeParent(parentRelativePath) {
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var key = currentFileTreeParentKey(parentRelativePath);
|
||||
@@ -1658,24 +1849,137 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return '';
|
||||
}
|
||||
|
||||
function setTreeRowExpanded(row, button, expanded, options) {
|
||||
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
var shellMode = row.getAttribute('data-shell-mode') || '';
|
||||
if (button) {
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
if (shellMode === 'filetree') button.textContent = expanded ? '▾' : '▸';
|
||||
function pageTreeChildrenContainer(row) {
|
||||
if (!(row instanceof HTMLElement)) return null;
|
||||
var node = row.closest('.tree-node');
|
||||
return node ? node.querySelector(':scope > .tree-children') : null;
|
||||
}
|
||||
|
||||
// Single write path for page-tree expand presentation (Sidex twistie model):
|
||||
// aria-expanded + data-expanded + children class stay one unit so chevron CSS
|
||||
// and click logic never disagree after SSR / refresh / restore.
|
||||
function setPageTreeExpandPresentation(row, button, expanded, options) {
|
||||
options = options || {};
|
||||
var children = options.children instanceof HTMLElement
|
||||
? options.children
|
||||
: pageTreeChildrenContainer(row);
|
||||
var open = Boolean(expanded);
|
||||
// Never paint open/▼ for an empty children container unless explicitly allowed
|
||||
// (in-flight optimistic expand before fetch returns).
|
||||
if (
|
||||
open &&
|
||||
children instanceof HTMLElement &&
|
||||
children.children.length === 0 &&
|
||||
options.allowEmptyOpen !== true
|
||||
) {
|
||||
open = false;
|
||||
if (options.forceCollapsed !== false) options.forceCollapsed = true;
|
||||
}
|
||||
var shouldPersist = !(options && options.persist === false);
|
||||
var attr = open ? 'true' : 'false';
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('aria-expanded', attr);
|
||||
row.setAttribute('data-expanded', attr);
|
||||
}
|
||||
if (button instanceof HTMLElement) {
|
||||
button.setAttribute('aria-expanded', attr);
|
||||
button.setAttribute('data-expanded', attr);
|
||||
var title = '';
|
||||
try {
|
||||
title = String(rowTitle(row) || '').trim();
|
||||
} catch (_) {
|
||||
title = String(
|
||||
(row && row.querySelector('.tree-link-title') && row.querySelector('.tree-link-title').textContent) || ''
|
||||
).trim();
|
||||
}
|
||||
button.setAttribute('aria-label', (open ? '折叠 ' : '展开 ') + title);
|
||||
}
|
||||
if (children instanceof HTMLElement) {
|
||||
var forceCollapsed = options.forceCollapsed;
|
||||
if (forceCollapsed === true) {
|
||||
children.classList.add('tree-children--collapsed');
|
||||
} else if (forceCollapsed === false) {
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
} else {
|
||||
children.classList.toggle('tree-children--collapsed', !open);
|
||||
}
|
||||
if (open && children.children.length > 0 && forceCollapsed !== true) {
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visual open only: children visible with rows. Never trust aria alone.
|
||||
function pageTreeRowIsOpen(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
var children = pageTreeChildrenContainer(row);
|
||||
if (!(children instanceof HTMLElement)) return false;
|
||||
if (children.classList.contains('tree-children--collapsed')) return false;
|
||||
return children.children.length > 0;
|
||||
}
|
||||
|
||||
// Icon and aria always follow visible children (Sidex twistie = tree open state).
|
||||
// Never leave chevron open while .tree-children--collapsed / empty — that was the
|
||||
// refresh desync. Restoring expandedIds re-opens children; icon follows after.
|
||||
function syncPageTreeExpandVisualState() {
|
||||
var changed = false;
|
||||
document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
if (!button) return;
|
||||
var children = pageTreeChildrenContainer(row);
|
||||
if (!(children instanceof HTMLElement)) {
|
||||
if (row.getAttribute('aria-expanded') === 'true' || row.getAttribute('data-expanded') === 'true') {
|
||||
setPageTreeExpandPresentation(row, button, false, { children: null });
|
||||
changed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
var hasChildRows = children.children.length > 0;
|
||||
var visuallyOpen = !children.classList.contains('tree-children--collapsed') && hasChildRows;
|
||||
var ariaOpen = row.getAttribute('aria-expanded') === 'true';
|
||||
var dataOpen = row.getAttribute('data-expanded') === 'true';
|
||||
var buttonOpen = button.getAttribute('aria-expanded') === 'true'
|
||||
|| button.getAttribute('data-expanded') === 'true';
|
||||
if (ariaOpen !== visuallyOpen || dataOpen !== visuallyOpen || buttonOpen !== visuallyOpen) {
|
||||
setPageTreeExpandPresentation(row, button, visuallyOpen, {
|
||||
children: children,
|
||||
forceCollapsed: !visuallyOpen
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
if (hasChildRows) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
document.documentElement.setAttribute('data-mnote-page-tree-expand-synced', String(Date.now()));
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function setTreeRowExpanded(row, button, expanded, options) {
|
||||
var shellMode = row.getAttribute('data-shell-mode') || '';
|
||||
if (shellMode === 'page') {
|
||||
setPageTreeExpandPresentation(row, button, expanded);
|
||||
var shouldPersistPage = !(options && options.persist === false);
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
if (!nodeId) return;
|
||||
var pageState = sidebarTreeViewStateFor('pagetree');
|
||||
if (expanded) pageState.expandedIds.add(nodeId);
|
||||
else pageState.expandedIds.delete(nodeId);
|
||||
pageState.hasUserState = true;
|
||||
if (shouldPersist) persistSidebarTreeViewState('pagetree');
|
||||
if (shouldPersistPage) persistSidebarTreeViewState('pagetree');
|
||||
return;
|
||||
}
|
||||
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
row.setAttribute('data-expanded', expanded ? 'true' : 'false');
|
||||
if (button) {
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
if (shellMode === 'filetree') button.textContent = expanded ? '▾' : '▸';
|
||||
}
|
||||
var shouldPersist = !(options && options.persist === false);
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath) return;
|
||||
if (expanded) fileTreeExpandedRelativePaths.add(relativePath);
|
||||
@@ -1838,9 +2142,199 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return request;
|
||||
}
|
||||
|
||||
async function fetchPageTreeProjection(parentRelativePath) {
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri) return null;
|
||||
var url = new URL('/api/tree/projections/page', window.location.origin);
|
||||
var workspaceId = currentWorkspaceId();
|
||||
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
if (parentRelativePath) url.searchParams.set('parentRelativePath', parentRelativePath);
|
||||
var response = await fetch(url.toString(), {
|
||||
credentials: 'include',
|
||||
headers: { accept: 'application/json' },
|
||||
cache: 'no-store'
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok) {
|
||||
throw new Error(payload && (payload.error || payload.message) || 'page_tree_projection_failed_' + response.status);
|
||||
}
|
||||
return readProjection(payload && (payload.result || payload));
|
||||
}
|
||||
|
||||
function patchPageTreeParentChildren(row, button, childItems, options) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
var node = row.closest('.tree-node');
|
||||
if (!node) return false;
|
||||
var children = node.querySelector(':scope > .tree-children');
|
||||
if (!children) {
|
||||
children = document.createElement('ul');
|
||||
children.className = 'tree-children';
|
||||
node.appendChild(children);
|
||||
}
|
||||
var parentNodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
var parentDepth = Number(row.getAttribute('data-depth') || 0);
|
||||
// Scope projection returns one shallow level under parentRelativePath. Server parentNodeId
|
||||
// often points at a directory/group id that is not present in this response. Do NOT run
|
||||
// groupRowsByParent here: it rewrites unknown parents to "" and renderPageRows(parentId)
|
||||
// then paints zero children (page-tree expand appears broken).
|
||||
var scoped = (Array.isArray(childItems) ? childItems : []).map(function(item) {
|
||||
var next = Object.assign({}, item);
|
||||
next.parentNodeId = parentNodeId;
|
||||
next.parentId = parentNodeId;
|
||||
// Preserve expandable for nested lazy expand even when childCount is still 0.
|
||||
if (next.expandable == null && Number(next.childCount || 0) > 0) {
|
||||
next.expandable = true;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
var activeId = currentDocumentId();
|
||||
var grouped = new Map();
|
||||
grouped.set(parentNodeId, scoped);
|
||||
var html = renderPageRows(parentNodeId, grouped, activeId, parentDepth + 1);
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
children.replaceChildren(template.content.cloneNode(true));
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
if (scoped.length === 0) {
|
||||
// Scope returned no children: keep closed (▶) and stop restore retries.
|
||||
children.classList.add('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, false, options);
|
||||
row.setAttribute('data-page-tree-restore-empty', 'true');
|
||||
return false;
|
||||
}
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
row.removeAttribute('data-page-tree-restore-empty');
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadPageTreeChildren(row, button, options) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.getAttribute('data-shell-mode') !== 'page') return false;
|
||||
if (currentSourceKind() !== 'local_folder') return false;
|
||||
var node = row.closest('.tree-node');
|
||||
var children = node ? node.querySelector(':scope > .tree-children') : null;
|
||||
// SSR often already painted nested rows under a collapsed container without
|
||||
// data-page-tree-children-loaded. Treat non-empty children as loaded so the
|
||||
// first click expands immediately (no "dead" click waiting on fetch).
|
||||
if (children && children.children.length > 0) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
// Nested expandedIds may still need cascade hydrate after SSR paint.
|
||||
if (!(options && options.skipCascadeRestore)) {
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var alreadyLoaded = row.getAttribute('data-page-tree-children-loaded') === 'true';
|
||||
if (alreadyLoaded && children) {
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
if (!(options && options.skipCascadeRestore)) {
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (row.getAttribute('data-page-tree-children-loading') === 'true') {
|
||||
// In-flight lazy load: optimistic empty-open so first click responds;
|
||||
// hydrate completion rewrites children and re-syncs icons.
|
||||
if (children) children.classList.remove('tree-children--collapsed');
|
||||
setPageTreeExpandPresentation(row, button, true, {
|
||||
children: children,
|
||||
forceCollapsed: false,
|
||||
allowEmptyOpen: true
|
||||
});
|
||||
return false;
|
||||
}
|
||||
var relativePath = pageTreeExpandPathFromRow(row);
|
||||
if (!relativePath) {
|
||||
// Expandable without path: toggle empty container only.
|
||||
if (children) {
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri) return false;
|
||||
// Optimistic UI: allow empty-open chevron until fetch fills children.
|
||||
if (children) children.classList.remove('tree-children--collapsed');
|
||||
setPageTreeExpandPresentation(row, button, true, {
|
||||
children: children,
|
||||
forceCollapsed: false,
|
||||
allowEmptyOpen: true
|
||||
});
|
||||
// Still record expanded id for persistence while hydrate runs.
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
if (nodeId && !(options && options.persist === false)) {
|
||||
var pageState = sidebarTreeViewStateFor('pagetree');
|
||||
pageState.expandedIds.add(nodeId);
|
||||
pageState.hasUserState = true;
|
||||
persistSidebarTreeViewState('pagetree');
|
||||
} else if (nodeId && options && options.persist === false) {
|
||||
var pageStateNoPersist = sidebarTreeViewStateFor('pagetree');
|
||||
pageStateNoPersist.expandedIds.add(nodeId);
|
||||
pageStateNoPersist.hasUserState = true;
|
||||
}
|
||||
row.setAttribute('data-page-tree-children-loading', 'true');
|
||||
try {
|
||||
var projection = await fetchPageTreeProjection(relativePath);
|
||||
if (!projection) return false;
|
||||
var items = projectionItems(projection).filter(function(item) {
|
||||
return String(item.rowKind || 'document') === 'document';
|
||||
});
|
||||
// Drop self row if scope returns the page at this folder.
|
||||
var selfId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
items = items.filter(function(item) {
|
||||
return nodeIdOf(item) !== selfId;
|
||||
});
|
||||
var patched = patchPageTreeParentChildren(row, button, items, options);
|
||||
// After one shallow level is painted, cascade restore deeper expandedIds.
|
||||
if (patched && !(options && options.skipCascadeRestore)) {
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
}
|
||||
return patched;
|
||||
} catch (error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '页面树子节点加载失败');
|
||||
if (children) children.classList.add('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, false, options);
|
||||
return false;
|
||||
} finally {
|
||||
row.removeAttribute('data-page-tree-children-loading');
|
||||
syncPageTreeExpandVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChildren(row, button) {
|
||||
var li = row && row.parentElement;
|
||||
var children = li ? li.querySelector(':scope > .tree-children') : null;
|
||||
var shellMode = row && row.getAttribute('data-shell-mode') || '';
|
||||
if (shellMode === 'page') {
|
||||
// Use visual open state (children not collapsed), not aria alone.
|
||||
// Otherwise a desynced "arrow open / children hidden" first click collapses.
|
||||
if (pageTreeRowIsOpen(row)) {
|
||||
if (children) children.classList.add('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, false);
|
||||
return;
|
||||
}
|
||||
// SSR / previous expand already has child rows: expand immediately.
|
||||
if (children && children.children.length > 0) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
return;
|
||||
}
|
||||
if (children && row.getAttribute('data-page-tree-children-loaded') === 'true') {
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
return;
|
||||
}
|
||||
void loadPageTreeChildren(row, button);
|
||||
return;
|
||||
}
|
||||
if (!children) {
|
||||
void loadFileTreeChildren(row, button);
|
||||
return;
|
||||
@@ -1892,9 +2386,116 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return restored;
|
||||
}
|
||||
|
||||
// Sidex-style: after load/setInput, expand nodes listed in view-state.
|
||||
// PageTree shallow SSR may already contain children (open immediately) or need
|
||||
// lazy children fetch for persisted expandedIds without nested rows.
|
||||
var pageTreeExpansionRestoreTimer = 0;
|
||||
var pageTreeExpansionRestoreQueued = false;
|
||||
var PAGE_TREE_RESTORE_MAX = 24;
|
||||
|
||||
function scheduleRestorePersistedPageTreeExpansionState() {
|
||||
if (pageTreeExpansionRestoreQueued) return;
|
||||
pageTreeExpansionRestoreQueued = true;
|
||||
var run = function() {
|
||||
pageTreeExpansionRestoreQueued = false;
|
||||
pageTreeExpansionRestoreTimer = 0;
|
||||
restorePersistedPageTreeExpansionState();
|
||||
};
|
||||
if (typeof window.requestAnimationFrame === 'function') {
|
||||
pageTreeExpansionRestoreTimer = window.requestAnimationFrame(run);
|
||||
} else {
|
||||
pageTreeExpansionRestoreTimer = window.setTimeout(run, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function restorePersistedPageTreeExpansionState() {
|
||||
var state = sidebarTreeViewStateFor('pagetree');
|
||||
if (!state || !state.hasUserState || !state.expandedIds.size) {
|
||||
// No user expands: force icons to match collapsed SSR children after refresh.
|
||||
syncPageTreeExpandVisualState();
|
||||
return false;
|
||||
}
|
||||
var restored = 0;
|
||||
var pendingLoads = [];
|
||||
document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
if (restored >= PAGE_TREE_RESTORE_MAX) return;
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
if (!nodeId || !state.expandedIds.has(nodeId)) return;
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
var children = pageTreeChildrenContainer(row);
|
||||
if (!button || !(children instanceof HTMLElement)) return;
|
||||
if (pageTreeRowIsOpen(row)) {
|
||||
restored += 1;
|
||||
return;
|
||||
}
|
||||
// Shallow SSR already nested child rows: open immediately (icon + body).
|
||||
if (children.children.length > 0) {
|
||||
row.setAttribute('data-page-tree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true, { persist: false });
|
||||
restored += 1;
|
||||
return;
|
||||
}
|
||||
// Empty body: do not fake-open the chevron; hydrate then open (icon follows).
|
||||
if (row.getAttribute('data-page-tree-children-loading') === 'true') return;
|
||||
// Scope already returned empty for this row — skip infinite restore loops.
|
||||
if (row.getAttribute('data-page-tree-restore-empty') === 'true') return;
|
||||
if (currentSourceKind() === 'local_folder') {
|
||||
pendingLoads.push({ row: row, button: button });
|
||||
restored += 1;
|
||||
}
|
||||
});
|
||||
pendingLoads.slice(0, 6).forEach(function(item) {
|
||||
// Cascade further levels after each hydrate paints its children.
|
||||
void loadPageTreeChildren(item.row, item.button, {
|
||||
persist: false,
|
||||
skipCascadeRestore: false
|
||||
});
|
||||
});
|
||||
if (restored) {
|
||||
document.documentElement.setAttribute('data-mnote-page-tree-expansion-restored', String(restored));
|
||||
}
|
||||
// After open/hydrate schedule: icons follow visible children only.
|
||||
syncPageTreeExpandVisualState();
|
||||
// If only shallow levels were opened from SSR children, schedule another
|
||||
// pass so nested expandedIds under newly-visible rows get hydrated.
|
||||
if (restored > 0 && pendingLoads.length === 0) {
|
||||
// Children already in DOM — nested empty expanded rows need a follow-up.
|
||||
var needsMore = false;
|
||||
state.expandedIds.forEach(function(id) {
|
||||
if (needsMore) return;
|
||||
var nested = document.querySelector(
|
||||
'#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="' +
|
||||
String(id).replace(/\\/g, '\\\\').replace(/"/g, '\\"') +
|
||||
'"]'
|
||||
);
|
||||
if (!(nested instanceof HTMLElement)) return;
|
||||
if (pageTreeRowIsOpen(nested)) return;
|
||||
var nestedChildren = pageTreeChildrenContainer(nested);
|
||||
if (nestedChildren && nestedChildren.children.length === 0) needsMore = true;
|
||||
});
|
||||
if (needsMore) scheduleRestorePersistedPageTreeExpansionState();
|
||||
}
|
||||
return restored > 0;
|
||||
}
|
||||
|
||||
function installTreeLiveApplyEventListeners() {
|
||||
// Load local view-state, apply to SSR DOM, then reconcile icons (Sidex setInput).
|
||||
sidebarTreeViewStateFor('pagetree');
|
||||
applyPageTreeExpansionState(sidebarTreeViewStateFor('pagetree'));
|
||||
scheduleRestorePersistedPageTreeExpansionState();
|
||||
syncPageTreeExpandVisualState();
|
||||
if (typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(function() {
|
||||
restorePersistedPageTreeExpansionState();
|
||||
syncPageTreeExpandVisualState();
|
||||
});
|
||||
} else {
|
||||
window.setTimeout(function() {
|
||||
restorePersistedPageTreeExpansionState();
|
||||
syncPageTreeExpandVisualState();
|
||||
}, 0);
|
||||
}
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden) return;
|
||||
flushPendingSidebarTreeViewState('filetree');
|
||||
@@ -1919,7 +2520,6 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}
|
||||
}
|
||||
updateTitleEverywhere(detail.documentId, detail.title);
|
||||
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
|
||||
document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
|
||||
});
|
||||
|
||||
@@ -2054,6 +2654,8 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
installTreeLiveApplyEventListeners,
|
||||
isFileTreePageRow,
|
||||
applyLocalFolderWatchBatch,
|
||||
hydratePendingFileTreeShell,
|
||||
isPendingFileTreeShell,
|
||||
localCommandNeedsProjectionRefresh,
|
||||
normalizeFileTreePageRenameTitle,
|
||||
objectIdentityAttr,
|
||||
@@ -2064,6 +2666,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
renderSidebarSnapshot,
|
||||
revealFileTreeResource,
|
||||
restorePersistedFileTreeExpansionState,
|
||||
restorePersistedPageTreeExpansionState,
|
||||
setTreeLiveApplyError,
|
||||
startLocalFolderSidebarWatch,
|
||||
toggleChildren,
|
||||
|
||||
Reference in New Issue
Block a user