Optimize local folder file tree lazy loading
This commit is contained in:
@@ -18,6 +18,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
shortMindmapFileName,
|
||||
syncSidebarFileTreeSelection,
|
||||
} = dependencies;
|
||||
var fileTreeLazyChildrenCache = new Map();
|
||||
var fileTreeExpandedRelativePaths = new Set();
|
||||
var fileTreeLazyCacheRootUri = '';
|
||||
|
||||
function updateTitleEverywhere(documentId, title) {
|
||||
if (!documentId) return;
|
||||
@@ -580,12 +583,13 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var objectIdentity = fileObjectIdentity(item);
|
||||
var ownerDocumentId = fileOwnerDocumentId(item, documentId, objectIdentity);
|
||||
var iconKind = iconKindOf(item);
|
||||
var cachedChildren = relativePath ? (fileTreeLazyChildrenCache.get(relativePath) || []) : [];
|
||||
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 expandable = Boolean(item.expandable || item.childCount > 0 || children.length || cachedChildren.length);
|
||||
var expanded = expandable && (fileTreeExpandedRelativePaths.has(relativePath) || 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
|
||||
@@ -594,16 +598,44 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
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>'
|
||||
var childRowsHtml = children.length
|
||||
? renderFileRows(nodeId, grouped, activeId, activeRowId)
|
||||
: cachedChildren.length
|
||||
? renderFileRows('', groupRowsByParent(cachedChildren), activeId, activeRowId)
|
||||
: '';
|
||||
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 || '')) + '" 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 ensureFileTreeLazyCacheScope() {
|
||||
var rootUri = currentRootUri();
|
||||
if (rootUri === fileTreeLazyCacheRootUri) return;
|
||||
fileTreeLazyCacheRootUri = rootUri;
|
||||
fileTreeLazyChildrenCache.clear();
|
||||
fileTreeExpandedRelativePaths.clear();
|
||||
}
|
||||
|
||||
function rememberFileTreeExpansionState() {
|
||||
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath) return;
|
||||
if (row.getAttribute('aria-expanded') === 'true') {
|
||||
fileTreeExpandedRelativePaths.add(relativePath);
|
||||
} else {
|
||||
fileTreeExpandedRelativePaths.delete(relativePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileProjection(projection) {
|
||||
var tree = document.getElementById('sidebar-file-tree-root');
|
||||
if (!tree) return false;
|
||||
ensureFileTreeLazyCacheScope();
|
||||
rememberFileTreeExpansionState();
|
||||
var rows = projectionItems(projection);
|
||||
var activeId = currentDocumentId();
|
||||
var activeRowId = currentFileTreeActiveRowId();
|
||||
@@ -763,18 +795,105 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
function localFileTreeRelativePathFromRow(row) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
||||
if (direct) return direct;
|
||||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath');
|
||||
if (runtimeFn) {
|
||||
try {
|
||||
return String(runtimeFn(row, { localFilePathFromAssetId: localFilePathFromAssetId }) || '').trim();
|
||||
} catch (_) {}
|
||||
}
|
||||
var rowId = String(row.getAttribute('data-row-id') || '').trim();
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length);
|
||||
if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length);
|
||||
if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length);
|
||||
if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length);
|
||||
return '';
|
||||
}
|
||||
|
||||
function setTreeRowExpanded(row, button, expanded) {
|
||||
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
if (button) {
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
button.textContent = expanded ? '▾' : '▸';
|
||||
}
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath) return;
|
||||
if (expanded) fileTreeExpandedRelativePaths.add(relativePath);
|
||||
else fileTreeExpandedRelativePaths.delete(relativePath);
|
||||
}
|
||||
|
||||
function renderCachedFileTreeChildren(row, button, relativePath) {
|
||||
var cachedRows = fileTreeLazyChildrenCache.get(relativePath) || [];
|
||||
if (!cachedRows.length) 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);
|
||||
}
|
||||
children.innerHTML = renderFileRows('', groupRowsByParent(cachedRows), currentDocumentId(), currentFileTreeActiveRowId());
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
syncSidebarFileTreeSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadFileTreeChildren(row, button) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.getAttribute('data-shell-mode') !== 'filetree') return false;
|
||||
if (currentSourceKind() !== 'local_folder') return false;
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (relativePath && renderCachedFileTreeChildren(row, button, relativePath)) return true;
|
||||
if (row.getAttribute('data-filetree-children-loaded') === 'true') return false;
|
||||
if (row.getAttribute('data-filetree-children-loading') === 'true') return true;
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri || !relativePath) return false;
|
||||
row.setAttribute('data-filetree-children-loading', 'true');
|
||||
try {
|
||||
var url = new URL('/api/tree/projections/file/children', window.location.origin);
|
||||
var workspaceId = currentWorkspaceId();
|
||||
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
url.searchParams.set('parentRelativePath', relativePath);
|
||||
var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok) throw new Error(payload && (payload.error || payload.message) || 'filetree_children_failed_' + response.status);
|
||||
var projection = readProjection(payload && (payload.result || payload));
|
||||
var rows = projectionItems(projection);
|
||||
fileTreeLazyChildrenCache.set(relativePath, rows);
|
||||
if (!rows.length) {
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
return true;
|
||||
}
|
||||
return renderCachedFileTreeChildren(row, button, relativePath);
|
||||
} catch (error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树子目录加载失败');
|
||||
return false;
|
||||
} finally {
|
||||
row.removeAttribute('data-filetree-children-loading');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChildren(row, button) {
|
||||
var li = row && row.parentElement;
|
||||
var children = li ? li.querySelector(':scope > .tree-children') : null;
|
||||
if (!children) return;
|
||||
if (!children) {
|
||||
void loadFileTreeChildren(row, button);
|
||||
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 ? '▸' : '▾';
|
||||
}
|
||||
setTreeRowExpanded(row, button, !collapsed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user