Complete workbench P0 resource tab cutover

This commit is contained in:
lix-2026
2026-05-20 20:13:24 +08:00
parent acf1305a7e
commit e0b0e70fb8
12 changed files with 1159 additions and 3 deletions
+48 -2
View File
@@ -3184,6 +3184,19 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return 'file';
};
const resolveResourceOpen = (input = {}) => {
const kind = normalizeResourceTabKind(input);
const badgeKind = resourceTabBadgeKind(input, kind);
const rawTarget = String(input?.openTarget || '').trim().toLowerCase();
const openTarget = rawTarget === 'new-window' || rawTarget === 'side' ? rawTarget : 'active-tab';
const editable = kind === 'markdown' || kind === 'text' || kind === 'code';
const defaultOpenMode = kind === 'office' && openTarget === 'active-tab' ? 'active-tab-iframe' : openTarget;
const viewerUrl = kind === 'office'
? String(input?.officeUrl || input?.href || '').trim()
: String(input?.href || '').trim();
return { editorKind: kind, badgeKind, defaultOpenMode, editable, viewerUrl, openTarget };
};
const touchResourceTabMru = (key) => {
const id = String(key || '').trim();
if (!id) return;
@@ -3193,6 +3206,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (resourceTabMru.length > resourceTabMruMax) resourceTabMru.length = resourceTabMruMax;
};
const setResourceTabLastActive = (key) => {
const entry = resourceTabRegistry.get(String(key || '').trim());
if (entry?.session) entry.session.lastActiveAt = Date.now();
};
const removeFromResourceTabMru = (key) => {
const index = resourceTabMru.indexOf(key);
if (index >= 0) resourceTabMru.splice(index, 1);
@@ -3239,7 +3257,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const activateMainEditorTab = (objectIdentity) => {
const nodes = resourceTabHostNodes();
const activeResource = String(objectIdentity || '').trim();
if (activeResource) touchResourceTabMru(activeResource);
if (activeResource) {
touchResourceTabMru(activeResource);
setResourceTabLastActive(activeResource);
}
if (nodes.pageTab instanceof HTMLElement) {
const activePage = !activeResource;
nodes.pageTab.classList.toggle('is-active', activePage);
@@ -3289,6 +3310,20 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
activateMainEditorTab(lastActiveResourceTabKey());
};
const markResourceTabError = (entry) => {
if (!entry) return;
entry.kind = 'error';
if (entry.tab instanceof HTMLElement) {
entry.tab.setAttribute('data-mnote-tab-kind', 'error');
entry.tab.setAttribute('data-mnote-tab-badge-kind', 'file');
entry.tab.classList.add('is-error');
}
if (entry.panel instanceof HTMLElement) {
entry.panel.setAttribute('data-resource-kind', 'error');
entry.panel.innerHTML = '<div class="mnote-resource-tab-error" data-resource-tab-error="true"><div class="mnote-resource-tab-error-inner"><h1>资源打开失败</h1><p>无法加载此资源,请检查文件路径和访问权限。</p></div></div>';
}
};
const createResourceTabDom = (input) => {
const nodes = resourceTabHostNodes();
if (!(nodes.strip instanceof HTMLElement) || !(nodes.panelRoot instanceof HTMLElement)) return null;
@@ -3356,6 +3391,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
fileVersion: conflictDetectionKey,
lastExternalConflictDetectionKey: conflictDetectionKey,
readOnly: false,
lastActiveAt: 0,
dirty: false,
saving: false,
hasExternalConflict: false,
@@ -3464,7 +3500,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return true;
} catch (error) {
console.warn('mnote resource tab 打开失败', error);
entry.panel.innerHTML = '<div class="mnote-resource-tab-text-shell" data-resource-tab-error="true">资源打开失败</div>';
markResourceTabError(entry);
return false;
}
};
@@ -3530,6 +3566,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (url instanceof URL) replaceUrlState(url);
return true;
},
resolveResourceOpen: (input) => resolveResourceOpen(input),
openResourceInActiveTab: openResourceInActiveTab,
closeSecondaryDocument: ({ url } = {}) => {
closeSecondaryPane(url instanceof URL ? url : currentUrl());
@@ -4246,6 +4283,15 @@ mod tests {
"const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);"
));
assert!(!html.contains("mnote-web-document-shell"));
// Resource open resolver contract
assert!(html.contains("normalizeResourceTabKind"));
assert!(html.contains("data-resource-tab-error"));
assert!(html.contains("resourceTabCloseGuardReason"));
assert!(html.contains("closeResourceTab"));
assert!(html.contains("resourceTabRegistry.delete(key)"));
assert!(html.contains("activateMainEditorTab(lastActiveResourceTabKey())"));
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
}
#[tokio::test]
@@ -2043,7 +2043,13 @@ const SIDEBAR_TREE_JS: &str = r##"
function buildOnlyOfficeOpenUrl(input) {
var target = new URL('/onlyoffice', window.location.origin);
target.searchParams.set('fileUrl', input.fileUrl || '');
var fileUrl = String(input.fileUrl || '').trim();
if (fileUrl) {
try {
fileUrl = new URL(fileUrl, window.location.origin).toString();
} catch (_error) {}
}
target.searchParams.set('fileUrl', fileUrl);
target.searchParams.set('fileName', input.fileName || '');
target.searchParams.set('fileType', input.fileType || 'docx');
if (input.assetId) target.searchParams.set('assetId', input.assetId);
+40
View File
@@ -2522,6 +2522,46 @@ body {
padding: 34px 48px;
}
.mnote-resource-tab-error {
display: flex;
place-items: center;
justify-content: center;
width: 100%;
min-height: calc(100vh - 80px);
background: #FFF;
color: #73726e;
font-size: 14px;
line-height: 1.6;
padding: 48px;
box-sizing: border-box;
}
.mnote-resource-tab-error[data-resource-tab-error="true"] {
display: flex;
}
.mnote-resource-tab-error-inner {
max-width: 420px;
text-align: center;
border: 1px solid var(--color-basic-200, #E3E3E0);
border-radius: 6px;
padding: 24px 28px;
background: var(--color-basic-50, #F7F7F5);
}
.mnote-resource-tab-error-inner h1 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
color: var(--color-basic-900, #37352F);
}
.mnote-resource-tab-error-inner p {
margin: 0;
font-size: 13px;
color: var(--color-basic-600, #9B9A97);
}
.mnote-resource-tab-editor-root {
min-height: calc(100vh - 112px);
}
@@ -151,6 +151,18 @@ pub enum FileTreeOpenTarget {
Asset {
document_id: String,
asset_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
resource_kind: Option<String>,
},
LocalFile {
root_uri: String,
path: String,
#[serde(skip_serializing_if = "Option::is_none")]
resource_kind: Option<String>,
},
Directory {
root_uri: String,
path: String,
},
}
@@ -455,10 +467,27 @@ fn resolve_open_target(
document_id: row.document_id.clone()?,
asset_id: row.asset_id.clone()?,
}),
"local-file" | "local_file" => Some(FileTreeOpenTarget::LocalFile {
root_uri: String::new(),
path: row.asset_id.clone().unwrap_or_default(),
resource_kind: None,
}),
"directory" => Some(FileTreeOpenTarget::Directory {
root_uri: String::new(),
path: row.asset_id.clone().unwrap_or_default(),
}),
"asset" => Some(FileTreeOpenTarget::Asset {
document_id: row.document_id.clone()?,
asset_id: row.asset_id.clone()?,
resource_kind: None,
}),
"office" | "image" | "pdf" | "code" | "text" | "mindmap" | "table" => {
Some(FileTreeOpenTarget::Asset {
document_id: row.document_id.clone()?,
asset_id: row.asset_id.clone()?,
resource_kind: Some(row.row_kind.clone()),
})
}
_ => None,
}
}
@@ -647,6 +676,7 @@ mod tests {
target: FileTreeOpenTarget::Asset {
document_id: "root".into(),
asset_id: "image".into(),
resource_kind: None,
},
},
)));
@@ -786,4 +816,53 @@ mod tests {
assert_eq!(escaped.state.drag_effect, None);
assert_eq!(escaped.state.drop_target_row_id, None);
}
#[test]
fn filetree_open_target_serializes_resource_identity_variants() {
let asset = serde_json::to_value(FileTreeOpenTarget::Asset {
document_id: "doc:page".into(),
asset_id: "local-file:Page/report.docx".into(),
resource_kind: Some("office".into()),
})
.expect("serialize asset target");
assert_eq!(
asset,
serde_json::json!({
"kind": "asset",
"documentId": "doc:page",
"assetId": "local-file:Page/report.docx",
"resourceKind": "office"
})
);
let local_file = serde_json::to_value(FileTreeOpenTarget::LocalFile {
root_uri: "file:///tmp/mnote".into(),
path: "Page/资源.ext".into(),
resource_kind: Some("local_file".into()),
})
.expect("serialize local file target");
assert_eq!(
local_file,
serde_json::json!({
"kind": "localFile",
"rootUri": "file:///tmp/mnote",
"path": "Page/资源.ext",
"resourceKind": "local_file"
})
);
let directory = serde_json::to_value(FileTreeOpenTarget::Directory {
root_uri: "file:///tmp/mnote".into(),
path: "Page/assets".into(),
})
.expect("serialize directory target");
assert_eq!(
directory,
serde_json::json!({
"kind": "directory",
"rootUri": "file:///tmp/mnote",
"path": "Page/assets"
})
);
}
}