|
|
|
@@ -4,20 +4,63 @@ use leptos::prelude::*;
|
|
|
|
|
|
|
|
|
|
const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
(function(){
|
|
|
|
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
|
|
|
|
window.__mnoteSidebarTreeRuntimeStarted = true;
|
|
|
|
|
|
|
|
|
|
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
|
|
|
|
|
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
|
|
|
|
|
var mnoteNavigationInFlight = '';
|
|
|
|
|
var draggingPageNodeId = '';
|
|
|
|
|
var activePageDropRow = null;
|
|
|
|
|
var draggingFileTreeRowIds = [];
|
|
|
|
|
var activeFileTreeDropRow = null;
|
|
|
|
|
var projectionRefreshTimer = 0;
|
|
|
|
|
|
|
|
|
|
function closestAction(target, selector) {
|
|
|
|
|
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value) {
|
|
|
|
|
return String(value == null ? '' : value)
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cssEscape(value) {
|
|
|
|
|
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
|
|
|
|
|
return String(value).replace(/["\\]/g, '\\$&');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function currentDocumentId() {
|
|
|
|
|
var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
|
|
|
|
|
return match ? decodeURIComponent(match[1]) : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveWorkspaceId(trigger) {
|
|
|
|
|
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
if (direct) return direct;
|
|
|
|
|
var root = trigger.closest('[data-workspace-id]');
|
|
|
|
|
return root ? (root.getAttribute('data-workspace-id') || '').trim() : '';
|
|
|
|
|
if (root) {
|
|
|
|
|
var value = (root.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
if (value) return value;
|
|
|
|
|
}
|
|
|
|
|
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCommandPending(trigger, pending) {
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
|
|
|
|
|
if ('disabled' in trigger) {
|
|
|
|
|
if (pending) trigger.setAttribute('disabled', 'disabled');
|
|
|
|
|
else trigger.removeAttribute('disabled');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function dispatchTreeCommand(trigger, body) {
|
|
|
|
|
trigger.setAttribute('data-pending', 'true');
|
|
|
|
|
trigger.setAttribute('disabled', 'disabled');
|
|
|
|
|
setCommandPending(trigger, true);
|
|
|
|
|
try {
|
|
|
|
|
var response = await fetch('/api/tree/commands', {
|
|
|
|
|
method: 'POST',
|
|
|
|
@@ -28,15 +71,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
if (!response.ok || !payload || !payload.result) {
|
|
|
|
|
throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status);
|
|
|
|
|
}
|
|
|
|
|
window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: body, result: payload.result } }));
|
|
|
|
|
return payload.result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
trigger.removeAttribute('disabled');
|
|
|
|
|
trigger.setAttribute('data-pending', 'false');
|
|
|
|
|
trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
|
|
|
|
|
setCommandPending(trigger, false);
|
|
|
|
|
if (trigger instanceof HTMLElement) {
|
|
|
|
|
trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function navigateToDocument(nodeId, workspaceId) {
|
|
|
|
|
if (!nodeId) return;
|
|
|
|
|
var url = '/documents/' + encodeURIComponent(nodeId) + (workspaceId ? '?workspaceId=' + encodeURIComponent(workspaceId) : '');
|
|
|
|
|
if (mnoteNavigationInFlight === url) return;
|
|
|
|
|
mnoteNavigationInFlight = url;
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
|
|
|
|
|
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
|
|
|
|
|
window.__mnoteTreeLiveEventSource.close();
|
|
|
|
|
}
|
|
|
|
|
window.location.assign(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createPage(trigger, parentId) {
|
|
|
|
|
var workspaceId = resolveWorkspaceId(trigger);
|
|
|
|
|
var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
|
|
|
|
@@ -48,7 +106,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
title: '新页面'
|
|
|
|
|
});
|
|
|
|
|
var nextWorkspaceId = result.workspaceId || workspaceId;
|
|
|
|
|
window.location.href = '/documents/' + encodeURIComponent(result.documentId) + '?workspaceId=' + encodeURIComponent(nextWorkspaceId);
|
|
|
|
|
navigateToDocument(result.documentId, nextWorkspaceId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function switchSidebarTreeTab(trigger) {
|
|
|
|
@@ -69,6 +127,192 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTitleEverywhere(documentId, title) {
|
|
|
|
|
if (!documentId) return;
|
|
|
|
|
var selectors = [
|
|
|
|
|
'[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
|
|
|
|
'[data-document-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
|
|
|
|
'[data-doc-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
|
|
|
|
'[data-node-id="' + cssEscape(documentId) + '"] .wolai-row-title'
|
|
|
|
|
];
|
|
|
|
|
selectors.forEach(function(selector) {
|
|
|
|
|
document.querySelectorAll(selector).forEach(function(node) {
|
|
|
|
|
if (node instanceof HTMLElement) node.textContent = title;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readProjection(value) {
|
|
|
|
|
if (!value || typeof value !== 'object') return null;
|
|
|
|
|
if (value.result && typeof value.result === 'object') return value.result;
|
|
|
|
|
if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
|
|
|
|
|
if (value.data && value.data.tree) return value.data.tree;
|
|
|
|
|
if (value.tree && typeof value.tree === 'object') return value.tree;
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function projectionItems(projection) {
|
|
|
|
|
var resolved = readProjection(projection);
|
|
|
|
|
return resolved && Array.isArray(resolved.items) ? resolved.items : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nodeIdOf(item) {
|
|
|
|
|
return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rowIdOf(item) {
|
|
|
|
|
return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parentIdOf(item) {
|
|
|
|
|
return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function titleOf(item) {
|
|
|
|
|
return String(item && item.title || '无标题').trim() || '无标题';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function groupRowsByParent(rows) {
|
|
|
|
|
var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
|
|
|
|
|
var grouped = new Map();
|
|
|
|
|
rows.forEach(function(item) {
|
|
|
|
|
var parentId = parentIdOf(item);
|
|
|
|
|
if (!ids.has(parentId)) parentId = '';
|
|
|
|
|
if (!grouped.has(parentId)) grouped.set(parentId, []);
|
|
|
|
|
grouped.get(parentId).push(item);
|
|
|
|
|
});
|
|
|
|
|
return grouped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPageRows(parentId, grouped, activeId) {
|
|
|
|
|
return (grouped.get(parentId) || []).map(function(item) {
|
|
|
|
|
var nodeId = nodeIdOf(item);
|
|
|
|
|
var title = titleOf(item);
|
|
|
|
|
var depth = Number(item.depth || 0);
|
|
|
|
|
var parent = parentIdOf(item);
|
|
|
|
|
var children = grouped.get(nodeId) || [];
|
|
|
|
|
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
|
|
|
|
var expanded = expandable && item.expandedByDefault !== false;
|
|
|
|
|
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) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
|
|
|
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
|
|
|
var childHtml = expandable && expanded
|
|
|
|
|
? '<ul class="tree-children">' + renderPageRows(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="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) + '" data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><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-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="' + escapeHtml(nodeId) + '" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPageProjection(projection) {
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
if (!tree) return false;
|
|
|
|
|
var rows = projectionItems(projection).filter(function(item) {
|
|
|
|
|
return String(item.rowKind || 'document') === 'document';
|
|
|
|
|
});
|
|
|
|
|
var activeId = currentDocumentId();
|
|
|
|
|
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileDocumentId(item) {
|
|
|
|
|
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
|
|
|
|
if (meta.documentId) return String(meta.documentId).trim();
|
|
|
|
|
if (item && item.documentId) return String(item.documentId).trim();
|
|
|
|
|
if (item && item.rowKind === 'document') return nodeIdOf(item);
|
|
|
|
|
if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileAssetId(item) {
|
|
|
|
|
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
|
|
|
|
if (meta.assetId) return String(meta.assetId).trim();
|
|
|
|
|
if (item && item.assetId) return String(item.assetId).trim();
|
|
|
|
|
if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function iconKindOf(item) {
|
|
|
|
|
return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderFileRows(parentId, grouped, activeId) {
|
|
|
|
|
return (grouped.get(parentId) || []).map(function(item) {
|
|
|
|
|
var nodeId = nodeIdOf(item);
|
|
|
|
|
var rowId = rowIdOf(item);
|
|
|
|
|
var rowKind = String(item.rowKind || 'document');
|
|
|
|
|
var title = titleOf(item);
|
|
|
|
|
var depth = Number(item.depth || 0);
|
|
|
|
|
var parent = parentIdOf(item);
|
|
|
|
|
var documentId = fileDocumentId(item);
|
|
|
|
|
var assetId = fileAssetId(item);
|
|
|
|
|
var children = grouped.get(nodeId) || [];
|
|
|
|
|
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
|
|
|
|
var expanded = expandable && item.expandedByDefault !== false;
|
|
|
|
|
var selected = rowId === 'doc:' + activeId || rowId === 'index:' + activeId || documentId === activeId;
|
|
|
|
|
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
|
|
|
|
|
var toggle = expandable
|
|
|
|
|
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
|
|
|
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
|
|
|
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>'
|
|
|
|
|
: '';
|
|
|
|
|
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('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderFileProjection(projection) {
|
|
|
|
|
var tree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (!tree) return false;
|
|
|
|
|
var rows = projectionItems(projection);
|
|
|
|
|
var activeId = currentDocumentId();
|
|
|
|
|
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="filetree-empty">暂无文件或页面</li>') + '</ul>';
|
|
|
|
|
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 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 toggleChildren(row, button) {
|
|
|
|
|
var li = row && row.parentElement;
|
|
|
|
|
var children = li ? li.querySelector(':scope > .tree-children') : null;
|
|
|
|
|
if (!children) 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.textContent = collapsed ? '▸' : '▾';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dispatchSidebarEvent(name, detail) {
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
|
|
|
|
|
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
|
|
|
|
if (tabTrigger) {
|
|
|
|
@@ -84,6 +328,43 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (fileTree && fileTree.contains(e.target)) {
|
|
|
|
|
var fileBtn = closestAction(e.target, '[data-rust-action]');
|
|
|
|
|
var fileRow = closestAction(e.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
if (!fileRow) return;
|
|
|
|
|
var fileAction = fileBtn ? fileBtn.getAttribute('data-rust-action') : 'open';
|
|
|
|
|
var rowId = fileRow.getAttribute('data-row-id') || '';
|
|
|
|
|
var rowKind = fileRow.getAttribute('data-row-kind') || '';
|
|
|
|
|
var documentId = fileRow.getAttribute('data-document-id') || fileRow.getAttribute('data-doc-id') || '';
|
|
|
|
|
var assetId = fileRow.getAttribute('data-asset-id') || '';
|
|
|
|
|
if (fileAction === 'toggle') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleChildren(fileRow, fileBtn);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (fileAction === 'create') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
void createPage(fileBtn || fileRow, documentId || fileRow.getAttribute('data-node-id'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (fileAction === 'menu') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
fileTree.querySelectorAll('.tree-row[data-selected="true"]').forEach(function(row) {
|
|
|
|
|
if (row instanceof HTMLElement) row.setAttribute('data-selected', 'false');
|
|
|
|
|
});
|
|
|
|
|
fileRow.setAttribute('data-selected', 'true');
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
|
|
|
|
if ((rowKind === 'document' || rowKind === 'index') && documentId) {
|
|
|
|
|
navigateToDocument(documentId, resolveWorkspaceId(fileRow));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
if (!tree || !tree.contains(e.target)) return;
|
|
|
|
|
var btn = closestAction(e.target, '[data-rust-action]');
|
|
|
|
@@ -94,18 +375,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
if (action === 'toggle') {
|
|
|
|
|
var row = btn.closest('.tree-row');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
var li = row.parentElement;
|
|
|
|
|
var children = li.querySelector(':scope > .tree-children');
|
|
|
|
|
if (children) {
|
|
|
|
|
children.classList.toggle('tree-children--collapsed');
|
|
|
|
|
var isCollapsed = children.classList.contains('tree-children--collapsed');
|
|
|
|
|
row.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
|
|
|
|
|
btn.textContent = isCollapsed ? '▸' : '▾';
|
|
|
|
|
}
|
|
|
|
|
toggleChildren(row, btn);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
} else if (action === 'open') {
|
|
|
|
|
var workspaceId = resolveWorkspaceId(btn);
|
|
|
|
|
window.location.href = '/documents/' + encodeURIComponent(nodeId) + (workspaceId ? '?workspaceId=' + encodeURIComponent(workspaceId) : '');
|
|
|
|
|
navigateToDocument(nodeId, workspaceId);
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
} else if (action === 'create') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
@@ -119,23 +393,323 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
workspaceId: resolveWorkspaceId(btn),
|
|
|
|
|
documentId: nodeId,
|
|
|
|
|
title: title.trim()
|
|
|
|
|
}).then(function(){ window.location.reload(); });
|
|
|
|
|
}).then(function(){
|
|
|
|
|
updateTitleEverywhere(nodeId, title.trim());
|
|
|
|
|
scheduleProjectionRefresh(resolveWorkspaceId(btn));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if (action === 'menu') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function readPageDragNodeId(event) {
|
|
|
|
|
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
|
|
|
|
|
return (fromTransfer || draggingPageNodeId || '').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearPageDropFeedback() {
|
|
|
|
|
if (activePageDropRow instanceof HTMLElement) {
|
|
|
|
|
activePageDropRow.setAttribute('data-drop-feedback', 'false');
|
|
|
|
|
}
|
|
|
|
|
activePageDropRow = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canDropPage(sourceNodeId, targetRow) {
|
|
|
|
|
if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false;
|
|
|
|
|
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
|
|
|
|
|
if (!targetNodeId || targetNodeId === sourceNodeId) return false;
|
|
|
|
|
var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]');
|
|
|
|
|
return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pageDropPosition(event, row) {
|
|
|
|
|
var rect = row.getBoundingClientRect();
|
|
|
|
|
var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
|
|
|
|
|
if (ratio < 0.25) return 'before';
|
|
|
|
|
if (ratio > 0.75) return 'after';
|
|
|
|
|
return 'inside';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolvePageMoveTarget(targetRow, position) {
|
|
|
|
|
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
|
|
|
|
|
var parentId = targetRow.getAttribute('data-parent-id') || null;
|
|
|
|
|
if (position === 'inside') {
|
|
|
|
|
var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
|
|
|
|
|
return { parentId: targetNodeId, sortOrder: children.length };
|
|
|
|
|
}
|
|
|
|
|
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) {
|
|
|
|
|
return (row.getAttribute('data-parent-id') || '') === (parentId || '');
|
|
|
|
|
});
|
|
|
|
|
var index = siblings.indexOf(targetRow);
|
|
|
|
|
return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('dragstart', function(event) {
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]');
|
|
|
|
|
if (pageRow) {
|
|
|
|
|
draggingPageNodeId = pageRow.getAttribute('data-node-id') || '';
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId);
|
|
|
|
|
event.dataTransfer.setData('text/plain', draggingPageNodeId);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
|
|
|
|
|
if (fileRow) {
|
|
|
|
|
var rowId = fileRow.getAttribute('data-row-id') || '';
|
|
|
|
|
draggingFileTreeRowIds = rowId ? [rowId] : [];
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
|
|
|
|
|
event.dataTransfer.effectAllowed = 'copyMove';
|
|
|
|
|
event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
|
|
|
|
|
event.dataTransfer.setData('application/x-mnote-file-tree', payload);
|
|
|
|
|
event.dataTransfer.setData('text/plain', payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
if (!tree) return;
|
|
|
|
|
var currentPath = window.location.pathname;
|
|
|
|
|
var match = currentPath.match(/^\/documents\/([^\/]+)/);
|
|
|
|
|
if (match) {
|
|
|
|
|
var activeId = match[1];
|
|
|
|
|
var links = tree.querySelectorAll('[data-rust-action="open"]');
|
|
|
|
|
for (var i = 0; i < links.length; i++) {
|
|
|
|
|
if (links[i].getAttribute('data-node-id') === activeId) {
|
|
|
|
|
links[i].closest('.tree-row').setAttribute('data-active', 'true');
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('dragover', function(event) {
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
|
|
|
|
var sourceNodeId = readPageDragNodeId(event);
|
|
|
|
|
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
pageRow.setAttribute('data-drop-feedback', 'true');
|
|
|
|
|
pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow));
|
|
|
|
|
activePageDropRow = pageRow;
|
|
|
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (fileTree && fileTree.contains(event.target)) {
|
|
|
|
|
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
|
|
|
|
|
var hasInternal = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
|
|
|
|
|
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
|
|
|
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = hasFiles || event.altKey ? 'copy' : 'move';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('drop', function(event) {
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
|
|
|
|
var sourceNodeId = readPageDragNodeId(event);
|
|
|
|
|
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
var position = pageDropPosition(event, pageRow);
|
|
|
|
|
var target = resolvePageMoveTarget(pageRow, position);
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
draggingPageNodeId = '';
|
|
|
|
|
void dispatchTreeCommand(pageRow, {
|
|
|
|
|
action: 'move',
|
|
|
|
|
workspaceId: resolveWorkspaceId(pageRow),
|
|
|
|
|
documentId: sourceNodeId,
|
|
|
|
|
parentId: target.parentId,
|
|
|
|
|
sortOrder: target.sortOrder
|
|
|
|
|
}).then(function(){ scheduleProjectionRefresh(resolveWorkspaceId(pageRow)); });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (fileTree && fileTree.contains(event.target)) {
|
|
|
|
|
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
|
|
|
|
|
var raw = event.dataTransfer ? event.dataTransfer.getData(FILETREE_DRAG_MIME) || event.dataTransfer.getData('application/x-mnote-file-tree') || '' : '';
|
|
|
|
|
var rowIds = draggingFileTreeRowIds.slice();
|
|
|
|
|
if (raw) {
|
|
|
|
|
try {
|
|
|
|
|
var parsed = JSON.parse(raw);
|
|
|
|
|
if (Array.isArray(parsed.rowIds)) rowIds = parsed.rowIds;
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
if (!files.length && !rowIds.length) return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
var detail = {
|
|
|
|
|
workspaceId: resolveWorkspaceId(targetRow || fileTree),
|
|
|
|
|
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
|
|
|
|
|
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
|
|
|
|
|
documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
|
|
|
|
|
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null
|
|
|
|
|
};
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
activeFileTreeDropRow = null;
|
|
|
|
|
if (files.length) {
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
|
|
|
|
|
} else {
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true }));
|
|
|
|
|
}
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('dragend', function() {
|
|
|
|
|
draggingPageNodeId = '';
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
activeFileTreeDropRow = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:title-updated', function(event) {
|
|
|
|
|
var detail = event.detail || {};
|
|
|
|
|
updateTitleEverywhere(detail.documentId, detail.title);
|
|
|
|
|
scheduleProjectionRefresh(detail.workspaceId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:snapshot', function(event) {
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
if (renderPageProjection(payload)) {
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
|
|
|
|
|
}
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:delta', function(event) {
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
var data = payload && (payload.data || payload.delta || payload);
|
|
|
|
|
var documents = data && (data.upsertDocuments || data.upsert_documents);
|
|
|
|
|
if (Array.isArray(documents)) {
|
|
|
|
|
documents.forEach(function(doc) {
|
|
|
|
|
updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:resync', function(event) {
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
var activeId = currentDocumentId();
|
|
|
|
|
if (tree && activeId) {
|
|
|
|
|
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
|
|
|
|
|
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
"##;
|
|
|
|
|
|
|
|
|
|
const TREE_LIVE_CONTROLLER_JS: &str = r##"
|
|
|
|
|
(function(){
|
|
|
|
|
if (window.__mnoteTreeLiveControllerStarted) return;
|
|
|
|
|
window.__mnoteTreeLiveControllerStarted = true;
|
|
|
|
|
|
|
|
|
|
function readBootstrap() {
|
|
|
|
|
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
|
|
|
|
|
var fallback = {
|
|
|
|
|
schema: 'mnote.tree_live_bootstrap.v1',
|
|
|
|
|
transport: 'convex-command-log-sse',
|
|
|
|
|
endpoint: '/api/tree/events',
|
|
|
|
|
resyncEndpoint: '/api/tree/projections/sidebar',
|
|
|
|
|
rootIds: [],
|
|
|
|
|
initialRevision: null
|
|
|
|
|
};
|
|
|
|
|
if (!script || !script.textContent) return fallback;
|
|
|
|
|
try {
|
|
|
|
|
return Object.assign(fallback, JSON.parse(script.textContent));
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveWorkspaceId() {
|
|
|
|
|
var withWorkspace = document.querySelector('[data-workspace-id]');
|
|
|
|
|
if (withWorkspace) {
|
|
|
|
|
var value = (withWorkspace.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
if (value) return value;
|
|
|
|
|
}
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
return (params.get('workspaceId') || 'default').trim() || 'default';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dispatchTreeEvent(name, detail) {
|
|
|
|
|
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyStatus(status) {
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-status', status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyTransport(transport) {
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeActiveSource() {
|
|
|
|
|
var source = window.__mnoteTreeLiveEventSource;
|
|
|
|
|
if (source && typeof source.close === 'function') {
|
|
|
|
|
source.close();
|
|
|
|
|
window.__mnoteTreeLiveEventSource = null;
|
|
|
|
|
applyStatus('closed');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function start() {
|
|
|
|
|
if (!('EventSource' in window)) {
|
|
|
|
|
applyStatus('unsupported');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var bootstrap = readBootstrap();
|
|
|
|
|
applyTransport(bootstrap.transport || 'convex-command-log-sse');
|
|
|
|
|
var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
|
|
|
|
|
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
|
|
|
|
|
url.searchParams.set('workspaceId', workspaceId);
|
|
|
|
|
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
|
|
|
|
|
url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
|
|
|
|
|
}
|
|
|
|
|
var failures = 0;
|
|
|
|
|
var source = new EventSource(url.toString());
|
|
|
|
|
window.__mnoteTreeLiveEventSource = source;
|
|
|
|
|
applyStatus('connecting');
|
|
|
|
|
|
|
|
|
|
source.addEventListener('open', function(){
|
|
|
|
|
failures = 0;
|
|
|
|
|
applyStatus('connected');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
source.addEventListener('snapshot', function(event){
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
source.addEventListener('delta', function(event){
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
source.addEventListener('resync', function(event){
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
source.onerror = function(){
|
|
|
|
|
failures += 1;
|
|
|
|
|
applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting');
|
|
|
|
|
if (failures >= 3) {
|
|
|
|
|
dispatchTreeEvent('tree:resync-requested', { endpoint: bootstrap.resyncEndpoint, workspaceId: workspaceId });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.__mnoteTreeLiveClose = closeActiveSource;
|
|
|
|
|
window.addEventListener('pagehide', closeActiveSource, { once: true });
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
|
|
|
} else {
|
|
|
|
|
start();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
"##;
|
|
|
|
@@ -198,6 +772,17 @@ pub fn PageLayout(
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
let tree_live_bootstrap = serde_json::json!({
|
|
|
|
|
"schema": "mnote.tree_live_bootstrap.v1",
|
|
|
|
|
"transport": "convex-command-log-sse",
|
|
|
|
|
"workspaceId": null,
|
|
|
|
|
"rootIds": [],
|
|
|
|
|
"initialRevision": null,
|
|
|
|
|
"endpoint": "/api/tree/events",
|
|
|
|
|
"resyncEndpoint": "/api/tree/projections/sidebar",
|
|
|
|
|
"views": ["page-tree", "file-tree"]
|
|
|
|
|
})
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
view! {
|
|
|
|
|
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
|
|
|
@@ -208,15 +793,17 @@ pub fn PageLayout(
|
|
|
|
|
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
|
|
|
|
</div>
|
|
|
|
|
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
|
|
|
|
<a href="/search" class:active={current_nav == "search"} title="搜索"><span class="nav-icon">"⌕"</span></a>
|
|
|
|
|
<a href="/graph" title="关系图"><span class="nav-icon">"⌘"</span></a>
|
|
|
|
|
<a href="/actions" title="快捷动作"><span class="nav-icon">"↯"</span></a>
|
|
|
|
|
<a href="/help" title="帮助"><span class="nav-icon">"?"</span></a>
|
|
|
|
|
<a href="/files" title="文件"><span class="nav-icon">"▣"</span></a>
|
|
|
|
|
<a href="/more" title="更多"><span class="nav-icon">"…"</span></a>
|
|
|
|
|
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><svg class="mnote-symbol nav-icon" data-icon="search" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="5.5"></circle><path d="m15 15 5 5"></path></svg></a>
|
|
|
|
|
<a href="/graph" title="关系图" aria-label="关系图"><svg class="mnote-symbol nav-icon" data-icon="account_tree" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6h5v5H6z"></path><path d="M15 3h5v5h-5z"></path><path d="M15 16h5v5h-5z"></path><path d="M11 8.5h2.5a4 4 0 0 0 4-4"></path><path d="M11 8.5h2.5a4 4 0 0 1 4 4V16"></path></svg></a>
|
|
|
|
|
<a href="/actions" title="快捷动作" aria-label="快捷动作"><svg class="mnote-symbol nav-icon" data-icon="bolt" viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 5 14h6l-1 8 9-13h-6z"></path></svg></a>
|
|
|
|
|
<a href="/help" title="帮助" aria-label="帮助"><svg class="mnote-symbol nav-icon" data-icon="help" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.8 9a2.4 2.4 0 0 1 4.5 1.2c0 1.7-2.3 2-2.3 3.8"></path><path d="M12 17.5h.01"></path></svg></a>
|
|
|
|
|
<a href="/files" title="文件" aria-label="文件"><svg class="mnote-symbol nav-icon" data-icon="inventory_2" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v4H4z"></path><path d="M6 10v9h12v-9"></path><path d="M9 14h6"></path></svg></a>
|
|
|
|
|
<a href="/more" title="更多" aria-label="更多"><svg class="mnote-symbol nav-icon" data-icon="more_horiz" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h.01"></path><path d="M12 12h.01"></path><path d="M19 12h.01"></path></svg></a>
|
|
|
|
|
</nav>
|
|
|
|
|
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
|
|
|
|
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
|
|
|
|
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
|
|
|
|
<script id="mnote-tree-live-controller" inner_html={TREE_LIVE_CONTROLLER_JS.to_string()}></script>
|
|
|
|
|
</aside>
|
|
|
|
|
<div class="mnote-main">
|
|
|
|
|
<header class="wolai-topbar" data-testid="wolai-topbar">
|
|
|
|
@@ -225,7 +812,7 @@ pub fn PageLayout(
|
|
|
|
|
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
|
|
|
|
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span>
|
|
|
|
|
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span>
|
|
|
|
|
<span class="wolai-breadcrumb-current"><span class="wolai-home-icon" aria-hidden="true">"⌂"</span>{topbar_title}</span>
|
|
|
|
|
<span class="wolai-breadcrumb-current"><svg class="mnote-symbol wolai-home-icon" data-icon="home" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 10.5 12 3l8 7.5"></path><path d="M6.5 9.5V21h11V9.5"></path><path d="M9.5 21v-6h5v6"></path></svg><span data-page-title-current="true">{topbar_title}</span></span>
|
|
|
|
|
</nav>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="wolai-topbar-actions" aria-label="页面操作">
|
|
|
|
@@ -248,3 +835,34 @@ pub fn PageLayout(
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::{SIDEBAR_TREE_JS, TREE_LIVE_CONTROLLER_JS};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("data-drop-feedback"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|