feat: 收口 Rust Web 3000 主链
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user