feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -141,7 +141,7 @@ import {
|
||||
};
|
||||
const pageAggregateUrlFromDescriptor = (descriptor) => {
|
||||
const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
||||
url.searchParams.set('sourceKind', descriptor.sourceKind || 'convex_workspace');
|
||||
url.searchParams.set('sourceKind', descriptor.sourceKind || 'local_folder');
|
||||
if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
||||
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
||||
return url;
|
||||
@@ -151,10 +151,10 @@ import {
|
||||
documentId: aggregate?.identity?.documentId || aggregate?.identity?.document_id || descriptor.documentId,
|
||||
workspaceId: aggregate?.identity?.workspaceId || aggregate?.identity?.workspace_id || descriptor.workspaceId || '',
|
||||
paneRole,
|
||||
sourceKind: descriptor.sourceKind || 'convex_workspace',
|
||||
sourceKind: descriptor.sourceKind || 'local_folder',
|
||||
rootUri: descriptor.rootUri || '',
|
||||
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
|
||||
saveEndpoint: (descriptor.sourceKind || 'convex_workspace') === 'local_folder'
|
||||
saveEndpoint: (descriptor.sourceKind || 'local_folder') === 'local_folder'
|
||||
? '/api/page-body/write'
|
||||
: '/api/documents/save',
|
||||
titleEndpoint: '/api/documents/title',
|
||||
@@ -225,6 +225,48 @@ import {
|
||||
if (pageTab instanceof HTMLElement) return String(pageTab.getAttribute('data-document-id') || '').trim();
|
||||
return '';
|
||||
};
|
||||
const currentWebShellSourceKind = () => {
|
||||
try {
|
||||
return currentUrl().searchParams.get('sourceKind') || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
const currentWebShellRootUri = () => {
|
||||
try {
|
||||
return currentUrl().searchParams.get('rootUri') || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const restoreResourceTabInputFromUrl = () => {
|
||||
const url = currentUrl();
|
||||
const raw = String(url.searchParams.get('resourceTab') || '').trim();
|
||||
if (!raw) return null;
|
||||
const objectIdentity = raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw;
|
||||
if (!objectIdentity.startsWith('resource:file:')) return null;
|
||||
const rootUri = currentWebShellRootUri();
|
||||
if (!rootUri) return null;
|
||||
const rest = objectIdentity.slice('resource:file:'.length);
|
||||
const prefix = `${rootUri}:`;
|
||||
if (!rest.startsWith(prefix)) return null;
|
||||
const path = rest.slice(prefix.length).replace(/^\/+/, '');
|
||||
if (!path) return null;
|
||||
const title = path.split('/').filter(Boolean).pop() || path;
|
||||
return {
|
||||
objectIdentity,
|
||||
documentId: currentWebShellDocumentId(),
|
||||
workspaceId: currentWebShellWorkspaceId(),
|
||||
sourceKind: currentWebShellSourceKind() || 'local_folder',
|
||||
rootUri,
|
||||
path,
|
||||
assetId: `local:asset:${path}`,
|
||||
title,
|
||||
fileName: title,
|
||||
openTarget: 'active-tab',
|
||||
};
|
||||
};
|
||||
|
||||
let nextViewId = 1;
|
||||
const mindmapHost = createMindmapHostRuntime({
|
||||
@@ -267,12 +309,18 @@ import {
|
||||
const markSessionExternalConflict = (...args) => documentSessions.markSessionExternalConflict(...args);
|
||||
const queueSessionSave = (...args) => documentSessions.queueSessionSave(...args);
|
||||
const refreshSessionFromExternalChange = (...args) => documentSessions.refreshSessionFromExternalChange(...args);
|
||||
const ensureLocalFolderEventChannel = (...args) => documentSessions.ensureLocalFolderEventChannel(...args);
|
||||
const sessionMatchesDocumentWorkspace = (...args) => documentSessions.sessionMatchesDocumentWorkspace(...args);
|
||||
const getOrCreateDocumentSession = (...args) => documentSessions.getOrCreateDocumentSession(...args);
|
||||
|
||||
const updatePaneChrome = (runtimeDescriptor) => {
|
||||
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
||||
if (!(pane instanceof HTMLElement)) return;
|
||||
if (runtimeDescriptor.paneRole === 'primary') {
|
||||
document.querySelectorAll('[data-mnote-navigation-page-placeholder][data-pane-role="primary"]').forEach((node) => {
|
||||
if (node instanceof HTMLElement) node.hidden = true;
|
||||
});
|
||||
}
|
||||
const aggregate = runtimeDescriptor.aggregate || {};
|
||||
const bootstrap = runtimeDescriptor.bootstrap || {};
|
||||
const title = aggregate?.head?.title || '无标题';
|
||||
@@ -366,9 +414,94 @@ import {
|
||||
return payload.result;
|
||||
};
|
||||
|
||||
const ensureLazyPrimaryPaneRoot = () => {
|
||||
let root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
|
||||
if (root instanceof HTMLElement) return root;
|
||||
const pagePanel = document.querySelector('[data-mnote-page-tab-panel][data-pane-role="primary"]');
|
||||
if (!(pagePanel instanceof HTMLElement)) return null;
|
||||
const pane = document.createElement('section');
|
||||
pane.className = 'document-pane';
|
||||
pane.setAttribute('data-document-pane', 'true');
|
||||
pane.setAttribute('data-pane-role', 'primary');
|
||||
pane.setAttribute('data-pane-document-id', '');
|
||||
pane.setAttribute('data-pane-workspace-id', currentWebShellWorkspaceId());
|
||||
pane.setAttribute('data-pane-visible', 'false');
|
||||
pane.hidden = true;
|
||||
|
||||
const shell = document.createElement('main');
|
||||
shell.className = 'document-shell';
|
||||
shell.setAttribute('data-editor-host', 'leptos_tiptap_island');
|
||||
shell.setAttribute('data-document-id', '');
|
||||
shell.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
|
||||
shell.setAttribute('data-pane-role', 'primary');
|
||||
shell.setAttribute('data-page-wide-layout', 'false');
|
||||
shell.setAttribute('data-page-small-text', 'false');
|
||||
shell.setAttribute('data-layout-density', 'normal');
|
||||
shell.setAttribute('data-page-font', 'default');
|
||||
shell.setAttribute('data-page-show-heading-numbers', 'false');
|
||||
|
||||
const header = document.createElement('header');
|
||||
header.className = 'document-shell-header';
|
||||
header.setAttribute('data-page-title-hidden', 'true');
|
||||
header.hidden = true;
|
||||
const titleHeading = document.createElement('h1');
|
||||
titleHeading.className = 'document-title-heading';
|
||||
const titleInput = document.createElement('textarea');
|
||||
titleInput.id = 'mnote-page-title-input';
|
||||
titleInput.className = 'document-title-input';
|
||||
titleInput.setAttribute('aria-label', '页面标题');
|
||||
titleInput.setAttribute('data-page-title-input', 'true');
|
||||
titleInput.setAttribute('data-document-id', '');
|
||||
titleInput.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
|
||||
titleInput.setAttribute('data-pane-role', 'primary');
|
||||
titleInput.setAttribute('data-title-endpoint', '/api/documents/title');
|
||||
titleInput.rows = 1;
|
||||
titleHeading.appendChild(titleInput);
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'document-shell-meta';
|
||||
meta.setAttribute('aria-label', '页面元信息');
|
||||
const currentTitle = document.createElement('span');
|
||||
currentTitle.setAttribute('data-page-title-current', 'true');
|
||||
meta.appendChild(currentTitle);
|
||||
header.append(titleHeading, meta);
|
||||
|
||||
const aggregateMarker = document.createElement('section');
|
||||
aggregateMarker.setAttribute('data-page-aggregate-snapshot', 'mnote.page_aggregate.v1');
|
||||
const subtree = document.createElement('section');
|
||||
subtree.setAttribute('data-testid', 'mnote-page-subtree');
|
||||
subtree.setAttribute('data-page-tree-source', 'page_aggregate.tree.pageSubtree');
|
||||
subtree.setAttribute('data-page-subtree-present', 'false');
|
||||
subtree.setAttribute('data-pane-role', 'primary');
|
||||
|
||||
const island = document.createElement('section');
|
||||
island.id = 'mnote-editor-island';
|
||||
island.setAttribute('data-editor-host', 'leptos_tiptap_island');
|
||||
island.setAttribute('data-pane-role', 'primary');
|
||||
root = document.createElement('div');
|
||||
root.id = 'mnote-leptos-tiptap-island-editor-root';
|
||||
root.setAttribute('data-testid', 'mnote-leptos-tiptap-island-editor-root');
|
||||
root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
||||
root.setAttribute('data-runtime-editor-status', 'booting');
|
||||
root.setAttribute('data-pane-role', 'primary');
|
||||
const observability = document.createElement('div');
|
||||
observability.className = 'sr-only';
|
||||
observability.setAttribute('data-editor-host-observability', 'rust-web-inline-island');
|
||||
observability.setAttribute('data-editor-host-active', 'leptos_tiptap_island');
|
||||
observability.setAttribute('data-editor-host-requested', 'leptos_tiptap_island');
|
||||
observability.setAttribute('data-editor-host-status', 'booting');
|
||||
observability.setAttribute('data-pane-role', 'primary');
|
||||
island.append(root, observability);
|
||||
shell.append(header, aggregateMarker, subtree, island);
|
||||
pane.appendChild(shell);
|
||||
pagePanel.appendChild(pane);
|
||||
return root;
|
||||
};
|
||||
|
||||
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
||||
mindmapHost.unmountMindmapPane(paneRole);
|
||||
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
||||
const root = paneRole === 'primary'
|
||||
? ensureLazyPrimaryPaneRoot()
|
||||
: document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
||||
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
|
||||
if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
|
||||
const runtimePromise = loadRuntime();
|
||||
@@ -428,14 +561,14 @@ import {
|
||||
return {
|
||||
documentId,
|
||||
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
||||
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'convex_workspace',
|
||||
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'local_folder',
|
||||
rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
documentId,
|
||||
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
||||
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'convex_workspace',
|
||||
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'local_folder',
|
||||
rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
|
||||
};
|
||||
};
|
||||
@@ -529,6 +662,11 @@ import {
|
||||
broadcastSessionContent(session, view);
|
||||
}
|
||||
if ((pendingExternalChange || recentExternalChange) && (session.dirty || recentLocalInput)) {
|
||||
if (session.sourceKind === 'local_folder' && session.sessionKind !== 'resource') {
|
||||
session.externalChangePending = true;
|
||||
if (session.dirty) queueSessionSave(session);
|
||||
return;
|
||||
}
|
||||
markSessionExternalConflict(session, externalConflictMessage);
|
||||
return;
|
||||
}
|
||||
@@ -629,6 +767,9 @@ import {
|
||||
} else {
|
||||
setStatus(runtimeDescriptor, 'ready');
|
||||
}
|
||||
if (session.pageBodySource) runtimeDescriptor.root.setAttribute('data-mnote-page-body-source', session.pageBodySource);
|
||||
if (session.projectionSource) runtimeDescriptor.root.setAttribute('data-mnote-projection-source', session.projectionSource);
|
||||
if (session.blockProjectionVersion) runtimeDescriptor.root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
|
||||
enhanceEditorAttachmentLinksSoon();
|
||||
};
|
||||
view.onError = (event) => {
|
||||
@@ -674,6 +815,7 @@ import {
|
||||
resourceTabs = createResourceTabRuntime({
|
||||
loadRuntime,
|
||||
createEditorViewBinding,
|
||||
ensureLocalFolderEventChannel,
|
||||
unmountEditorViewBinding,
|
||||
setStatus,
|
||||
documentSessionRegistry,
|
||||
@@ -828,7 +970,16 @@ import {
|
||||
}, { once: true });
|
||||
|
||||
if (paneRuntimes.length) {
|
||||
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).catch((error) => {
|
||||
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).then(() => {
|
||||
const restoreInput = restoreResourceTabInputFromUrl();
|
||||
if (restoreInput) {
|
||||
return openResourceInActiveTab(restoreInput).catch((error) => {
|
||||
console.warn('mnote resource tab URL 恢复失败', error);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}).catch((error) => {
|
||||
console.error('mnote multi-pane editor mount failed', error);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user