fix local office resource editing

- add local-folder OnlyOffice sign/callback writeback and edit-tab handling

- align main resource tabs, attachment edit menu, slash isolation, and filetree context behavior

- record Sidex/Hermes gap reviews and Reasonix task checklists
This commit is contained in:
lix-2026
2026-05-21 05:40:06 +08:00
parent a29d9868f6
commit eba1010191
50 changed files with 4309 additions and 283 deletions
+99 -8
View File
@@ -465,6 +465,8 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
document.title = title;
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
const pageTabTitle = document.querySelector('[data-mnote-main-tab="page"] .mnote-main-tab-title');
if (pageTabTitle instanceof HTMLElement) pageTabTitle.textContent = title;
}
if (documentId) {
const escapedId = cssEscape(documentId);
@@ -2616,6 +2618,16 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
document.title = title;
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
const pageTab = document.querySelector('[data-mnote-main-tab="page"]');
if (pageTab instanceof HTMLElement) {
pageTab.setAttribute('data-document-id', documentId);
pageTab.setAttribute('data-workspace-id', workspaceId);
const pageTabTitle = pageTab.querySelector('.mnote-main-tab-title');
if (pageTabTitle instanceof HTMLElement) pageTabTitle.textContent = title;
}
window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
detail: { documentId, workspaceId, title }
}));
document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
@@ -2986,12 +2998,20 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE
? anchorNode
: anchorNode?.parentElement || null;
const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => node instanceof HTMLElement);
const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => (
node instanceof HTMLElement
&& node.offsetParent !== null
&& node.getAttribute('data-mnote-side-target-unsupported') !== 'true'
));
if (anchorElement instanceof Element) {
const activeRoot = roots.find((root) => root.contains(anchorElement));
if (activeRoot) return activeRoot;
}
return roots.find((root) => root.offsetParent !== null) || roots[0] || null;
const focused = document.activeElement instanceof Element
? roots.find((root) => root.contains(document.activeElement))
: null;
if (focused) return focused;
return roots.find((root) => root.querySelector('.ProseMirror:focus-within')) || roots[0] || null;
};
const slashMenuAnchorFromSelection = (root) => {
@@ -3042,12 +3062,42 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return fallback();
};
const positionSlashMenuForRoot = (root) => {
if (!(root instanceof HTMLElement)) root = activeEditorRootForSlashMenu();
if (!(root instanceof HTMLElement)) return;
const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]')
|| document.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
const setSlashMenuInactive = (menu, inactive) => {
if (!(menu instanceof HTMLElement)) return;
if (inactive) {
if (menu.getAttribute('data-mnote-slash-inactive') !== 'true') {
menu.setAttribute('data-mnote-slash-inactive', 'true');
}
if (menu.style.display !== 'none') menu.style.display = 'none';
return;
}
if (menu.getAttribute('data-mnote-slash-inactive') === 'true') {
menu.removeAttribute('data-mnote-slash-inactive');
}
if (menu.style.display === 'none') menu.style.display = '';
};
const hideSlashMenusOutsideRoot = (activeRoot) => {
document.querySelectorAll(`${ROOT_SELECTOR} [data-testid="mnote-leptos-tiptap-slash-menu"]`).forEach((menu) => {
const root = menu.closest(ROOT_SELECTOR);
if (root !== activeRoot) setSlashMenuInactive(menu, true);
});
};
const positionSlashMenuForRoot = (root) => {
const activeRoot = activeEditorRootForSlashMenu();
if (!(root instanceof HTMLElement)) root = activeRoot;
if (!(root instanceof HTMLElement)) return;
if (activeRoot instanceof HTMLElement && root !== activeRoot) {
const inactiveMenu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
setSlashMenuInactive(inactiveMenu, true);
hideSlashMenusOutsideRoot(activeRoot);
return;
}
hideSlashMenusOutsideRoot(root);
const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
if (!(menu instanceof HTMLElement)) return;
setSlashMenuInactive(menu, false);
const anchor = slashMenuAnchorFromSelection(root);
const gap = 8;
const menuWidth = Math.min(316, Math.max(160, window.innerWidth - 16));
@@ -3611,6 +3661,18 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
}
};
const refreshExistingOfficeResourceTab = (entry, input) => {
if (!entry || entry.kind !== 'office') return false;
const nextHref = String(input.officeUrl || input.href || '').trim();
if (!nextHref) return false;
const frame = entry.panel?.querySelector?.('iframe.mnote-resource-tab-frame');
const currentHref = frame instanceof HTMLIFrameElement
? String(frame.getAttribute('src') || frame.src || '').trim()
: '';
if (currentHref !== nextHref) openPassiveResourceTab(entry, input);
return true;
};
const openMindmapResourceTab = async (entry, input) => {
const documentId = String(input.documentId || currentDocumentId() || '').trim();
const mindmapId = String(input.mindmapId || input.assetId || '').trim();
@@ -3668,6 +3730,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const title = String(input.title || input.fileName || input.assetId || '资源').trim() || '资源';
if (root instanceof HTMLElement) {
root.replaceChildren();
document.querySelectorAll('[data-document-pane="true"][data-pane-role="secondary"] [data-testid="mnote-leptos-tiptap-slash-menu"]').forEach((node) => {
if (node instanceof HTMLElement) node.remove();
});
root.setAttribute('data-editor-host-kind', 'unsupported_resource_side_target');
root.setAttribute('data-mnote-side-target-unsupported', 'true');
root.setAttribute('data-mnote-side-target-asset-id', String(input.assetId || ''));
@@ -3691,6 +3756,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (!objectIdentity) return false;
const existing = resourceTabRegistry.get(objectIdentity);
if (existing) {
refreshExistingOfficeResourceTab(existing, input);
activateMainEditorTab(objectIdentity);
return true;
}
@@ -3754,6 +3820,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
await replacePaneDocument('primary', descriptor);
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
activateMainEditorTab('');
return true;
},
openPrimaryMindmap: async ({ documentId, mindmapId, workspaceId, url } = {}) => {
@@ -3778,6 +3845,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
},
resolveResourceOpen: (input) => resolveResourceOpen(input),
openResourceInActiveTab: openResourceInActiveTab,
activatePageTab: () => {
bindMainEditorPageTab();
activateMainEditorTab('');
return true;
},
openResourceAsSideTarget: async (input = {}) => {
const resolved = resolveResourceOpen({ ...input, openTarget: 'side' });
if (resolved.editorKind === 'markdown' || resolved.editorKind === 'text' || resolved.editorKind === 'code') {
@@ -4466,12 +4538,25 @@ mod tests {
assert!(html.contains("data-mnote-main-tab-strip"));
assert!(html.contains("class=\"mnote-main-tab-badge\""));
assert!(!html.contains(">description</span><span class=\"mnote-main-tab-title\""));
assert!(html.contains("[data-mnote-main-tab=\"page\"] .mnote-main-tab-title"));
assert!(html.contains("pageTab.setAttribute('data-document-id', documentId);"));
assert!(html.contains("data-mnote-tab-badge-kind"));
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
assert!(html.contains("positionSlashMenuForRoot"));
assert!(html.contains("menu.style.position = 'fixed';"));
assert!(html.contains("installGlobalSlashMenuPositioning();"));
assert!(html.contains("data-mnote-slash-positioned', 'host'"));
assert!(html.contains(
"const menu = root.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]');"
));
assert!(html.contains("const setSlashMenuInactive = (menu, inactive) =>"));
assert!(html.contains("const hideSlashMenusOutsideRoot = (activeRoot) =>"));
assert!(html.contains("data-mnote-slash-inactive"));
assert!(html.contains("if (activeRoot instanceof HTMLElement && root !== activeRoot)"));
assert!(!html.contains(
"|| document.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]')"
));
assert!(html.contains("data-mnote-side-target-unsupported') !== 'true'"));
assert!(
html.contains("runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);")
);
@@ -4509,6 +4594,12 @@ mod tests {
assert!(html.contains("resourceTabRegistry.delete(key)"));
assert!(html.contains("activateMainEditorTab(lastActiveResourceTabKey())"));
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
assert!(html.contains("const refreshExistingOfficeResourceTab = (entry, input) =>"));
assert!(html.contains("if (!entry || entry.kind !== 'office') return false;"));
assert!(
html.contains("if (currentHref !== nextHref) openPassiveResourceTab(entry, input);")
);
assert!(html.contains("refreshExistingOfficeResourceTab(existing, input);"));
}
#[tokio::test]
@@ -4769,7 +4860,7 @@ mod tests {
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("openTrigger.getAttribute('data-page-openable') === 'false'"));
assert!(html.contains("data-mnote-action=\"open-local-folder\""));
assert!(html.contains("refreshSessionFromExternalFileChange"));
assert!(html.contains("refreshSessionFromExternalChange"));