Improve local filetree view state and sidebar performance
This commit is contained in:
@@ -18,11 +18,29 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
shortMindmapFileName,
|
||||
syncSidebarFileTreeSelection,
|
||||
} = dependencies;
|
||||
var fileTreeLazyChildrenCache = new Map();
|
||||
var fileTreeExpandedRelativePaths = new Set();
|
||||
var fileTreeViewState = {
|
||||
rootUri: '',
|
||||
scope: '',
|
||||
expandedParents: new Set(),
|
||||
selectedRowIds: new Set(),
|
||||
focusedRowId: '',
|
||||
activeRowId: '',
|
||||
scrollTop: 0,
|
||||
rowsByParent: new Map(),
|
||||
loadedParents: new Set(),
|
||||
loadingParents: new Map(),
|
||||
dirtyParents: new Set(),
|
||||
staleParents: new Set(),
|
||||
revisionByParent: new Map(),
|
||||
requestGeneration: 0,
|
||||
latestGenerationByParent: new Map()
|
||||
};
|
||||
var fileTreeState = fileTreeViewState;
|
||||
var fileTreeExpandedRelativePaths = fileTreeViewState.expandedParents;
|
||||
var fileTreeLazyCacheRootUri = '';
|
||||
var fileTreeExpansionStorageRootUri = '';
|
||||
var fileTreeExpansionRestoreTimer = 0;
|
||||
var fileTreeCommandBatchRefreshParents = new Map();
|
||||
var FILETREE_EXPANSION_STORAGE_KEY = 'mnote.localFileTree.expandedRelativePaths.v1';
|
||||
|
||||
function updateTitleEverywhere(documentId, title) {
|
||||
@@ -70,6 +88,8 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
function isFileTreePageRow(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.getAttribute('data-asset-id')) return false;
|
||||
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
|
||||
if (documentId.indexOf('local-md:') === 0) return true;
|
||||
var rowKind = row.getAttribute('data-row-kind') || '';
|
||||
return rowKind === 'document' || rowKind === 'doc' || rowKind === 'index' || rowKind === 'markdown';
|
||||
}
|
||||
@@ -295,6 +315,134 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
function parentRelativePathForPath(relativePath) {
|
||||
var normalized = String(relativePath || '').trim().replace(/^\/+|\/+$/g, '');
|
||||
if (!normalized || normalized === '.') return '';
|
||||
var index = normalized.lastIndexOf('/');
|
||||
return index > 0 ? normalized.slice(0, index) : '';
|
||||
}
|
||||
|
||||
function addCommandRefreshParent(parents, value) {
|
||||
var normalized = String(value || '').trim().replace(/^\/+|\/+$/g, '');
|
||||
if (normalized === '.') normalized = '';
|
||||
parents.add(normalized);
|
||||
}
|
||||
|
||||
function addAffectedParentsFromCommandResult(parents, result) {
|
||||
var affectedParents = Array.isArray(result && result.affectedParents)
|
||||
? result.affectedParents
|
||||
: Array.isArray(result && result.execution && result.execution.affectedParents)
|
||||
? result.execution.affectedParents
|
||||
: null;
|
||||
if (!affectedParents) return false;
|
||||
var before = parents.size;
|
||||
affectedParents.forEach(function(parent) {
|
||||
addCommandRefreshParent(parents, parent && (parent.relativePath || parent.relative_path));
|
||||
});
|
||||
return parents.size > before;
|
||||
}
|
||||
|
||||
function collectFileTreeRefreshParentsForCommand(result, body) {
|
||||
var parents = new Set();
|
||||
if (!addAffectedParentsFromCommandResult(parents, result)) {
|
||||
document.documentElement.setAttribute('data-mnote-filetree-command-refresh-fallback', 'legacy-path-fields');
|
||||
var values = [
|
||||
result && (result.parentRelativePath || result.parent_relative_path),
|
||||
result && result.execution && (result.execution.parentRelativePath || result.execution.parent_relative_path),
|
||||
result && (result.relativePath || result.relative_path),
|
||||
result && result.execution && (result.execution.relativePath || result.execution.relative_path),
|
||||
result && (result.previousRelativePath || result.previous_relative_path),
|
||||
result && result.execution && (result.execution.previousRelativePath || result.execution.previous_relative_path),
|
||||
body && (body.parentRelativePath || body.parent_relative_path)
|
||||
];
|
||||
values.forEach(function(value) {
|
||||
var normalized = String(value || '').trim();
|
||||
if (!normalized) return;
|
||||
if (normalized.indexOf('/') >= 0 || /\.[^/]+$/.test(normalized)) addCommandRefreshParent(parents, parentRelativePathForPath(normalized));
|
||||
else addCommandRefreshParent(parents, normalized);
|
||||
});
|
||||
}
|
||||
if (!parents.size) addCommandRefreshParent(parents, currentFileTreeScope());
|
||||
return parents;
|
||||
}
|
||||
|
||||
function fileTreeRefreshParentsForCommand(result, body) {
|
||||
var parents = collectFileTreeRefreshParentsForCommand(result, body);
|
||||
return Promise.all(Array.from(parents).map(function(parentRelativePath) {
|
||||
return refreshFileTreeParent(parentRelativePath).catch(function(error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树局部刷新失败');
|
||||
return false;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function queueFileTreeBatchRefresh(batchId, result, body) {
|
||||
var normalizedBatchId = String(batchId || '').trim();
|
||||
if (!normalizedBatchId) return false;
|
||||
var batchParents = fileTreeCommandBatchRefreshParents.get(normalizedBatchId);
|
||||
if (!batchParents) {
|
||||
batchParents = new Set();
|
||||
fileTreeCommandBatchRefreshParents.set(normalizedBatchId, batchParents);
|
||||
}
|
||||
collectFileTreeRefreshParentsForCommand(result, body).forEach(function(parent) {
|
||||
batchParents.add(parent);
|
||||
});
|
||||
document.documentElement.setAttribute('data-mnote-filetree-batch-refresh-pending', normalizedBatchId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function flushFileTreeBatchRefresh(batchId) {
|
||||
var normalizedBatchId = String(batchId || '').trim();
|
||||
if (!normalizedBatchId) return false;
|
||||
var parents = fileTreeCommandBatchRefreshParents.get(normalizedBatchId);
|
||||
fileTreeCommandBatchRefreshParents.delete(normalizedBatchId);
|
||||
if (!parents || !parents.size) return false;
|
||||
document.documentElement.setAttribute('data-mnote-filetree-batch-refresh-applied', normalizedBatchId);
|
||||
void Promise.all(Array.from(parents).map(function(parentRelativePath) {
|
||||
return refreshFileTreeParent(parentRelativePath).catch(function(error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树批量刷新失败');
|
||||
return false;
|
||||
});
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyLocalFolderWatchBatch(payload) {
|
||||
var batch = payload && (payload.payload || payload);
|
||||
var affectedParents = Array.isArray(batch && batch.affectedParents)
|
||||
? batch.affectedParents
|
||||
: Array.isArray(batch && batch.affected_parents)
|
||||
? batch.affected_parents
|
||||
: [];
|
||||
if (!affectedParents.length) {
|
||||
setTreeLiveApplyError('local_folder_watch_batch_missing_affected_parents');
|
||||
return false;
|
||||
}
|
||||
var parents = new Set();
|
||||
affectedParents.forEach(function(parent) {
|
||||
addCommandRefreshParent(parents, parent && (parent.relativePath || parent.relative_path));
|
||||
});
|
||||
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-applied', String(batch.revision || 'true'));
|
||||
void Promise.all(Array.from(parents).map(function(parentRelativePath) {
|
||||
return refreshFileTreeParent(parentRelativePath).catch(function(error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树 watch batch 刷新失败');
|
||||
return false;
|
||||
});
|
||||
})).then(function() {
|
||||
markLocalFolderWatchApplied('watch_batch');
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function refreshLocalFolderAfterCommand(action, result, body) {
|
||||
if (!localCommandNeedsProjectionRefresh(action, result)) return;
|
||||
if (body && body.batchId) {
|
||||
queueFileTreeBatchRefresh(body.batchId, result, body);
|
||||
return;
|
||||
}
|
||||
void fileTreeRefreshParentsForCommand(result, body);
|
||||
}
|
||||
|
||||
function sortOrderFromDelta(data) {
|
||||
var raw = data && (data.sortOrder ?? data.sort_order);
|
||||
var value = Number(raw);
|
||||
@@ -440,6 +588,17 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return String(item && (item.relativePath || item.rootRelativePath) || '').trim();
|
||||
}
|
||||
|
||||
function normalizeFileTreeRelativePath(value) {
|
||||
var normalized = String(value || '').trim().replace(/^\/+|\/+$/g, '');
|
||||
return normalized === '.' ? '' : normalized;
|
||||
}
|
||||
|
||||
function fileTreeRowByRelativePath(relativePath) {
|
||||
var normalized = normalizeFileTreeRelativePath(relativePath);
|
||||
if (!normalized) return null;
|
||||
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="' + cssEscape(normalized) + '"]');
|
||||
}
|
||||
|
||||
function groupRowsByParent(rows) {
|
||||
var runtimeFn = fileTreeRuntimeFunction('groupRowsByParent');
|
||||
if (runtimeFn) return runtimeFn(rows);
|
||||
@@ -454,6 +613,53 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function fileTreeParentKey(rootUri, parentRelativePath) {
|
||||
return String(rootUri || '').trim() + '\n' + String(parentRelativePath || '').trim();
|
||||
}
|
||||
|
||||
function currentFileTreeParentKey(parentRelativePath) {
|
||||
return fileTreeParentKey(currentRootUri(), parentRelativePath);
|
||||
}
|
||||
|
||||
function projectionParentRelativePath(projection) {
|
||||
var resolved = readProjection(projection);
|
||||
return String(resolved && (resolved.parentRelativePath || resolved.parent_relative_path) || '').trim();
|
||||
}
|
||||
|
||||
function isFileTreeRootProjectionParent(parentRelativePath) {
|
||||
return normalizeFileTreeRelativePath(parentRelativePath) === normalizeFileTreeRelativePath(currentFileTreeScope());
|
||||
}
|
||||
|
||||
function rememberFileTreeProjection(parentRelativePath, rows, projection) {
|
||||
var key = currentFileTreeParentKey(parentRelativePath);
|
||||
fileTreeState.rowsByParent.set(key, rows);
|
||||
fileTreeState.loadedParents.add(key);
|
||||
fileTreeState.dirtyParents.delete(key);
|
||||
fileTreeState.staleParents.delete(key);
|
||||
var resolved = readProjection(projection);
|
||||
var watchRevision = resolved && (resolved.watchRevision || resolved.watch_revision);
|
||||
if (watchRevision) fileTreeState.revisionByParent.set(key, watchRevision);
|
||||
}
|
||||
|
||||
function cachedFileTreeRows(parentRelativePath) {
|
||||
return fileTreeState.rowsByParent.get(currentFileTreeParentKey(parentRelativePath)) || [];
|
||||
}
|
||||
|
||||
function beginFileTreeRequest(key) {
|
||||
fileTreeState.requestGeneration += 1;
|
||||
fileTreeState.latestGenerationByParent.set(key, fileTreeState.requestGeneration);
|
||||
return fileTreeState.requestGeneration;
|
||||
}
|
||||
|
||||
function isLatestFileTreeRequest(key, generation) {
|
||||
return fileTreeState.latestGenerationByParent.get(key) === generation;
|
||||
}
|
||||
|
||||
function markFileTreeParentStale(key) {
|
||||
fileTreeState.staleParents.add(key);
|
||||
fileTreeState.dirtyParents.add(key);
|
||||
}
|
||||
|
||||
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>';
|
||||
}
|
||||
@@ -586,7 +792,7 @@ 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 cachedChildren = relativePath ? cachedFileTreeRows(relativePath) : [];
|
||||
var title = isFileTreeProjectionPageRow(rowKind, assetId)
|
||||
? fileTreePageTitle(rawTitle)
|
||||
: normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
|
||||
@@ -615,9 +821,19 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
|
||||
function ensureFileTreeLazyCacheScope() {
|
||||
var rootUri = currentRootUri();
|
||||
if (rootUri === fileTreeLazyCacheRootUri) return;
|
||||
var scope = currentFileTreeScope();
|
||||
if (rootUri === fileTreeLazyCacheRootUri && scope === fileTreeState.scope) return;
|
||||
fileTreeLazyCacheRootUri = rootUri;
|
||||
fileTreeLazyChildrenCache.clear();
|
||||
fileTreeState.rootUri = rootUri;
|
||||
fileTreeState.scope = scope;
|
||||
fileTreeState.rowsByParent.clear();
|
||||
fileTreeState.loadedParents.clear();
|
||||
fileTreeState.loadingParents.clear();
|
||||
fileTreeState.dirtyParents.clear();
|
||||
fileTreeState.staleParents.clear();
|
||||
fileTreeState.revisionByParent.clear();
|
||||
fileTreeState.latestGenerationByParent.clear();
|
||||
fileTreeState.requestGeneration += 1;
|
||||
fileTreeExpandedRelativePaths.clear();
|
||||
loadStoredFileTreeExpansionState(rootUri);
|
||||
}
|
||||
@@ -683,15 +899,80 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (changed) persistFileTreeExpansionState();
|
||||
}
|
||||
|
||||
function rememberFileTreeSelectionState() {
|
||||
fileTreeViewState.selectedRowIds.clear();
|
||||
fileTreeViewState.focusedRowId = '';
|
||||
fileTreeViewState.activeRowId = '';
|
||||
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
var rowId = String(row.getAttribute('data-row-id') || '').trim();
|
||||
if (!rowId) return;
|
||||
if (row.getAttribute('data-selected') === 'true') fileTreeViewState.selectedRowIds.add(rowId);
|
||||
if (row.getAttribute('data-focused') === 'true') fileTreeViewState.focusedRowId = rowId;
|
||||
if (row.getAttribute('data-active') === 'true') fileTreeViewState.activeRowId = rowId;
|
||||
});
|
||||
}
|
||||
|
||||
function reprojectFileTreeSelectionState() {
|
||||
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
var rowId = String(row.getAttribute('data-row-id') || '').trim();
|
||||
row.setAttribute('data-selected', String(fileTreeViewState.selectedRowIds.has(rowId)));
|
||||
row.setAttribute('data-focused', String(rowId === fileTreeViewState.focusedRowId));
|
||||
if (fileTreeViewState.activeRowId) {
|
||||
row.setAttribute('data-active', String(rowId === fileTreeViewState.activeRowId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileProjection(projection) {
|
||||
var tree = document.getElementById('sidebar-file-tree-root');
|
||||
if (!tree) return false;
|
||||
ensureFileTreeLazyCacheScope();
|
||||
rememberFileTreeExpansionState();
|
||||
rememberFileTreeSelectionState();
|
||||
var rows = projectionItems(projection);
|
||||
var parentRelativePath = projectionParentRelativePath(projection) || currentFileTreeScope();
|
||||
rememberFileTreeProjection(parentRelativePath, rows, projection);
|
||||
if (!isFileTreeRootProjectionParent(parentRelativePath)) {
|
||||
return patchFileTreeParentChildren(parentRelativePath, rows);
|
||||
}
|
||||
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>';
|
||||
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));
|
||||
reprojectFileTreeSelectionState();
|
||||
scheduleRestorePersistedFileTreeExpansionState();
|
||||
return true;
|
||||
}
|
||||
|
||||
function patchFileTreeParentChildren(parentRelativePath, rows) {
|
||||
var row = fileTreeRowByRelativePath(parentRelativePath);
|
||||
if (!(row instanceof HTMLElement)) return true;
|
||||
var node = row.closest('.tree-node');
|
||||
if (!node) return true;
|
||||
rememberFileTreeSelectionState();
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
var wasExpanded = row.getAttribute('aria-expanded') === 'true';
|
||||
var children = node.querySelector(':scope > .tree-children');
|
||||
if (!children && !wasExpanded && row.getAttribute('data-filetree-children-loaded') !== 'true') {
|
||||
return true;
|
||||
}
|
||||
if (!children) {
|
||||
children = document.createElement('ul');
|
||||
children.className = 'tree-children';
|
||||
node.appendChild(children);
|
||||
}
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = renderFileRows('', groupRowsByParent(rows), currentDocumentId(), currentFileTreeActiveRowId());
|
||||
children.replaceChildren(template.content.cloneNode(true));
|
||||
children.classList.toggle('tree-children--collapsed', !wasExpanded);
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
row.removeAttribute('data-filetree-children-loading');
|
||||
setTreeRowExpanded(row, button, wasExpanded);
|
||||
syncSidebarFileTreeSelection();
|
||||
reprojectFileTreeSelectionState();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -702,6 +983,23 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return renderedPage || renderedFile;
|
||||
}
|
||||
|
||||
function renderLiveSidebarSnapshot(payload) {
|
||||
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
|
||||
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
|
||||
var hasFileProjection = fileProjection && hasProjectionItems(fileProjection);
|
||||
if (currentFileTreeScope()) {
|
||||
if (!hasFileProjection) return renderedPage;
|
||||
var projectionParent = projectionParentRelativePath(fileProjection);
|
||||
if (isFileTreeRootProjectionParent(projectionParent)) {
|
||||
return renderFileProjection(fileProjection) || renderedPage;
|
||||
}
|
||||
document.documentElement.setAttribute('data-mnote-filetree-scoped-live-snapshot-ignored', projectionParent || 'root');
|
||||
return true;
|
||||
}
|
||||
var renderedFile = hasFileProjection ? renderFileProjection(fileProjection) : false;
|
||||
return renderedPage || renderedFile;
|
||||
}
|
||||
|
||||
function replaceSidebarTreeFromDocument(nextDocument, rootId) {
|
||||
var runtimeFn = fileTreeRuntimeFunction('replaceSidebarTreeFromDocument');
|
||||
if (runtimeFn) return runtimeFn(nextDocument, rootId);
|
||||
@@ -739,10 +1037,61 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
function currentFileTreeScope() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var fromUrl = String(params.get('fileTreeScope') || '').trim();
|
||||
if (fromUrl) return fromUrl;
|
||||
var root = document.getElementById('sidebar-file-tree-root');
|
||||
return root instanceof HTMLElement ? String(root.getAttribute('data-mnote-filetree-scope') || '').trim() : '';
|
||||
}
|
||||
|
||||
async function fetchFileTreeProjection(parentRelativePath, options) {
|
||||
options = options || {};
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri) return null;
|
||||
var url = new URL(options.childrenOnly ? '/api/tree/projections/file/children' : '/api/tree/projections/file', 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 currentId = currentDocumentId();
|
||||
if (!options.childrenOnly && currentId) url.searchParams.set('rootNodeId', currentId);
|
||||
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_projection_failed_' + response.status);
|
||||
return readProjection(payload && (payload.result || payload));
|
||||
}
|
||||
|
||||
async function refreshFileTreeParent(parentRelativePath) {
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var key = currentFileTreeParentKey(parentRelativePath);
|
||||
fileTreeState.dirtyParents.add(key);
|
||||
if (!isFileTreeRootProjectionParent(parentRelativePath)) {
|
||||
var row = fileTreeRowByRelativePath(parentRelativePath);
|
||||
if (row instanceof HTMLElement && row.getAttribute('aria-expanded') !== 'true') {
|
||||
markFileTreeParentStale(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (fileTreeState.loadingParents.has(key)) {
|
||||
return fileTreeState.loadingParents.get(key).then(function(rows) {
|
||||
fileTreeState.dirtyParents.delete(key);
|
||||
return isFileTreeRootProjectionParent(parentRelativePath)
|
||||
? renderFileProjection({ parentRelativePath: parentRelativePath, items: rows })
|
||||
: patchFileTreeParentChildren(parentRelativePath, rows);
|
||||
});
|
||||
}
|
||||
var projection = await fetchFileTreeProjection(parentRelativePath, { childrenOnly: false });
|
||||
if (!projection) return false;
|
||||
return renderFileProjection(projection);
|
||||
}
|
||||
|
||||
async function refreshLocalFolderSidebarSnapshot() {
|
||||
var workspaceId = currentWorkspaceId();
|
||||
var rootUri = currentRootUri();
|
||||
var currentId = currentDocumentId();
|
||||
var fileTreeScope = currentFileTreeScope();
|
||||
if (!workspaceId || !rootUri) return false;
|
||||
|
||||
var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin);
|
||||
@@ -750,22 +1099,24 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
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([
|
||||
var responses = await Promise.allSettled([
|
||||
fetch(sidebarUrl.toString(), { headers: { accept: 'application/json' } }),
|
||||
fetch(fileUrl.toString(), { headers: { accept: 'application/json' } })
|
||||
refreshFileTreeParent(fileTreeScope)
|
||||
]);
|
||||
if (!responses[0].ok && !responses[1].ok) return false;
|
||||
var sidebarResponse = responses[0].status === 'fulfilled' ? responses[0].value : null;
|
||||
var renderedFile = responses[1].status === 'fulfilled' ? responses[1].value : false;
|
||||
if ((!sidebarResponse || !sidebarResponse.ok) && !renderedFile) 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;
|
||||
var sidebarPayload = sidebarResponse && sidebarResponse.ok ? await sidebarResponse.json().catch(function() { return null; }) : null;
|
||||
var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null;
|
||||
var renderedPage = resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)
|
||||
? renderPageProjection(resolvedSidebarPayload)
|
||||
: false;
|
||||
if (renderedFile && fileTreeScope) {
|
||||
var fileRoot = document.getElementById('sidebar-file-tree-root');
|
||||
if (fileRoot instanceof HTMLElement) fileRoot.setAttribute('data-mnote-filetree-scope', fileTreeScope);
|
||||
document.documentElement.setAttribute('data-mnote-filetree-scope', fileTreeScope);
|
||||
}
|
||||
if (!renderedPage && !renderedFile) return false;
|
||||
syncSidebarFileTreeSelection();
|
||||
schedulePendingLocalFolderRestoreFocus();
|
||||
@@ -783,22 +1134,35 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
scheduleRestorePersistedFileTreeExpansionState();
|
||||
var revision = '';
|
||||
var refreshTimer = 0;
|
||||
var treeLiveEventsActive = function() {
|
||||
var treeTransport = document.documentElement.getAttribute('data-mnote-tree-live-transport') || '';
|
||||
return treeTransport === 'local-folder-events';
|
||||
};
|
||||
var scheduleRefresh = function() {
|
||||
if (treeLiveEventsActive()) return;
|
||||
if (refreshTimer) return;
|
||||
refreshTimer = window.setTimeout(function() {
|
||||
refreshTimer = 0;
|
||||
if (treeLiveEventsActive()) return;
|
||||
var fileTreeScope = currentFileTreeScope();
|
||||
if (fileTreeScope) {
|
||||
markFileTreeParentStale(currentFileTreeParentKey(fileTreeScope));
|
||||
markLocalFolderWatchApplied('scope_stale');
|
||||
document.documentElement.setAttribute('data-mnote-filetree-scope-watch-stale', fileTreeScope);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (treeLiveEventsActive()) 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;
|
||||
if (treeLiveEventsActive()) return;
|
||||
var payload = await response.json();
|
||||
var nextRevision = payload && payload.result && typeof payload.result.revision === 'string'
|
||||
? payload.result.revision
|
||||
@@ -881,8 +1245,23 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
persistFileTreeExpansionState();
|
||||
}
|
||||
|
||||
function markExistingFileTreeChildrenLoaded(row, button) {
|
||||
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 instanceof HTMLElement)) return false;
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
syncSidebarFileTreeSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderCachedFileTreeChildren(row, button, relativePath) {
|
||||
var cachedRows = fileTreeLazyChildrenCache.get(relativePath) || [];
|
||||
var key = currentFileTreeParentKey(relativePath);
|
||||
if (fileTreeState.staleParents.has(key) || fileTreeState.dirtyParents.has(key)) return false;
|
||||
var cachedRows = cachedFileTreeRows(relativePath);
|
||||
if (!cachedRows.length) return false;
|
||||
var node = row.closest('.tree-node');
|
||||
if (!node) return false;
|
||||
@@ -892,7 +1271,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
children.className = 'tree-children';
|
||||
node.appendChild(children);
|
||||
}
|
||||
children.innerHTML = renderFileRows('', groupRowsByParent(cachedRows), currentDocumentId(), currentFileTreeActiveRowId());
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = renderFileRows('', groupRowsByParent(cachedRows), currentDocumentId(), currentFileTreeActiveRowId());
|
||||
children.replaceChildren(template.content.cloneNode(true));
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
@@ -900,31 +1281,47 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getFileTreeChildren(parentRelativePath) {
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var key = currentFileTreeParentKey(parentRelativePath);
|
||||
if (fileTreeState.loadedParents.has(key) && !fileTreeState.dirtyParents.has(key) && !fileTreeState.staleParents.has(key)) {
|
||||
return fileTreeState.rowsByParent.get(key) || [];
|
||||
}
|
||||
if (fileTreeState.loadingParents.has(key)) {
|
||||
return fileTreeState.loadingParents.get(key);
|
||||
}
|
||||
var generation = beginFileTreeRequest(key);
|
||||
var promise = fetchFileTreeProjection(parentRelativePath, { childrenOnly: true }).then(function(projection) {
|
||||
if (!isLatestFileTreeRequest(key, generation)) {
|
||||
return fileTreeState.rowsByParent.get(key) || [];
|
||||
}
|
||||
var rows = projectionItems(projection);
|
||||
rememberFileTreeProjection(parentRelativePath, rows, projection);
|
||||
return rows;
|
||||
}).finally(function() {
|
||||
fileTreeState.loadingParents.delete(key);
|
||||
});
|
||||
fileTreeState.loadingParents.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
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;
|
||||
var key = currentFileTreeParentKey(relativePath);
|
||||
var stale = fileTreeState.staleParents.has(key) || fileTreeState.dirtyParents.has(key);
|
||||
if (!stale && markExistingFileTreeChildrenLoaded(row, button)) return true;
|
||||
if (!stale && 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;
|
||||
setTreeRowExpanded(row, button, true);
|
||||
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);
|
||||
var rows = await getFileTreeChildren(relativePath);
|
||||
if (!rows.length) {
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
@@ -939,6 +1336,53 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
function fileTreeParentChainForRelativePath(relativePath) {
|
||||
var normalized = normalizeFileTreeRelativePath(relativePath);
|
||||
if (!normalized) return [];
|
||||
var parts = normalized.split('/').filter(Boolean);
|
||||
parts.pop();
|
||||
var chain = [];
|
||||
for (var index = 0; index < parts.length; index += 1) {
|
||||
chain.push(parts.slice(0, index + 1).join('/'));
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
async function revealFileTreeResource(input) {
|
||||
input = input || {};
|
||||
var rootUri = String(input.rootUri || currentRootUri() || '').trim();
|
||||
if (!rootUri || rootUri !== currentRootUri()) return false;
|
||||
ensureFileTreeLazyCacheScope();
|
||||
var relativePath = normalizeFileTreeRelativePath(input.relativePath || input.relative_path || '');
|
||||
var rowId = String(input.rowId || input.row_id || '').trim();
|
||||
var parentChain = fileTreeParentChainForRelativePath(relativePath);
|
||||
for (var index = 0; index < parentChain.length; index += 1) {
|
||||
var parentRelativePath = parentChain[index];
|
||||
var parentRow = fileTreeRowByRelativePath(parentRelativePath);
|
||||
if (!(parentRow instanceof HTMLElement)) continue;
|
||||
var button = parentRow.querySelector('[data-rust-action="toggle"]');
|
||||
await getFileTreeChildren(parentRelativePath);
|
||||
renderCachedFileTreeChildren(parentRow, button, parentRelativePath);
|
||||
setTreeRowExpanded(parentRow, button, true);
|
||||
}
|
||||
var targetRow = rowId
|
||||
? document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]')
|
||||
: null;
|
||||
if (!(targetRow instanceof HTMLElement) && relativePath) {
|
||||
targetRow = fileTreeRowByRelativePath(relativePath);
|
||||
}
|
||||
if (!(targetRow instanceof HTMLElement)) return false;
|
||||
var targetRowId = String(targetRow.getAttribute('data-row-id') || rowId || '').trim();
|
||||
if (targetRowId) {
|
||||
fileTreeViewState.selectedRowIds = new Set([targetRowId]);
|
||||
fileTreeViewState.focusedRowId = targetRowId;
|
||||
fileTreeViewState.activeRowId = targetRowId;
|
||||
}
|
||||
reprojectFileTreeSelectionState();
|
||||
try { targetRow.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleChildren(row, button) {
|
||||
var li = row && row.parentElement;
|
||||
var children = li ? li.querySelector(':scope > .tree-children') : null;
|
||||
@@ -962,6 +1406,10 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath || !fileTreeExpandedRelativePaths.has(relativePath)) return;
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
if (markExistingFileTreeChildrenLoaded(row, button)) {
|
||||
restored = true;
|
||||
return;
|
||||
}
|
||||
if (row.getAttribute('data-filetree-children-loaded') === 'true') {
|
||||
setTreeRowExpanded(row, button, true);
|
||||
restored = true;
|
||||
@@ -998,7 +1446,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var result = detail.result || {};
|
||||
if (action === 'create') {
|
||||
applyCreatedDocumentLocally(result, body.parentId || null, body.title || '新页面');
|
||||
if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
|
||||
refreshLocalFolderAfterCommand(action, result, body);
|
||||
return;
|
||||
}
|
||||
if (action === 'rename' && body.documentId && body.title) {
|
||||
@@ -1012,7 +1460,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
} else {
|
||||
updateTitleEverywhere(body.documentId, body.title);
|
||||
}
|
||||
if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot();
|
||||
refreshLocalFolderAfterCommand(action, result, body);
|
||||
document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'rename');
|
||||
return;
|
||||
}
|
||||
@@ -1020,20 +1468,25 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
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();
|
||||
refreshLocalFolderAfterCommand(action, result, body);
|
||||
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();
|
||||
refreshLocalFolderAfterCommand(action, result, body);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('tree:local-command-batch-complete', function(event) {
|
||||
var detail = event.detail || {};
|
||||
flushFileTreeBatchRefresh(detail.batchId || detail.batch_id || '');
|
||||
});
|
||||
|
||||
window.addEventListener('tree:snapshot', function(event) {
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
if (renderSidebarSnapshot(payload)) {
|
||||
if (renderLiveSidebarSnapshot(payload)) {
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
|
||||
return;
|
||||
}
|
||||
@@ -1061,7 +1514,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
|
||||
});
|
||||
}
|
||||
if (renderSidebarSnapshot(payload)) {
|
||||
if (renderLiveSidebarSnapshot(payload)) {
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
|
||||
return;
|
||||
}
|
||||
@@ -1073,7 +1526,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
|
||||
window.addEventListener('tree:resync', function(event) {
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
if (renderSidebarSnapshot(payload)) {
|
||||
if (renderLiveSidebarSnapshot(payload)) {
|
||||
refreshEditorLocalAttachmentExistence();
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
|
||||
return;
|
||||
@@ -1081,6 +1534,18 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
|
||||
setTreeLiveApplyError('tree_resync_missing_projection_payload');
|
||||
});
|
||||
|
||||
window.addEventListener('tree:local-folder-watch-batch', function(event) {
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
applyLocalFolderWatchBatch(payload);
|
||||
});
|
||||
|
||||
window.addEventListener('tree:error', function(event) {
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
var code = payload && (payload.code || payload.error || payload.message);
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-error-schema', String(payload && payload.schema || ''));
|
||||
setTreeLiveApplyError(code || 'tree_live_error');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1091,14 +1556,17 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
commandDocumentTitle,
|
||||
deltaNeedsProjectionRefresh,
|
||||
fileTreePageTitle,
|
||||
flushFileTreeBatchRefresh,
|
||||
installTreeLiveApplyEventListeners,
|
||||
isFileTreePageRow,
|
||||
applyLocalFolderWatchBatch,
|
||||
localCommandNeedsProjectionRefresh,
|
||||
normalizeFileTreePageRenameTitle,
|
||||
objectIdentityAttr,
|
||||
refreshLocalFolderSidebarSnapshot,
|
||||
removeDocumentRowForMode,
|
||||
renderSidebarSnapshot,
|
||||
revealFileTreeResource,
|
||||
restorePersistedFileTreeExpansionState,
|
||||
setTreeLiveApplyError,
|
||||
startLocalFolderSidebarWatch,
|
||||
|
||||
Reference in New Issue
Block a user