feat: 收口 Rust Web 3000 主链

This commit is contained in:
lix-2026
2026-05-11 13:16:34 +08:00
parent 7f3f7d4e2f
commit 17c003976b
61 changed files with 2090 additions and 2256 deletions
+99 -66
View File
@@ -23,7 +23,6 @@ const SIDEBAR_TREE_JS: &str = r##"
anchorRowId: null,
focusedRowId: null
};
var projectionRefreshTimer = 0;
var activeTreeContextMenu = null;
var activeEditorAttachmentLink = null;
var attachmentActionsHideTimer = 0;
@@ -854,6 +853,27 @@ const SIDEBAR_TREE_JS: &str = r##"
return value;
}
function readSidebarDataset(value) {
if (!value || typeof value !== 'object') return null;
if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset;
if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset;
if (value.dataset && typeof value.dataset === 'object') return value.dataset;
if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar;
return null;
}
function readDatasetProjection(value, snakeCaseKey, camelCaseKey) {
var dataset = readSidebarDataset(value);
if (!dataset || typeof dataset !== 'object') return null;
if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey];
if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey];
return null;
}
function setTreeLiveApplyError(reason) {
document.documentElement.setAttribute('data-mnote-tree-live-apply-error', String(reason || 'tree_live_apply_failed'));
}
function projectionItems(projection) {
var resolved = readProjection(projection);
return resolved && Array.isArray(resolved.items) ? resolved.items : [];
@@ -901,14 +921,16 @@ const SIDEBAR_TREE_JS: &str = r##"
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
var toggle = expandable
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
: '<span class="tree-spacer" aria-hidden="true"></span>';
var childHtml = expandable && expanded
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
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-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) + '"><span class="tree-link-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="' + 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) + '"><span class="tree-link-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('');
}
@@ -965,8 +987,8 @@ const SIDEBAR_TREE_JS: &str = r##"
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 && expanded
? '<ul class="tree-children">' + renderFileRows(nodeId, grouped, activeId) + '</ul>'
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId) + '</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-asset-id="' + escapeHtml(assetId) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKindOf(item)) + '" 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-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-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('');
@@ -981,30 +1003,41 @@ const SIDEBAR_TREE_JS: &str = r##"
return true;
}
async function fetchProjection(path, workspaceId) {
var url = new URL(path, window.location.origin);
url.searchParams.set('workspaceId', workspaceId || 'default');
url.searchParams.set('depth', '99');
var response = await fetch(url.toString(), { cache: 'no-store' });
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || !payload.result) throw new Error('tree_projection_failed_' + response.status);
return payload.result;
function renderSidebarSnapshot(payload) {
var renderedPage = renderPageProjection(payload);
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
var renderedFile = fileProjection ? renderFileProjection(fileProjection) : false;
return renderedPage || renderedFile;
}
function scheduleProjectionRefresh(workspaceId) {
if (projectionRefreshTimer) window.clearTimeout(projectionRefreshTimer);
projectionRefreshTimer = window.setTimeout(function() {
projectionRefreshTimer = 0;
var resolvedWorkspaceId = workspaceId || resolveWorkspaceId(document.body);
Promise.all([
fetchProjection('/api/tree/projections/sidebar', resolvedWorkspaceId).then(renderPageProjection),
fetchProjection('/api/tree/projections/file', resolvedWorkspaceId).then(renderFileProjection)
]).then(function() {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'true');
}).catch(function(error) {
document.documentElement.setAttribute('data-mnote-tree-live-apply-error', error instanceof Error ? error.message : String(error));
});
}, 180);
function isTitleOnlyDocumentPatch(candidate) {
if (!candidate || typeof candidate !== 'object') return false;
var allowedKeys = {
id: true,
documentId: true,
title: true,
updatedAt: true,
updated_at: true
};
return Object.keys(candidate).every(function(key) {
return allowedKeys[key] === true;
});
}
function deltaNeedsProjectionRefresh(payload) {
var data = payload && (payload.data || payload.delta || payload);
var op = data && typeof data.op === 'string' ? String(data.op).trim() : '';
if (!op || op === 'noop') return false;
if (op === 'upsert_document') {
return !isTitleOnlyDocumentPatch(data.node || data.document || null);
}
if (op === 'upsert_documents') {
var documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
if (documents.length > 0 && documents.every(isTitleOnlyDocumentPatch)) {
return false;
}
}
return true;
}
function toggleChildren(row, button) {
@@ -1673,19 +1706,6 @@ const SIDEBAR_TREE_JS: &str = r##"
return url.toString();
}
function openTreePicker(mode, detail) {
var url = new URL('/tree', window.location.origin);
url.searchParams.set('mode', 'picker');
url.searchParams.set('allowRootPick', '1');
url.searchParams.set('intent', mode);
if (detail.workspaceId) url.searchParams.set('workspaceId', detail.workspaceId);
if (detail.documentId) {
url.searchParams.set('sourceDocumentId', detail.documentId);
url.searchParams.set('excludeIds', detail.documentId);
}
window.location.assign(url.pathname + url.search);
}
function convertToPreviousSiblingChild(trigger, detail) {
var documentId = detail.documentId || '';
var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]');
@@ -1708,7 +1728,7 @@ const SIDEBAR_TREE_JS: &str = r##"
documentId: documentId,
parentId: previousId,
sortOrder: children.length
}).then(function(){ scheduleProjectionRefresh(detail.workspaceId || resolveWorkspaceId(row)); });
});
}
function handleTreeContextMenuAction(action, detail, trigger) {
@@ -1749,14 +1769,6 @@ const SIDEBAR_TREE_JS: &str = r##"
dispatchSidebarEvent('tree.page.share', detail);
return;
}
if (action === 'move') {
openTreePicker('move', detail);
return;
}
if (action === 'embed') {
openTreePicker('embed', detail);
return;
}
if (action === 'copy-link') {
void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link');
return;
@@ -1807,7 +1819,7 @@ const SIDEBAR_TREE_JS: &str = r##"
action: 'purge',
workspaceId: workspaceId,
documentId: documentId
}).then(function(){ scheduleProjectionRefresh(workspaceId); });
});
}
}
@@ -1877,14 +1889,10 @@ const SIDEBAR_TREE_JS: &str = r##"
{ action: 'color', icon: 'format_paint', label: '颜色' }
] : isAsset ? [
{ action: 'open-right', icon: 'open_in_new', label: '在右侧边栏打开', shortcut: 'Alt + O' },
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' }
] : [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ separator: true },
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
@@ -3340,6 +3348,12 @@ const SIDEBAR_TREE_JS: &str = r##"
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
return;
}
if (fileAction === 'open' && rowKind === 'folder') {
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
toggleChildren(fileRow, fileRow.querySelector('[data-rust-action="toggle"]'));
return;
}
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
@@ -3367,6 +3381,10 @@ const SIDEBAR_TREE_JS: &str = r##"
toggleChildren(row, btn);
e.preventDefault();
} else if (action === 'open') {
if (btn.getAttribute('data-page-openable') === 'false') {
e.preventDefault();
return;
}
var workspaceId = resolveWorkspaceId(btn);
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
e.preventDefault();
@@ -3384,7 +3402,6 @@ const SIDEBAR_TREE_JS: &str = r##"
title: title.trim()
}).then(function(){
updateTitleEverywhere(nodeId, title.trim());
scheduleProjectionRefresh(resolveWorkspaceId(btn));
});
}
} else if (action === 'menu') {
@@ -3586,7 +3603,7 @@ const SIDEBAR_TREE_JS: &str = r##"
documentId: sourceNodeId,
parentId: target.parentId,
sortOrder: target.sortOrder
}).then(function(){ scheduleProjectionRefresh(resolveWorkspaceId(pageRow)); });
});
return;
}
var fileTree = document.getElementById('sidebar-file-tree-root');
@@ -3637,10 +3654,11 @@ const SIDEBAR_TREE_JS: &str = r##"
window.addEventListener('tree:snapshot', function(event) {
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
if (renderPageProjection(payload)) {
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
return;
}
scheduleProjectionRefresh(payload && payload.workspaceId);
setTreeLiveApplyError('tree_snapshot_missing_projection_payload');
});
window.addEventListener('tree:delta', function(event) {
@@ -3652,14 +3670,24 @@ const SIDEBAR_TREE_JS: &str = r##"
updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
});
}
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
return;
}
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
scheduleProjectionRefresh(payload && payload.workspaceId);
if (deltaNeedsProjectionRefresh(payload)) {
setTreeLiveApplyError('tree_delta_missing_projection_payload');
}
});
window.addEventListener('tree:resync', function(event) {
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
return;
}
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
scheduleProjectionRefresh(payload && payload.workspaceId);
setTreeLiveApplyError('tree_resync_missing_projection_payload');
});
var tree = document.getElementById('sidebar-tree-root');
@@ -3693,7 +3721,6 @@ const TREE_LIVE_CONTROLLER_JS: &str = r##"
schema: 'mnote.tree_live_bootstrap.v1',
transport: 'convex-command-log-sse',
endpoint: '/api/tree/events',
resyncEndpoint: '/api/tree/projections/sidebar',
rootIds: [],
initialRevision: null
};
@@ -3790,9 +3817,6 @@ const TREE_LIVE_CONTROLLER_JS: &str = r##"
source.onerror = function(){
failures += 1;
applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting');
if (failures >= 3) {
dispatchTreeEvent('tree:resync-requested', { endpoint: bootstrap.resyncEndpoint, workspaceId: workspaceId });
}
};
}
@@ -3872,7 +3896,6 @@ pub fn PageLayout(
"rootIds": [],
"initialRevision": null,
"endpoint": "/api/tree/events",
"resyncEndpoint": "/api/tree/projections/sidebar",
"views": ["page-tree", "file-tree"]
})
.to_string();
@@ -3998,6 +4021,14 @@ mod tests {
));
assert!(!SIDEBAR_TREE_JS
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
assert!(SIDEBAR_TREE_JS.contains("renderSidebarSnapshot"));
assert!(SIDEBAR_TREE_JS.contains("kernel_file_tree_projection"));
assert!(SIDEBAR_TREE_JS.contains("deltaNeedsProjectionRefresh"));
assert!(SIDEBAR_TREE_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_JS.contains("setTreeLiveApplyError"));
assert!(!SIDEBAR_TREE_JS.contains("scheduleProjectionRefresh"));
assert!(!SIDEBAR_TREE_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_JS.contains("/api/tree/projections/file"));
}
#[test]
@@ -4009,5 +4040,7 @@ mod tests {
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested"));
}
}