050901 非全量刷新
This commit is contained in:
@@ -76,40 +76,40 @@ pub async fn document_page_shell(
|
||||
.await?;
|
||||
let title = aggregate.head.title.as_str();
|
||||
let workspace_id = aggregate.identity.workspace_id.clone();
|
||||
let requested_secondary_document_id = normalize_optional_owned(query.secondary_document_id.as_deref());
|
||||
let requested_secondary_document_id =
|
||||
normalize_optional_owned(query.secondary_document_id.as_deref());
|
||||
let secondary_source_kind = normalize_source_kind(
|
||||
query.secondary_source_kind
|
||||
query
|
||||
.secondary_source_kind
|
||||
.as_deref()
|
||||
.or(primary_source_kind),
|
||||
);
|
||||
let secondary_root_uri = normalize_optional_query_value(
|
||||
query.secondary_root_uri
|
||||
.as_deref()
|
||||
.or(primary_root_uri),
|
||||
);
|
||||
let secondary_root_uri =
|
||||
normalize_optional_query_value(query.secondary_root_uri.as_deref().or(primary_root_uri));
|
||||
let mut secondary_requested = false;
|
||||
let mut secondary_invalid = false;
|
||||
let secondary_aggregate = if let Some(secondary_document_id) = requested_secondary_document_id.as_deref() {
|
||||
secondary_requested = true;
|
||||
match build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
secondary_document_id,
|
||||
query.workspace_id.as_deref(),
|
||||
secondary_source_kind,
|
||||
secondary_root_uri,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(aggregate) => Some(aggregate),
|
||||
Err(_) => {
|
||||
secondary_invalid = true;
|
||||
None
|
||||
let secondary_aggregate =
|
||||
if let Some(secondary_document_id) = requested_secondary_document_id.as_deref() {
|
||||
secondary_requested = true;
|
||||
match build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
secondary_document_id,
|
||||
query.workspace_id.as_deref(),
|
||||
secondary_source_kind,
|
||||
secondary_root_uri,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(aggregate) => Some(aggregate),
|
||||
Err(_) => {
|
||||
secondary_invalid = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let default_workspace_name = format!("{} 的空间", state.config().dev_user_name);
|
||||
let mut workspace_projection = load_workspace_shell_projection(
|
||||
state.config(),
|
||||
@@ -153,18 +153,14 @@ pub async fn document_page_shell(
|
||||
let page_options_json = serde_json::to_string(&aggregate.layout.page_options)
|
||||
.unwrap_or_else(|_| "null".to_string());
|
||||
let snapshot_json = serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
|
||||
let bootstrap_json = build_editor_bootstrap_json(
|
||||
&aggregate,
|
||||
&context,
|
||||
primary_source_kind,
|
||||
primary_root_uri,
|
||||
);
|
||||
let secondary_page_subtree_json = secondary_aggregate
|
||||
.as_ref()
|
||||
.map(|aggregate| serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string()));
|
||||
let secondary_page_options_json = secondary_aggregate
|
||||
.as_ref()
|
||||
.map(|aggregate| serde_json::to_string(&aggregate.layout.page_options).unwrap_or_else(|_| "null".to_string()));
|
||||
let bootstrap_json =
|
||||
build_editor_bootstrap_json(&aggregate, &context, primary_source_kind, primary_root_uri);
|
||||
let secondary_page_subtree_json = secondary_aggregate.as_ref().map(|aggregate| {
|
||||
serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string())
|
||||
});
|
||||
let secondary_page_options_json = secondary_aggregate.as_ref().map(|aggregate| {
|
||||
serde_json::to_string(&aggregate.layout.page_options).unwrap_or_else(|_| "null".to_string())
|
||||
});
|
||||
let secondary_snapshot_json = secondary_aggregate
|
||||
.as_ref()
|
||||
.map(|aggregate| serde_json::to_string(aggregate).unwrap_or_else(|_| "null".to_string()));
|
||||
@@ -565,6 +561,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const replaceUrlState = (url) => {
|
||||
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
||||
};
|
||||
const pushUrlState = (url) => {
|
||||
window.history.pushState({}, '', url.pathname + url.search + url.hash);
|
||||
};
|
||||
const clearSecondaryParams = () => {
|
||||
const url = currentUrl();
|
||||
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
|
||||
@@ -584,6 +583,15 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam);
|
||||
if (primaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, primaryRootUri);
|
||||
else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam);
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openSecondaryDocument === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.openSecondaryDocument({
|
||||
documentId,
|
||||
sourceKind: primarySourceKind || null,
|
||||
rootUri: primaryRootUri || null,
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.location.assign(url.pathname + url.search + url.hash);
|
||||
});
|
||||
|
||||
@@ -592,6 +600,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
event.preventDefault();
|
||||
const url = currentUrl();
|
||||
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.closeSecondaryDocument === 'function') {
|
||||
window.__mnoteDocumentPaneRuntime.closeSecondaryDocument({ url });
|
||||
return;
|
||||
}
|
||||
window.location.assign(url.pathname + url.search + url.hash);
|
||||
});
|
||||
});
|
||||
@@ -968,9 +980,29 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
if (bootstrap.rootUri) url.searchParams.set('rootUri', bootstrap.rootUri);
|
||||
return url;
|
||||
};
|
||||
const pageAggregateUrlFromDescriptor = (descriptor) => {
|
||||
const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
||||
url.searchParams.set('sourceKind', descriptor.sourceKind || 'convex_workspace');
|
||||
if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
||||
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
||||
return url;
|
||||
};
|
||||
const buildBootstrapFromAggregate = (aggregate, descriptor, paneRole) => ({
|
||||
schema: 'mnote.editor_bootstrap.v1',
|
||||
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',
|
||||
rootUri: descriptor.rootUri || '',
|
||||
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
|
||||
saveEndpoint: '/api/documents/save',
|
||||
titleEndpoint: '/api/documents/title',
|
||||
editorHostKind: 'leptos_tiptap_island',
|
||||
});
|
||||
|
||||
const documentSessionRegistry = new Map();
|
||||
const localFolderEventRegistry = new Map();
|
||||
const paneViewRegistry = new Map();
|
||||
let nextViewId = 1;
|
||||
const externalConflictMessage = '本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突';
|
||||
const SESSION_RELEASE_DELAY_MS = 1200;
|
||||
@@ -1452,6 +1484,174 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return session;
|
||||
};
|
||||
|
||||
const updatePaneChrome = (runtimeDescriptor) => {
|
||||
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
||||
if (!(pane instanceof HTMLElement)) return;
|
||||
const aggregate = runtimeDescriptor.aggregate || {};
|
||||
const bootstrap = runtimeDescriptor.bootstrap || {};
|
||||
const title = aggregate?.head?.title || '无标题';
|
||||
const documentId = bootstrap.documentId || aggregate?.identity?.documentId || '';
|
||||
const workspaceId = bootstrap.workspaceId || aggregate?.identity?.workspaceId || '';
|
||||
pane.setAttribute('data-pane-document-id', documentId);
|
||||
pane.setAttribute('data-pane-workspace-id', workspaceId);
|
||||
pane.setAttribute('data-pane-visible', 'true');
|
||||
pane.hidden = false;
|
||||
const shell = pane.querySelector('.document-shell');
|
||||
if (shell instanceof HTMLElement) {
|
||||
shell.setAttribute('data-document-id', documentId);
|
||||
shell.setAttribute('data-workspace-id', workspaceId);
|
||||
const options = aggregate?.layout?.pageOptions || {};
|
||||
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
|
||||
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
|
||||
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
|
||||
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
|
||||
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
||||
}
|
||||
pane.querySelectorAll('[data-page-title-input="true"]').forEach((node) => {
|
||||
if (!(node instanceof HTMLTextAreaElement)) return;
|
||||
node.value = title;
|
||||
node.setAttribute('data-document-id', documentId);
|
||||
node.setAttribute('data-workspace-id', workspaceId);
|
||||
node.setAttribute('data-title-save-status', 'saved');
|
||||
node.style.height = 'auto';
|
||||
node.style.height = `${Math.max(48, node.scrollHeight)}px`;
|
||||
});
|
||||
pane.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
|
||||
if (node instanceof HTMLElement) node.textContent = title;
|
||||
});
|
||||
if (runtimeDescriptor.paneRole === 'primary') {
|
||||
document.body.dataset.documentId = documentId;
|
||||
document.title = title;
|
||||
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
|
||||
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
|
||||
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('data-active', 'false');
|
||||
row.setAttribute('data-selected', 'false');
|
||||
}
|
||||
});
|
||||
const escapedId = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape(documentId) : String(documentId).replace(/["\\]/g, '\\$&');
|
||||
document.querySelectorAll(`.tree-row[data-node-id="${escapedId}"], .tree-row[data-document-id="${escapedId}"], .tree-row[data-doc-id="${escapedId}"]`).forEach((row) => {
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
if (row.getAttribute('data-shell-mode') === 'filetree') {
|
||||
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === `index:${documentId}`));
|
||||
} else {
|
||||
row.setAttribute('data-active', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPageAggregateForPane = async (descriptor) => {
|
||||
const response = await fetch(pageAggregateUrlFromDescriptor(descriptor).toString(), {
|
||||
cache: 'no-store',
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) throw new Error(`page_aggregate_failed_${response.status}`);
|
||||
const payload = await response.json();
|
||||
if (!payload?.result) throw new Error('page_aggregate_missing_result');
|
||||
return payload.result;
|
||||
};
|
||||
|
||||
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
||||
const runtime = await loadRuntime();
|
||||
const root = 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 previousView = paneViewRegistry.get(paneRole);
|
||||
if (previousView) {
|
||||
unmountEditorViewBinding(previousView);
|
||||
paneViewRegistry.delete(paneRole);
|
||||
}
|
||||
const aggregate = options.aggregate || await fetchPageAggregateForPane(descriptor);
|
||||
const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
|
||||
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap };
|
||||
updatePaneChrome(runtimeDescriptor);
|
||||
if (paneRole === 'secondary' && workspace instanceof HTMLElement) {
|
||||
workspace.setAttribute('data-has-secondary-pane', 'true');
|
||||
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
|
||||
if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
|
||||
applyStoredSecondaryWidth();
|
||||
}
|
||||
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
||||
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
||||
const mountOptions = {
|
||||
documentId: session.documentId,
|
||||
workspaceId: session.workspaceId,
|
||||
title: session.title,
|
||||
content: session.currentTiptapDocument,
|
||||
revision: session.revision,
|
||||
conflictDetectionKey: session.conflictDetectionKey,
|
||||
readOnly: session.readOnly,
|
||||
editable: !session.readOnly,
|
||||
pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
|
||||
};
|
||||
setStatus(runtimeDescriptor, 'loading-assets');
|
||||
clearEmbeddedLocalDraft(runtimeDescriptor);
|
||||
try {
|
||||
const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
|
||||
view.mountId = mountId;
|
||||
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
|
||||
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
||||
setStatus(runtimeDescriptor, 'mounting-editor');
|
||||
paneViewRegistry.set(paneRole, view);
|
||||
return view;
|
||||
} catch (error) {
|
||||
unmountEditorViewBinding(view);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const descriptorFromCurrentUrl = (paneRole, documentId, explicit = {}) => {
|
||||
const url = currentUrl();
|
||||
if (paneRole === 'secondary') {
|
||||
return {
|
||||
documentId,
|
||||
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
||||
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'convex_workspace',
|
||||
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',
|
||||
rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
|
||||
};
|
||||
};
|
||||
|
||||
const updatePrimaryUrl = (descriptor, urlFromCaller) => {
|
||||
const url = urlFromCaller instanceof URL
|
||||
? urlFromCaller
|
||||
: new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
||||
if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
||||
if (descriptor.sourceKind && descriptor.sourceKind !== 'convex_workspace') url.searchParams.set('sourceKind', descriptor.sourceKind);
|
||||
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
||||
pushUrlState(url);
|
||||
};
|
||||
|
||||
const closeSecondaryPane = (url) => {
|
||||
const view = paneViewRegistry.get('secondary');
|
||||
if (view) {
|
||||
unmountEditorViewBinding(view);
|
||||
paneViewRegistry.delete('secondary');
|
||||
}
|
||||
const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
|
||||
if (pane instanceof HTMLElement) {
|
||||
pane.hidden = true;
|
||||
pane.setAttribute('data-pane-visible', 'false');
|
||||
}
|
||||
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="secondary"]`);
|
||||
if (root instanceof HTMLElement) root.replaceChildren();
|
||||
if (workspace instanceof HTMLElement) {
|
||||
workspace.setAttribute('data-has-secondary-pane', 'false');
|
||||
workspace.style.removeProperty('grid-template-columns');
|
||||
}
|
||||
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
|
||||
if (resizerNode instanceof HTMLElement) resizerNode.hidden = true;
|
||||
replaceUrlState(url);
|
||||
};
|
||||
|
||||
const handleSessionChange = (session, view, event) => {
|
||||
const payload = normalizeEnvelopePayload(event);
|
||||
if (!payload) return;
|
||||
@@ -1629,12 +1829,36 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
|
||||
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
||||
setStatus(runtimeDescriptor, 'mounting-editor');
|
||||
paneViewRegistry.set(runtimeDescriptor.paneRole, view);
|
||||
} catch (error) {
|
||||
unmountEditorViewBinding(view);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
window.__mnoteDocumentPaneRuntime = {
|
||||
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
|
||||
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
||||
if (!id) return false;
|
||||
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
|
||||
await replacePaneDocument('primary', descriptor);
|
||||
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
|
||||
return true;
|
||||
},
|
||||
openSecondaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
|
||||
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
||||
if (!id) return false;
|
||||
const descriptor = descriptorFromCurrentUrl('secondary', id, { workspaceId, sourceKind, rootUri });
|
||||
await replacePaneDocument('secondary', descriptor);
|
||||
if (url instanceof URL) replaceUrlState(url);
|
||||
return true;
|
||||
},
|
||||
closeSecondaryDocument: ({ url } = {}) => {
|
||||
closeSecondaryPane(url instanceof URL ? url : currentUrl());
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener('pagehide', () => {
|
||||
Array.from(documentSessionRegistry.values()).forEach((session) => {
|
||||
sessionViews(session).forEach((view) => {
|
||||
@@ -2377,7 +2601,9 @@ mod tests {
|
||||
assert!(html.contains("localFolderEventRegistry"));
|
||||
assert!(html.contains("command: 'replaceContent'"));
|
||||
assert!(html.contains("external-change-conflict"));
|
||||
assert!(!html.contains("setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);"));
|
||||
assert!(!html.contains(
|
||||
"setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -633,6 +633,28 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
copyWorkspaceSourceParams(targetUrl);
|
||||
var url = targetUrl.pathname + targetUrl.search;
|
||||
if (mnoteNavigationInFlight === url) return;
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') {
|
||||
mnoteNavigationInFlight = url;
|
||||
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
||||
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
|
||||
window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
|
||||
documentId: nodeId,
|
||||
workspaceId: workspaceId || '',
|
||||
sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
|
||||
rootUri: targetUrl.searchParams.get('rootUri') || '',
|
||||
url: targetUrl,
|
||||
}).then(function(){
|
||||
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
|
||||
document.documentElement.removeAttribute('data-mnote-navigation-pending');
|
||||
}).catch(function(error){
|
||||
console.warn('mnote pane 内导航失败,将回退整页导航', error);
|
||||
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
|
||||
window.__mnoteTreeLiveEventSource.close();
|
||||
}
|
||||
window.location.assign(url);
|
||||
});
|
||||
return;
|
||||
}
|
||||
mnoteNavigationInFlight = url;
|
||||
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
||||
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
|
||||
|
||||
Reference in New Issue
Block a user