feat: 收口 Rust Web 3000 主链
This commit is contained in:
@@ -50,13 +50,6 @@ pub struct DocumentShellQuery {
|
||||
pub secondary_root_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DocumentPageCompatQuery {
|
||||
pub document_id: String,
|
||||
pub workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn document_page_shell(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -434,30 +427,38 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
inputs.forEach((input) => {
|
||||
input.setAttribute('data-title-controller', CONTRACT);
|
||||
const endpoint = input.getAttribute('data-title-endpoint') || '/api/documents/title';
|
||||
const paneRole = (input.getAttribute('data-pane-role') || 'primary').trim();
|
||||
const rawDocumentId = (input.getAttribute('data-document-id') || '').trim();
|
||||
const documentId = rawDocumentId || (
|
||||
paneRole === 'primary'
|
||||
? (document.body?.dataset.documentId || '').trim()
|
||||
: ''
|
||||
);
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const paneQueryParams = paneRole === 'secondary'
|
||||
? { sourceKindParam: 'secondarySourceKind', rootUriParam: 'secondaryRootUri' }
|
||||
: { sourceKindParam: 'sourceKind', rootUriParam: 'rootUri' };
|
||||
const sourceKindParam = paneQueryParams.sourceKindParam;
|
||||
const rootUriParam = paneQueryParams.rootUriParam;
|
||||
const workspaceId = (input.getAttribute('data-workspace-id') || query.get('workspaceId') || '').trim();
|
||||
const sourceKind = (query.get(sourceKindParam) || '').trim();
|
||||
const rootUri = (query.get(rootUriParam) || '').trim();
|
||||
let lastSavedTitle = input.value.trim() || '无标题';
|
||||
const resolveTitleTarget = (targetInput) => {
|
||||
const paneRole = (targetInput.getAttribute('data-pane-role') || 'primary').trim();
|
||||
const rawDocumentId = (targetInput.getAttribute('data-document-id') || '').trim();
|
||||
const documentId = rawDocumentId || (
|
||||
paneRole === 'primary'
|
||||
? (document.body?.dataset.documentId || '').trim()
|
||||
: ''
|
||||
);
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const paneQueryParams = paneRole === 'secondary'
|
||||
? { sourceKindParam: 'secondarySourceKind', rootUriParam: 'secondaryRootUri' }
|
||||
: { sourceKindParam: 'sourceKind', rootUriParam: 'rootUri' };
|
||||
return {
|
||||
documentId,
|
||||
workspaceId: (targetInput.getAttribute('data-workspace-id') || query.get('workspaceId') || '').trim(),
|
||||
sourceKind: (query.get(paneQueryParams.sourceKindParam) || '').trim(),
|
||||
rootUri: (query.get(paneQueryParams.rootUriParam) || '').trim(),
|
||||
};
|
||||
};
|
||||
const readLastSavedTitle = () => input.getAttribute('data-title-last-saved') || '无标题';
|
||||
const writeLastSavedTitle = (title) => {
|
||||
input.setAttribute('data-title-last-saved', title || '无标题');
|
||||
};
|
||||
writeLastSavedTitle(input.value.trim() || '无标题');
|
||||
let saving = false;
|
||||
|
||||
const saveTitle = async () => {
|
||||
const title = input.value.trim() || '无标题';
|
||||
const currentTarget = resolveTitleTarget(input);
|
||||
autosize(input);
|
||||
if (!documentId || saving || title === lastSavedTitle) {
|
||||
updateVisibleTitle(input, title, documentId);
|
||||
if (!currentTarget.documentId || saving || title === readLastSavedTitle()) {
|
||||
updateVisibleTitle(input, title, currentTarget.documentId);
|
||||
setStatus(input, 'saved');
|
||||
return;
|
||||
}
|
||||
@@ -468,10 +469,10 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId,
|
||||
workspaceId: workspaceId || null,
|
||||
sourceKind: sourceKind || undefined,
|
||||
rootUri: rootUri || undefined,
|
||||
documentId: currentTarget.documentId,
|
||||
workspaceId: currentTarget.workspaceId || null,
|
||||
sourceKind: currentTarget.sourceKind || undefined,
|
||||
rootUri: currentTarget.rootUri || undefined,
|
||||
title,
|
||||
commandName: 'page.head.updateTitle',
|
||||
}),
|
||||
@@ -480,11 +481,11 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload?.error?.message || payload?.message || `title_failed_${response.status}`);
|
||||
}
|
||||
lastSavedTitle = title;
|
||||
updateVisibleTitle(input, title, documentId);
|
||||
writeLastSavedTitle(title);
|
||||
updateVisibleTitle(input, title, currentTarget.documentId);
|
||||
setStatus(input, 'saved');
|
||||
window.dispatchEvent(new CustomEvent('tree:title-updated', {
|
||||
detail: { documentId, workspaceId: workspaceId || null, title, payload },
|
||||
detail: { documentId: currentTarget.documentId, workspaceId: currentTarget.workspaceId || null, title, payload },
|
||||
}));
|
||||
} catch (error) {
|
||||
setStatus(input, 'error', error instanceof Error ? error.message : String(error));
|
||||
@@ -495,7 +496,7 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
autosize(input);
|
||||
setStatus(input, input.value.trim() === lastSavedTitle ? 'saved' : 'dirty');
|
||||
setStatus(input, (input.value.trim() || '无标题') === readLastSavedTitle() ? 'saved' : 'dirty');
|
||||
});
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
@@ -505,7 +506,7 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
|
||||
});
|
||||
input.addEventListener('blur', () => { void saveTitle(); });
|
||||
autosize(input);
|
||||
updateVisibleTitle(input, lastSavedTitle, documentId);
|
||||
updateVisibleTitle(input, readLastSavedTitle(), resolveTitleTarget(input).documentId);
|
||||
setStatus(input, 'saved');
|
||||
});
|
||||
})();
|
||||
@@ -681,6 +682,12 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
throw new Error('island runtime 导出不完整');
|
||||
}
|
||||
await runtime.default(wasmUrl);
|
||||
if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
|
||||
window.__MNOTE_MINDMAP_RUST_SHELL__ = {
|
||||
mount: runtime.mount_mindmap_shell,
|
||||
unmount: runtime.unmount_mindmap_shell,
|
||||
};
|
||||
}
|
||||
return runtime;
|
||||
})();
|
||||
return window.__mnoteLeptosTiptapRuntimePromise;
|
||||
@@ -1560,6 +1567,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
node.value = title;
|
||||
node.setAttribute('data-document-id', documentId);
|
||||
node.setAttribute('data-workspace-id', workspaceId);
|
||||
node.setAttribute('data-title-last-saved', title);
|
||||
node.setAttribute('data-title-save-status', 'saved');
|
||||
node.style.height = 'auto';
|
||||
node.style.height = `${Math.max(48, node.scrollHeight)}px`;
|
||||
@@ -2015,49 +2023,6 @@ pub async fn leptos_tiptap_asset(Path(asset_path): Path<String>) -> Result<Respo
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn documents_page_compat(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<DocumentPageCompatQuery>,
|
||||
) -> Result<Response, WebError> {
|
||||
let document_id = query.document_id.trim().to_string();
|
||||
if document_id.is_empty() {
|
||||
return Err(
|
||||
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
|
||||
.with_context(&context),
|
||||
);
|
||||
}
|
||||
let aggregate = build_page_aggregate_snapshot(
|
||||
&state,
|
||||
&context,
|
||||
&document_id,
|
||||
query.workspace_id.as_deref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let projection_owner = aggregate.source_label();
|
||||
let mut response = (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"owner": "mnote-web",
|
||||
"schema": "mnote.documents_page_compat.v1",
|
||||
"page": aggregate,
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
stamp_shell_headers(response.headers_mut(), "documents-page-compat");
|
||||
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-page-aggregate-owner") {
|
||||
if let Ok(value) = HeaderValue::from_str(projection_owner) {
|
||||
response.headers_mut().insert(name, value);
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn page_aggregate(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
@@ -2488,6 +2453,8 @@ mod tests {
|
||||
assert!(html.contains("data-page-title-input=\"true\""));
|
||||
assert!(html.contains("data-title-endpoint=\"/api/documents/title\""));
|
||||
assert!(html.contains("mnote.document_title_controller.v1"));
|
||||
assert!(html.contains("const currentTarget = resolveTitleTarget(input);"));
|
||||
assert!(html.contains("documentId: currentTarget.documentId"));
|
||||
assert!(html.contains(
|
||||
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
|
||||
));
|
||||
@@ -2642,6 +2609,9 @@ mod tests {
|
||||
assert!(html.contains("Child Page"));
|
||||
assert!(html.contains("asset.png"));
|
||||
assert!(html.contains("data-row-kind=\"markdown\""));
|
||||
assert!(html.contains("data-page-openable=\"false\""));
|
||||
assert!(html.contains("fileAction === 'open' && rowKind === 'folder'"));
|
||||
assert!(html.contains("btn.getAttribute('data-page-openable') === 'false'"));
|
||||
assert!(html.contains("data-mnote-action=\"open-local-folder\""));
|
||||
assert!(html.contains("refreshSessionFromExternalFileChange"));
|
||||
assert!(html.contains("/api/local-folder/events"));
|
||||
|
||||
Reference in New Issue
Block a user