feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -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);
});
}
@@ -1,12 +1,45 @@
import {
localMarkdownDocumentIdFromRelativePath,
localMarkdownRelativePathFromDocumentId,
localizeTiptapAssetUrls,
} from './document-tiptap-conversion-runtime.js';
/**
* @typedef {Object} MNoteWorkspacePath
* @property {'mnote.workspace_path.v1'} schema
* @property {string} workspaceId
* @property {string} sourceKind
* @property {string} rootUri
* @property {string} relativePath
* @property {string} documentId
* @property {string} objectIdentity
* @property {string} assetId
* @property {string} resourceKind
*
* @typedef {Object} MNoteOpenEditorEntry
* @property {string} objectIdentity
* @property {MNoteWorkspacePath|null} workspacePath
* @property {'primary'|'secondary'} paneRole
* @property {string} editorKind
* @property {boolean} active
* @property {string} dirtyState
* @property {boolean} preview
* @property {boolean} pinned
* @property {number} lastActiveAt
*
* @typedef {Object} MNoteOpenEditorsSnapshot
* @property {'mnote.open_editors_snapshot.v1'} schema
* @property {number} generatedAt
* @property {string} activeObjectIdentity
* @property {MNoteOpenEditorEntry[]} editors
* @property {{primary: Object, secondary: Object}} groups
*/
export const createResourceTabRuntime = (dependencies = {}) => {
const {
loadRuntime,
createEditorViewBinding,
ensureLocalFolderEventChannel,
unmountEditorViewBinding,
setStatus,
documentSessionRegistry,
@@ -62,9 +95,31 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return 'file';
};
const resourceTabWidthType = (entry) => {
if (!entry) return '';
const kind = String(entry.kind || '').trim();
if (kind === 'office') {
const badgeKind = String(entry.badgeKind || resourceTabBadgeKind(entry, kind) || '').trim();
if (badgeKind === 'ppt') return 'ppt';
if (badgeKind === 'sheet') return 'excel';
return 'word';
}
if (kind === 'pdf') return 'pdf';
if (kind === 'mindmap') return 'mindmap';
if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'markdown';
return '';
};
const currentWebShellWorkspaceId = () => {
const explicit = typeof currentWorkspaceId === 'function' ? String(currentWorkspaceId() || '').trim() : '';
if (explicit) return explicit;
const fromBody = document.body?.dataset?.workspaceId || '';
if (fromBody) return String(fromBody).trim();
const shell = document.querySelector('.document-shell[data-workspace-id], [data-document-pane="true"][data-pane-role="primary"][data-pane-workspace-id]');
if (shell instanceof HTMLElement) {
const value = shell.getAttribute('data-workspace-id') || shell.getAttribute('data-pane-workspace-id') || '';
if (value) return value.trim();
}
try {
return currentUrl().searchParams.get('workspaceId') || '';
} catch (_) {
@@ -82,6 +137,80 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return '';
};
const documentIdForPane = (paneRole) => {
const role = normalizePaneRole(paneRole);
const nodes = resourceTabHostNodes(role);
const fromTab = String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim();
if (fromTab) return fromTab;
const pane = document.querySelector(`[data-document-pane="true"][data-pane-role="${role}"]`);
if (pane instanceof HTMLElement) {
const fromPane = String(pane.getAttribute('data-pane-document-id') || '').trim();
if (fromPane) return fromPane;
const root = pane.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"][data-document-id], .document-shell[data-document-id]');
if (root instanceof HTMLElement) {
const fromRoot = String(root.getAttribute('data-document-id') || '').trim();
if (fromRoot) return fromRoot;
}
}
return role === 'primary' ? currentWebShellDocumentId() : '';
};
const currentWebShellSourceKind = () => {
try {
return currentUrl().searchParams.get('sourceKind') || '';
} catch (_) {
return '';
}
};
const currentWebShellRootUri = () => {
try {
return currentUrl().searchParams.get('rootUri') || '';
} catch (_) {
return '';
}
};
const buildWorkspacePath = ({
workspacePath,
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId,
resourceKind,
} = {}) => {
const fromInput = workspacePath && typeof workspacePath === 'object' ? workspacePath : null;
if (fromInput && String(fromInput.schema || '') === 'mnote.workspace_path.v1') {
return {
schema: 'mnote.workspace_path.v1',
workspaceId: String(fromInput.workspaceId || workspaceId || '').trim(),
sourceKind: String(fromInput.sourceKind || sourceKind || '').trim(),
rootUri: String(fromInput.rootUri || rootUri || '').trim(),
relativePath: String(fromInput.relativePath || fromInput.localRelativePath || relativePath || '').trim(),
documentId: String(fromInput.documentId || documentId || '').trim(),
objectIdentity: fromInput.objectIdentity ?? String(objectIdentity || '').trim(),
assetId: String(fromInput.assetId || assetId || '').trim(),
resourceKind: String(fromInput.resourceKind || fromInput.objectKind || resourceKind || '').trim(),
};
}
const id = String(objectIdentity || documentId || assetId || relativePath || '').trim();
if (!id) return null;
return {
schema: 'mnote.workspace_path.v1',
workspaceId: String(workspaceId || '').trim(),
sourceKind: String(sourceKind || '').trim(),
rootUri: String(rootUri || '').trim(),
relativePath: String(relativePath || '').trim(),
documentId: String(documentId || '').trim(),
objectIdentity: String(objectIdentity || '').trim(),
assetId: String(assetId || '').trim(),
resourceKind: String(resourceKind || '').trim(),
};
};
const normalizeResourceTabKind = (input) => {
const kind = String(input?.kind || '').trim().toLowerCase();
const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
@@ -121,7 +250,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const setResourceTabLastActive = (key) => {
const entry = resourceTabRegistry.get(String(key || '').trim());
if (entry?.session) entry.session.lastActiveAt = Date.now();
if (!entry) return;
const timestamp = Date.now();
entry.lastActiveAt = timestamp;
if (entry.session) entry.session.lastActiveAt = timestamp;
};
const removeFromResourceTabMru = (paneRole, key) => {
@@ -140,6 +272,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const resourceTabCloseGuardReason = (session) => {
if (!session) return '';
const bufferDirtyState = String(session.bufferDirtyState || '').trim();
if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') {
return bufferDirtyState;
}
const hasUnsavedChanges = session.dirty
|| Boolean(session.saveTimer)
|| sessionHasRecentLocalInput(session)
@@ -150,7 +286,49 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return '';
};
const syncResourceTabCloseGuard = (entry) => {
const documentSessionForPane = (paneRole, documentId) => {
const id = String(documentId || '').trim();
if (!id || !documentSessionRegistry || typeof documentSessionRegistry.values !== 'function') return null;
const role = normalizePaneRole(paneRole);
return Array.from(documentSessionRegistry.values()).find((session) => (
session
&& session.sessionKind !== 'resource'
&& String(session.documentId || '').trim() === id
&& Array.from(session.views?.values?.() || []).some((view) => normalizePaneRole(view?.runtimeDescriptor?.paneRole) === role)
)) || Array.from(documentSessionRegistry.values()).find((session) => (
session
&& session.sessionKind !== 'resource'
&& String(session.documentId || '').trim() === id
)) || null;
};
const documentSessionDirtyState = (session) => {
if (!session) return '';
const bufferDirtyState = String(session.bufferDirtyState || '').trim();
if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') return bufferDirtyState;
if (session.hasExternalConflict) return 'ExternalModified';
if (session.dirty || Boolean(session.saveTimer) || sessionHasRecentLocalInput(session)) return 'Dirty';
if (session.saving) return 'Saving';
return '';
};
const resourceTabBufferStateUrl = (entry) => {
const session = entry?.session;
if (!session || session.sourceKind !== 'local_folder') return null;
const relativePath = String(session.resourcePath || entry?.workspacePath?.relativePath || '').trim();
const rootUri = String(session.rootUri || entry?.workspacePath?.rootUri || '').trim();
if (!relativePath || !rootUri) return null;
const url = new URL('/api/documents/buffer-state', window.location.origin);
url.searchParams.set('documentId', String(session.documentId || entry.objectIdentity || ''));
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('relativePath', relativePath);
const workspaceId = String(session.workspaceId || entry?.workspacePath?.workspaceId || '').trim();
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
return url;
};
const applyResourceTabCloseGuard = (entry) => {
if (!entry?.tab || !(entry.tab instanceof HTMLElement)) return;
const reason = resourceTabCloseGuardReason(entry.session);
if (reason) {
@@ -162,6 +340,30 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
};
const refreshResourceTabBufferState = async (entry) => {
const url = resourceTabBufferStateUrl(entry);
if (!url || !entry?.session) return null;
try {
const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || payload.ok !== true || !payload.result) return null;
const dirtyState = String(payload.result.dirtyState || '').trim();
if (dirtyState) entry.session.bufferDirtyState = dirtyState;
if (typeof payload.result.fileVersion === 'string' && payload.result.fileVersion.trim()) {
entry.session.fileVersion = payload.result.fileVersion.trim();
}
applyResourceTabCloseGuard(entry);
return payload.result;
} catch (_) {
return null;
}
};
const syncResourceTabCloseGuard = (entry) => {
applyResourceTabCloseGuard(entry);
void refreshResourceTabBufferState(entry);
};
const syncResourceSessionTabGuards = (session) => {
if (!session || session.sessionKind !== 'resource') return;
resourceTabRegistry.forEach((entry) => {
@@ -169,52 +371,126 @@ export const createResourceTabRuntime = (dependencies = {}) => {
});
};
const openEditorsSnapshotEntry = (entry, key) => ({
objectIdentity: String(entry?.objectIdentity || key || '').trim(),
title: String(entry?.title || '资源').trim() || '资源',
kind: normalizeResourceTabKind(entry),
badgeKind: entry?.tab instanceof HTMLElement
? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
: resourceTabBadgeKind(entry, entry?.kind),
active: entry?.tab instanceof HTMLElement
const openEditorsSnapshotEntry = (entry, key, generatedAt = Date.now()) => {
const active = entry?.tab instanceof HTMLElement
? entry.tab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: resourceTabCloseGuardReason(entry?.session),
assetId: String(entry?.assetId || entry?.session?.assetId || '').trim(),
path: String(entry?.path || entry?.session?.resourcePath || '').trim(),
});
: false;
const documentId = String(entry?.documentId || entry?.ownerDocumentId || entry?.session?.ownerDocumentId || '').trim();
const workspaceId = String(entry?.workspaceId || entry?.session?.workspaceId || currentWebShellWorkspaceId() || '').trim();
const sourceKind = String(entry?.sourceKind || entry?.session?.sourceKind || currentWebShellSourceKind() || '').trim();
const rootUri = String(entry?.rootUri || entry?.session?.rootUri || currentWebShellRootUri() || '').trim();
const relativePath = String(entry?.path || entry?.session?.resourcePath || '').trim();
const objectIdentity = String(entry?.objectIdentity || key || '').trim();
const assetId = String(entry?.assetId || entry?.session?.assetId || '').trim();
const kind = normalizeResourceTabKind(entry);
const dirtyState = resourceTabCloseGuardReason(entry?.session);
return {
objectIdentity,
workspacePath: buildWorkspacePath({
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId,
resourceKind: kind,
workspacePath: entry?.workspacePath,
}),
paneRole: normalizePaneRole(entry?.paneRole),
documentId,
workspaceId,
title: String(entry?.title || '资源').trim() || '资源',
kind,
editorKind: kind,
badgeKind: entry?.tab instanceof HTMLElement
? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
: resourceTabBadgeKind(entry, entry?.kind),
active,
dirtyState,
dirtyGuard: dirtyState,
assetId,
path: relativePath,
lastActiveAt: Number(entry?.session?.lastActiveAt || entry?.lastActiveAt || 0) || (active ? generatedAt : 0),
preview: false,
pinned: false,
};
};
const buildOpenEditorsSnapshot = () => {
const generatedAt = Date.now();
const pageEntries = ['primary', 'secondary'].map((paneRole) => {
const nodes = resourceTabHostNodes(paneRole);
const documentId = documentIdForPane(paneRole);
const workspaceId = String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim();
const sourceKind = currentWebShellSourceKind();
const rootUri = currentWebShellRootUri();
const relativePath = sourceKind === 'local_folder' ? localMarkdownRelativePathFromDocumentId(documentId) : '';
const objectIdentity = `page:${paneRole}`;
const active = nodes.pageTab instanceof HTMLElement
? nodes.pageTab.getAttribute('aria-selected') === 'true'
: false;
const pageSession = documentSessionForPane(paneRole, documentId);
const dirtyState = documentSessionDirtyState(pageSession);
const pageTitle = nodes.pageTab instanceof HTMLElement
? String(nodes.pageTab.querySelector('.mnote-main-tab-title')?.textContent || '页面').trim()
: '页面';
return {
objectIdentity: `page:${paneRole}`,
objectIdentity,
workspacePath: buildWorkspacePath({
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId: '',
resourceKind: 'page',
}),
paneRole,
documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim(),
workspaceId: String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(),
documentId,
workspaceId,
title: pageTitle || '页面',
kind: 'page',
editorKind: 'page',
badgeKind: 'code',
active: nodes.pageTab instanceof HTMLElement
? nodes.pageTab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: '',
active,
dirtyState,
dirtyGuard: dirtyState,
lastActiveAt: active ? generatedAt : 0,
preview: false,
pinned: true,
};
}).filter((entry) => entry.paneRole === 'primary' || entry.documentId || document.querySelector('[data-document-pane="true"][data-pane-role="secondary"][data-pane-visible="true"]'));
const resources = [];
resourceTabRegistry.forEach((entry, key) => {
resources.push(openEditorsSnapshotEntry(entry, key));
resources.push(openEditorsSnapshotEntry(entry, key, generatedAt));
});
const active = [...pageEntries, ...resources].find((entry) => entry.active);
const editors = [...pageEntries, ...resources];
const groupForPane = (paneRole) => {
const role = normalizePaneRole(paneRole);
const groupEditors = editors.filter((entry) => normalizePaneRole(entry.paneRole) === role);
const groupResources = resources.filter((entry) => normalizePaneRole(entry.paneRole) === role);
const groupActive = groupEditors.find((entry) => entry.active);
return {
paneRole: role,
activeObjectIdentity: groupActive?.objectIdentity || '',
editors: groupEditors,
resourceEditors: groupResources,
};
};
const groups = {
primary: groupForPane('primary'),
secondary: groupForPane('secondary'),
};
const activeObjectIdentity = groups.primary.activeObjectIdentity || groups.secondary.activeObjectIdentity || '';
return {
schema: 'mnote.open_editors_snapshot.v1',
generatedAt: Date.now(),
activeObjectIdentity: active?.objectIdentity || '',
editors: [...pageEntries, ...resources],
generatedAt,
activeObjectIdentity,
editors,
resourceEditors: resources,
groups,
};
};
@@ -295,6 +571,29 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
};
const syncActiveResourceWidthType = (activeEntry, paneRole = 'primary') => {
const role = normalizePaneRole(paneRole);
const widthType = role === 'primary' ? resourceTabWidthType(activeEntry) : '';
const targetNodes = [document.documentElement, document.body].filter((node) => node instanceof HTMLElement);
targetNodes.forEach((node) => {
if (widthType) node.setAttribute('data-mnote-active-resource-width-type', widthType);
else node.removeAttribute('data-mnote-active-resource-width-type');
});
const shell = document.querySelector(`.document-pane[data-pane-role="${role}"] .document-shell`);
if (shell instanceof HTMLElement) {
if (widthType) shell.setAttribute('data-mnote-active-resource-width-type', widthType);
else shell.removeAttribute('data-mnote-active-resource-width-type');
}
window.dispatchEvent(new CustomEvent('mnote:active-resource-tab-changed', {
detail: {
paneRole: role,
active: Boolean(activeEntry),
widthType,
objectIdentity: String(activeEntry?.objectIdentity || '')
}
}));
};
const syncActiveResourceUrlState = (activeResource, paneRole = 'primary') => {
const role = normalizePaneRole(paneRole);
if (role !== 'primary') return;
@@ -340,6 +639,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active;
syncResourceTabCloseGuard(entry);
});
syncActiveResourceWidthType(activeEntry, role);
if (activeEntry) markIntendedSlashRoot(activeEntry);
if (!activeEntry && window.__mnoteIntendedSlashRoot instanceof HTMLElement) window.__mnoteIntendedSlashRoot = null;
if (role === 'primary') syncActiveResourceFileTreeRow(activeResource);
@@ -399,10 +699,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
syncOpenEditorsSnapshot();
};
const closeResourceTab = (objectIdentity) => {
const closeResourceTab = async (objectIdentity) => {
const key = String(objectIdentity || '').trim();
const entry = resourceTabRegistry.get(key);
if (!entry) return;
await refreshResourceTabBufferState(entry);
const guardReason = resourceTabCloseGuardReason(entry.session);
if (guardReason) {
console.warn(`mnote resource tab 关闭阻止: ${entry.title} (${guardReason})`);
@@ -489,6 +790,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
paneRole,
title,
kind,
badgeKind: resourceTabBadgeKind(input, kind),
tab,
panel,
view: null,
@@ -497,6 +799,21 @@ export const createResourceTabRuntime = (dependencies = {}) => {
path: String(input.path || '').trim(),
documentId: String(input.documentId || '').trim(),
ownerDocumentId: String(input.ownerDocumentId || input.documentId || '').trim(),
workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || '').trim(),
sourceKind: String(input.sourceKind || currentWebShellSourceKind() || '').trim(),
rootUri: String(input.rootUri || currentWebShellRootUri() || '').trim(),
workspacePath: buildWorkspacePath({
workspacePath: input.workspacePath,
workspaceId: input.workspaceId || currentWebShellWorkspaceId(),
sourceKind: input.sourceKind || currentWebShellSourceKind(),
rootUri: input.rootUri || currentWebShellRootUri(),
relativePath: input.path,
documentId: input.documentId,
objectIdentity,
assetId: input.assetId,
resourceKind: kind,
}),
lastActiveAt: 0,
};
};
@@ -515,6 +832,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
entry.mindmapRuntime = null;
}
if (entry.resourceWatchEventSource) {
try {
entry.resourceWatchEventSource.close();
} catch (_) {}
entry.resourceWatchEventSource = null;
}
if (entry.panel instanceof HTMLElement) entry.panel.replaceChildren();
};
@@ -525,6 +848,80 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return url.toString();
};
const localResourceStatUrl = (rootUri, path) => {
const url = new URL('/api/local-folder/files/stat', window.location.origin);
url.searchParams.set('rootUri', rootUri || '');
url.searchParams.set('path', path || '');
return url.toString();
};
const withResourceReloadToken = (href) => {
if (!href) return href;
const url = new URL(href, window.location.origin);
url.searchParams.set('mnoteResourceReload', String(Date.now()));
return url.toString();
};
const renderPassiveResourceMissing = (entry, message) => {
if (!(entry?.panel instanceof HTMLElement)) return;
entry.panel.setAttribute('data-mnote-resource-missing', 'true');
entry.panel.innerHTML = '<div class="mnote-resource-tab-error" data-resource-tab-error="true" data-mnote-resource-missing="true"><div class="mnote-resource-tab-error-inner"><h1>资源已不可用</h1><p></p></div></div>';
const text = entry.panel.querySelector('p');
if (text instanceof HTMLElement) text.textContent = message || '本地资源文件已被删除或移动。';
};
const reloadPassiveResourceTab = (entry) => {
if (!(entry?.panel instanceof HTMLElement)) return;
entry.panel.removeAttribute('data-mnote-resource-missing');
const img = entry.panel.querySelector('img.mnote-resource-tab-image');
if (img instanceof HTMLImageElement) {
img.src = withResourceReloadToken(img.getAttribute('src') || img.src || '');
return;
}
const frame = entry.panel.querySelector('iframe.mnote-resource-tab-frame');
if (frame instanceof HTMLIFrameElement) {
frame.src = withResourceReloadToken(frame.getAttribute('src') || frame.src || '');
}
};
const installPassiveResourceWatch = (entry) => {
if (!entry || entry.session || typeof window.EventSource !== 'function') return;
const rootUri = String(entry.rootUri || '').trim();
const path = String(entry.path || '').trim();
if (!rootUri || !path) return;
if (entry.resourceWatchEventSource) {
try {
entry.resourceWatchEventSource.close();
} catch (_) {}
entry.resourceWatchEventSource = null;
}
const url = new URL('/api/local-folder/events', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('resourcePath', path);
const eventSource = new EventSource(url.toString());
entry.resourceWatchEventSource = eventSource;
eventSource.addEventListener('ready', () => {
if (entry.panel instanceof HTMLElement) entry.panel.setAttribute('data-mnote-resource-watch-ready', 'true');
});
eventSource.addEventListener('change', async () => {
try {
const response = await fetch(localResourceStatUrl(rootUri, path), {
cache: 'no-store',
headers: { accept: 'application/json' },
});
const payload = await response.json().catch(() => null);
const exists = Boolean(response.ok && payload?.ok === true && payload?.result?.exists);
if (!exists) {
renderPassiveResourceMissing(entry, '本地资源文件已被删除或移动。');
return;
}
reloadPassiveResourceTab(entry);
} catch (error) {
renderPassiveResourceMissing(entry, error instanceof Error ? error.message : String(error));
}
});
};
const createResourceSession = (entry, input, readResult) => {
const resourcePath = String(input.path || '');
const tiptapDocument = localizeTiptapAssetUrls(
@@ -577,6 +974,9 @@ export const createResourceTabRuntime = (dependencies = {}) => {
error: null,
};
documentSessionRegistry.set(session.key, session);
if (typeof ensureLocalFolderEventChannel === 'function') {
ensureLocalFolderEventChannel(session);
}
return session;
};
@@ -644,6 +1044,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
img.src = href;
img.alt = entry.title;
}
installPassiveResourceWatch(entry);
return;
}
entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
@@ -652,6 +1053,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
frame.title = entry.title;
frame.src = href;
}
installPassiveResourceWatch(entry);
};
const refreshExistingOfficeResourceTab = (entry, input) => {
@@ -5,6 +5,7 @@ import {
hydrateMindmapAttrsFromDom,
legacyBlocksFromEditorDocument,
pageBodyTiptapDocument,
pageBodyTiptapDocumentSource,
revisionFromConflictKey,
textToTiptapDocument,
toTiptapDocument,
@@ -102,7 +103,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const normalizeSessionSourceKind = (bootstrap) => {
const value = typeof bootstrap?.sourceKind === 'string' ? bootstrap.sourceKind.trim() : '';
return value || 'convex_workspace';
return value || 'local_folder';
};
const buildDocumentSessionKey = (bootstrap) => {
@@ -115,7 +116,90 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const sessionViews = (session) => Array.from(session.views.values());
const localFolderEventChannelKey = (session) => `${String(session?.rootUri || '').trim()}#${String(session?.documentId || '').trim()}`;
const resourceWatchPath = (session) => String(session?.resourcePath || session?.relativePath || '').trim();
const localFolderEventChannelKey = (session) => {
const scope = session?.sessionKind === 'resource'
? `resource:${resourceWatchPath(session)}`
: String(session?.documentId || '').trim();
return `${String(session?.rootUri || '').trim()}#${scope}`;
};
const localMarkdownRelativePathFromDocumentId = (documentId) => {
const value = String(documentId || '').trim();
if (!value.startsWith('local-md:')) return '';
return value.slice('local-md:'.length).replace(/~2F/g, '/');
};
const sessionRelativePath = (session) => (
String(session?.relativePath || '').trim()
|| localMarkdownRelativePathFromDocumentId(session?.documentId)
);
const sessionBufferStateUrl = (session) => {
if (!session || session.sourceKind !== 'local_folder' || !session.rootUri || !session.documentId) return null;
const url = new URL('/api/documents/buffer-state', window.location.origin);
url.searchParams.set('documentId', session.documentId);
url.searchParams.set('sourceKind', session.sourceKind);
url.searchParams.set('rootUri', session.rootUri);
if (session.workspaceId) url.searchParams.set('workspaceId', session.workspaceId);
const relativePath = sessionRelativePath(session);
if (relativePath) url.searchParams.set('relativePath', relativePath);
return url;
};
const applyBufferStateToSession = (session, bufferState) => {
if (!session || !bufferState || typeof bufferState !== 'object') return;
const dirtyState = String(bufferState.dirtyState || '').trim();
if (dirtyState) session.bufferDirtyState = dirtyState;
if (typeof bufferState.fileVersion === 'string' && bufferState.fileVersion.trim()) {
session.fileVersion = bufferState.fileVersion.trim();
if (!session.conflictDetectionKey) session.conflictDetectionKey = session.fileVersion;
}
if (typeof bufferState.externalActor === 'string' && bufferState.externalActor.trim()) {
session.lastExternalWriteSource = bufferState.externalActor.trim();
}
};
const fetchSessionBufferState = async (session) => {
const url = sessionBufferStateUrl(session);
if (!url) return null;
try {
const response = await fetch(url.toString(), {
cache: 'no-store',
headers: { accept: 'application/json' },
});
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || payload.ok !== true) return null;
const bufferState = payload.result || null;
applyBufferStateToSession(session, bufferState);
return bufferState;
} catch (_error) {
return null;
}
};
const markSessionBufferDirty = (session) => {
if (!session || session.sourceKind !== 'local_folder' || session.sessionKind === 'resource') return;
const relativePath = sessionRelativePath(session);
if (!relativePath) return;
const serialized = String(session.currentSerialized || '');
const contentHash = `browser-dirty:${serialized.length}:${serialized.charCodeAt(0) || 0}:${serialized.charCodeAt(serialized.length - 1) || 0}`;
void fetch('/api/documents/buffer-state/dirty', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({
documentId: session.documentId,
workspaceId: session.workspaceId,
sourceKind: session.sourceKind,
rootUri: session.rootUri,
relativePath,
contentHash,
}),
}).then((response) => response.json().catch(() => null)).then((payload) => {
if (payload && payload.ok === true) applyBufferStateToSession(session, payload.result);
}).catch(() => undefined);
};
const detachSessionFromLocalFolderChannel = (session) => {
const channel = session.localFolderChannel;
@@ -416,16 +500,21 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const ensureSessionConflictEnvelope = (session, message) => {
if (!session) return null;
if (session.lastExternalConflictEnvelope) return session.lastExternalConflictEnvelope;
const isResourceSession = session.sessionKind === 'resource';
const defaultMessage = isResourceSession
? '当前本地资源文件已在外部更新,请刷新或保存前先处理冲突'
: externalConflictMessage;
const envelope = {
code: 'local_markdown_external_change',
code: isResourceSession ? 'local_resource_external_change' : 'local_markdown_external_change',
documentId: session.documentId,
rootUri: session.rootUri || null,
path: isResourceSession ? (session.resourcePath || '') : undefined,
currentDiskVersion: session.conflictDetectionKey || session.fileVersion || null,
editorBaseVersion: session.lastExternalConflictDetectionKey || session.conflictDetectionKey || session.fileVersion || null,
externalActor: session.lastExternalWriteSource || null,
dirtyState: session.dirty ? 'Dirty' : (session.hasExternalConflict ? 'ExternalModified' : 'Clean'),
bufferFileVersion: session.fileVersion || session.conflictDetectionKey || null,
message: message || externalConflictMessage,
message: isResourceSession && (!message || message === externalConflictMessage) ? defaultMessage : (message || defaultMessage),
suggestedActions: ['accept_disk', 'keep_editor', 'open_diff', 'merge'],
};
session.lastExternalConflictEnvelope = envelope;
@@ -802,13 +891,13 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const markSessionExternalConflict = (session, message) => {
session.externalChangePending = false;
ensureSessionConflictEnvelope(session, message);
const envelope = ensureSessionConflictEnvelope(session, message);
session.hasExternalConflict = true;
if (session.saveTimer) {
window.clearTimeout(session.saveTimer);
session.saveTimer = 0;
}
const nextMessage = message || externalConflictMessage;
const nextMessage = envelope?.message || message || externalConflictMessage;
setSessionStatus(session, 'external-change-conflict', nextMessage);
renderSessionConflictSurface(session, nextMessage);
};
@@ -817,12 +906,23 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
if (session.readOnly || session.hasExternalConflict) return;
if (session.saveTimer) window.clearTimeout(session.saveTimer);
setSessionStatus(session, 'dirty');
markSessionBufferDirty(session);
session.saveTimer = window.setTimeout(() => {
session.saveTimer = 0;
void persistSession(session);
}, 650);
};
const makeSaveOperationToken = (prefix, session) => {
const doc = String(session?.documentId || session?.resourcePath || 'unknown')
.replace(/[^a-zA-Z0-9._:-]+/g, '-')
.slice(0, 80);
const random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
return `${prefix}:${doc}:${random}`;
};
const persistSession = async (session) => {
if (session.readOnly || session.saving || session.hasExternalConflict) return;
const hydrateView = sessionViews(session).find((item) => item.mountId != null) || sessionViews(session)[0];
@@ -841,11 +941,30 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const editorDocument = editorDocumentFromTiptapDocument({ documentId: session.documentId }, session.currentTiptapDocument);
const content = legacyBlocksFromEditorDocument(editorDocument);
const saveEndpoint = session.saveEndpoint || '/api/documents/save';
const expectedFileVersion = (
session.sourceKind === 'local_folder'
&& !session.lastExternalConflictEnvelope
&& session.lastExternalConflictDetectionKey
)
? session.lastExternalConflictDetectionKey
: session.conflictDetectionKey;
if (session.sourceKind === 'local_folder' && expectedFileVersion && expectedFileVersion !== session.conflictDetectionKey) {
session.conflictDetectionKey = expectedFileVersion;
session.fileVersion = expectedFileVersion;
}
const writeIntentId = session.sourceKind === 'local_folder'
? makeSaveOperationToken('intent:tiptap', session)
: '';
const saveOperationId = session.sourceKind === 'local_folder'
? makeSaveOperationToken('save:tiptap', session)
: '';
const savePayload = session.sessionKind === 'resource'
? {
rootUri: session.rootUri,
path: session.resourcePath,
expectedFileVersion: session.conflictDetectionKey,
expectedFileVersion: expectedFileVersion,
writeIntentId,
saveOperationId,
contentFormat: 'editorBlocks',
editorSource: 'tiptap-resource-tab',
editorDocument,
@@ -859,7 +978,9 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
sourceKind: session.sourceKind,
rootUri: session.rootUri,
revision: session.revision,
expectedFileVersion: session.conflictDetectionKey,
expectedFileVersion: expectedFileVersion,
writeIntentId,
saveOperationId,
contentFormat: 'editorBlocks',
editorSource: 'tiptap',
editorDocument,
@@ -889,6 +1010,16 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
throw new Error(message);
}
const saved = result.result || {};
if (typeof saved.writeIntentId === 'string' && saved.writeIntentId.trim()) {
session.lastWriteIntentId = saved.writeIntentId.trim();
} else if (writeIntentId) {
session.lastWriteIntentId = writeIntentId;
}
if (typeof saved.saveOperationId === 'string' && saved.saveOperationId.trim()) {
session.lastSaveOperationId = saved.saveOperationId.trim();
} else if (saveOperationId) {
session.lastSaveOperationId = saveOperationId;
}
if (Number.isInteger(saved.revision)) session.revision = saved.revision;
if (typeof saved.conflict_detection_key === 'string' && saved.conflict_detection_key.trim()) {
session.conflictDetectionKey = saved.conflict_detection_key.trim();
@@ -902,6 +1033,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
if (session.conflictDetectionKey) session.lastExternalConflictDetectionKey = session.conflictDetectionKey;
session.hasExternalConflict = false;
session.externalChangePending = false;
session.bufferDirtyState = 'Clean';
session.lastUserInputAt = 0;
syncSessionMetaToViews(session);
session.lastPersistedSerialized = serialized;
@@ -951,7 +1083,47 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
if (document.hidden) return;
if (!session || session.views.size === 0) return;
if (session.sourceKind === 'local_folder' && !session.rootUri) return;
if (session.sessionKind === 'resource') {
await refreshResourceSessionFromExternalChange(session, source);
return;
}
try {
const bufferState = await fetchSessionBufferState(session);
const dirtyState = String(bufferState?.dirtyState || session.bufferDirtyState || '').trim();
if (dirtyState === 'Deleted') {
session.lastExternalConflictEnvelope = {
code: 'local_markdown_deleted',
documentId: session.documentId,
rootUri: session.rootUri || null,
currentDiskVersion: bufferState?.fileVersion || session.conflictDetectionKey || session.fileVersion || null,
editorBaseVersion: session.conflictDetectionKey || session.fileVersion || null,
externalActor: bufferState?.externalActor || null,
dirtyState,
bufferFileVersion: bufferState?.fileVersion || session.fileVersion || null,
message: '当前本地 Markdown 文件已被删除,请恢复文件或关闭当前标签',
suggestedActions: ['keep_editor', 'open_diff'],
};
markSessionExternalConflict(session, session.lastExternalConflictEnvelope.message);
return;
}
if (session.hasExternalConflict) {
markSessionExternalConflict(session, conflictMessageFromEnvelope(session, {
message: externalConflictMessage,
dirtyState,
fileVersion: bufferState?.fileVersion || null,
externalActor: bufferState?.externalActor || null,
}));
return;
}
if (sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
session.externalChangePending = true;
if (session.dirty && !session.saveTimer && !session.saving) {
queueSessionSave(session);
} else if (!session.saving) {
setSessionStatus(session, 'dirty');
}
return;
}
const response = await fetch(pageAggregateUrl({
documentId: session.documentId,
sourceKind: session.sourceKind,
@@ -999,13 +1171,17 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const nextSerialized = JSON.stringify(nextTiptapDocument);
const contentChanged = nextSerialized !== session.currentSerialized;
session.externalChangePending = false;
session.bufferDirtyState = 'Clean';
if (!nextConflictKey || !session.lastExternalConflictDetectionKey) {
session.lastExternalConflictDetectionKey = nextConflictKey || session.lastExternalConflictDetectionKey;
if (!contentChanged) return;
}
if (nextConflictKey === session.lastExternalConflictDetectionKey && !contentChanged) return;
if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
markSessionExternalConflict(session, externalConflictMessage);
if (nextConflictKey === session.lastExternalConflictDetectionKey) {
if (nextConflictKey && session.conflictDetectionKey !== nextConflictKey) {
session.conflictDetectionKey = nextConflictKey;
session.fileVersion = nextConflictKey;
syncSessionMetaToViews(session);
}
return;
}
const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey);
@@ -1031,6 +1207,78 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
}
};
const refreshResourceSessionFromExternalChange = async (session, source) => {
if (document.hidden) return;
if (!session || session.views.size === 0) return;
if (session.sourceKind !== 'local_folder' || !session.rootUri || !session.resourcePath) return;
let nextResource = null;
try {
nextResource = await fetchLatestResourceSnapshot(session);
} catch (error) {
session.lastExternalConflictEnvelope = {
code: 'local_resource_deleted',
documentId: session.documentId,
rootUri: session.rootUri || null,
path: session.resourcePath || '',
currentDiskVersion: null,
editorBaseVersion: session.conflictDetectionKey || session.fileVersion || null,
externalActor: 'external-editor',
dirtyState: 'Deleted',
message: '当前本地资源文件已被删除或移动,请恢复文件或关闭当前标签',
suggestedActions: ['keep_editor', 'open_diff'],
};
markSessionExternalConflict(session, session.lastExternalConflictEnvelope.message);
return;
}
const nextConflictKey = String(nextResource?.fileVersion || nextResource?.conflictDetectionKey || '').trim();
const nextTiptapDocument = toTiptapDocument(nextResource?.content, nextResource?.text || '');
const nextSerialized = JSON.stringify(nextTiptapDocument);
const contentChanged = nextSerialized !== session.currentSerialized;
session.externalChangePending = false;
if (!nextConflictKey || !session.lastExternalConflictDetectionKey) {
session.lastExternalConflictDetectionKey = nextConflictKey || session.lastExternalConflictDetectionKey;
if (!contentChanged) return;
}
if (nextConflictKey === session.lastExternalConflictDetectionKey) {
if (nextConflictKey && session.conflictDetectionKey !== nextConflictKey) {
session.conflictDetectionKey = nextConflictKey;
session.fileVersion = nextConflictKey;
syncSessionMetaToViews(session);
}
return;
}
if (session.hasExternalConflict || sessionHasRecentLocalInput(session) || session.dirty || session.saveTimer || session.saving) {
session.lastExternalConflictEnvelope = {
code: 'local_resource_external_change',
documentId: session.documentId,
rootUri: session.rootUri || null,
path: session.resourcePath || '',
currentDiskVersion: nextConflictKey || null,
editorBaseVersion: session.conflictDetectionKey || session.fileVersion || null,
externalActor: 'external-editor',
dirtyState: session.dirty ? 'Dirty' : 'Clean',
message: '当前本地资源文件已在外部更新,请刷新或保存前先处理冲突',
suggestedActions: ['accept_disk', 'keep_editor', 'open_diff'],
};
markSessionExternalConflict(session, session.lastExternalConflictEnvelope.message);
return;
}
session.currentTiptapDocument = nextTiptapDocument;
session.currentSerialized = nextSerialized;
session.lastPersistedSerialized = nextSerialized;
session.conflictDetectionKey = nextConflictKey;
session.fileVersion = nextConflictKey;
session.lastExternalConflictDetectionKey = nextConflictKey || '';
session.dirty = false;
session.hasExternalConflict = false;
session.lastUserInputAt = 0;
sessionViews(session).forEach((view) => {
if (view.mountId != null) dispatchSessionContentToView(session, view, source || 'mnote-web-resource-watch');
});
setSessionStatus(session, 'synced-external-change');
syncResourceSessionTabGuards(session);
};
const refreshSessionFromExternalFileChange = async (session) => {
if (session.sourceKind !== 'local_folder') return;
await refreshSessionFromExternalChange(session, 'mnote-web-local-folder-watch');
@@ -1045,12 +1293,17 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
if (!channel) {
const url = new URL('/api/local-folder/events', window.location.origin);
url.searchParams.set('rootUri', session.rootUri);
url.searchParams.set('documentId', session.documentId);
if (session.sessionKind === 'resource') {
url.searchParams.set('resourcePath', resourceWatchPath(session));
} else {
url.searchParams.set('documentId', session.documentId);
}
const eventSource = new EventSource(url.toString());
channel = {
key: channelKey,
rootUri: session.rootUri,
documentId: session.documentId,
resourcePath: session.sessionKind === 'resource' ? resourceWatchPath(session) : '',
eventSource,
sessions: new Map(),
};
@@ -1060,6 +1313,19 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
Array.from(channel.sessions.values()).forEach((targetSession) => {
if (!targetSession || targetSession.views.size === 0) return;
const documentId = typeof payload.documentId === 'string' ? payload.documentId.trim() : '';
const relativePath = typeof payload.relativePath === 'string' ? payload.relativePath.trim() : '';
if (targetSession.sessionKind === 'resource') {
if (!relativePath || relativePath !== resourceWatchPath(targetSession)) return;
if (targetSession.saving) {
targetSession.externalChangePending = false;
targetSession.lastSelfSaveSignalAt = Date.now();
return;
}
targetSession.lastExternalChangeSignalAt = Date.now();
targetSession.externalChangePending = true;
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-resource-watch');
return;
}
if (!documentId) return;
const eventKind = String(payload.eventKind || '');
const targetsCurrentDocument = Boolean(documentId && documentId === targetSession.documentId);
@@ -1076,10 +1342,6 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
}
targetSession.lastExternalChangeSignalAt = Date.now();
targetSession.externalChangePending = true;
if (targetsCurrentDocument && (targetSession.hasExternalConflict || sessionHasRecentLocalInput(targetSession) || targetSession.dirty || targetSession.saveTimer || targetSession.saving)) {
markSessionExternalConflict(targetSession, externalConflictMessage);
return;
}
scheduleSessionExternalRefresh(targetSession, 'mnote-web-local-folder-watch');
});
});
@@ -1277,11 +1539,67 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
return scheduled;
};
const applyLocalUploadEditorSave = (detail) => {
const documentId = String(detail?.documentId || '').trim();
const rootUri = String(detail?.rootUri || '').trim();
const fileVersion = String(detail?.fileVersion || '').trim();
const writeIntentId = String(detail?.writeIntentId || '').trim();
const saveOperationId = String(detail?.saveOperationId || '').trim();
const serialized = String(detail?.serialized || '');
if (!documentId || !rootUri || !fileVersion) return 0;
let applied = 0;
Array.from(documentSessionRegistry.values()).forEach((session) => {
if (session.sourceKind !== 'local_folder') return;
if (session.documentId !== documentId) return;
if (String(session.rootUri || '').trim() !== rootUri) return;
if (session.saveTimer) {
window.clearTimeout(session.saveTimer);
session.saveTimer = 0;
}
session.conflictDetectionKey = fileVersion;
session.fileVersion = fileVersion;
session.lastExternalConflictDetectionKey = fileVersion;
if (writeIntentId) session.lastWriteIntentId = writeIntentId;
if (saveOperationId) session.lastSaveOperationId = saveOperationId;
if (session.latestAggregate && typeof session.latestAggregate === 'object') {
const nextAggregate = { ...session.latestAggregate };
const nextBody = nextAggregate.body && typeof nextAggregate.body === 'object'
? { ...nextAggregate.body }
: {};
nextBody.fileVersion = fileVersion;
nextBody.conflictDetectionKey = fileVersion;
nextBody.conflict_detection_key = fileVersion;
nextAggregate.body = nextBody;
session.latestAggregate = nextAggregate;
syncPageAggregateScript(session, nextAggregate);
}
session.externalChangePending = false;
session.hasExternalConflict = false;
session.lastExternalConflictEnvelope = null;
session.bufferDirtyState = 'Clean';
if (!serialized || serialized === session.currentSerialized) {
session.lastPersistedSerialized = session.currentSerialized;
session.dirty = false;
setSessionStatus(session, 'saved');
} else {
session.dirty = true;
setSessionStatus(session, 'dirty');
queueSessionSave(session);
}
syncResourceSessionTabGuards(session);
applied += 1;
});
return applied;
};
window.addEventListener('tree:delta', handleTreeExternalChange);
window.addEventListener('tree:resync', handleTreeExternalChange);
window.addEventListener('mnote:page-ai-tool-write-completed', (event) => {
refreshDocumentSessionsFromExternalWrite(event?.detail || {}, 'mnote-hermes-tool');
});
window.addEventListener('mnote:local-upload-editor-save-completed', (event) => {
applyLocalUploadEditorSave(event?.detail || {});
});
const createDocumentSession = (runtimeDescriptor) => {
const pageBody = runtimeDescriptor.aggregate.body || {};
@@ -1291,6 +1609,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
const keyRevision = revisionFromConflictKey(conflictDetectionKey);
const sourceKind = normalizeSessionSourceKind(runtimeDescriptor.bootstrap);
const tiptapDocument = pageBodyTiptapDocument(pageBody, '', runtimeDescriptor.bootstrap);
const pageBodySource = pageBodyTiptapDocumentSource(pageBody, '');
const session = {
key: buildDocumentSessionKey(runtimeDescriptor.bootstrap),
documentId: runtimeDescriptor.bootstrap.documentId,
@@ -1301,6 +1620,9 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
pageAggregateScriptId: runtimeDescriptor.bootstrap.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__',
latestAggregate: runtimeDescriptor.aggregate,
title: runtimeDescriptor.aggregate.head?.title || '无标题',
pageBodySource,
projectionSource: String(pageBody.projectionSource || pageBody.projection_source || '').trim(),
blockProjectionVersion: String(pageBody.blockProjectionVersion || pageBody.block_projection_version || '').trim(),
currentTiptapDocument: tiptapDocument,
currentSerialized: JSON.stringify(tiptapDocument),
lastPersistedSerialized: JSON.stringify(tiptapDocument),
@@ -1308,6 +1630,8 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
conflictDetectionKey,
fileVersion: typeof pageBody.fileVersion === 'string' ? pageBody.fileVersion : conflictDetectionKey,
lastExternalConflictDetectionKey: conflictDetectionKey || '',
relativePath: localMarkdownRelativePathFromDocumentId(runtimeDescriptor.bootstrap.documentId),
bufferDirtyState: 'Clean',
readOnly: Boolean(permissions.readOnly),
dirty: false,
saving: false,
@@ -1355,6 +1679,7 @@ export const createDocumentSessionRuntime = (dependencies = {}) => {
markSessionExternalConflict,
queueSessionSave,
refreshSessionFromExternalChange,
ensureLocalFolderEventChannel,
releaseDocumentSession,
scheduleDocumentSessionRelease,
sessionHasRecentExternalSignal,
@@ -394,8 +394,18 @@ export const localizeTiptapAssetUrls = (node, context) => {
return node;
};
export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
export const pageBodyTiptapDocumentSource = (body, fallbackText = '') => {
if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) {
return 'local_markdown.content';
}
const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
if (blockDocument) return 'page_aggregate.block_document';
if (body?.content) return 'compat.legacy_content';
return fallbackText ? 'degraded.fallback_text' : 'empty';
};
export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
if (pageBodyTiptapDocumentSource(body, fallbackText) === 'local_markdown.content') {
return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
}
const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
@@ -1,7 +1,122 @@
// MNote 文件树上下文菜单运行时外置模块。
// 当前先承接 CommandContext 构造与 when 表达式求值,树语义仍由 Rust/kernel 主导。
function commandContextResourceKindForRow(row) {
if (!(row instanceof HTMLElement)) return 'unknown';
var rowKind = String(row.getAttribute('data-row-kind') || '').trim();
var assetType = String(row.getAttribute('data-asset-type') || '').trim();
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
if (rowKind === 'folder' || rowKind === 'directory' || rowKind === 'asset-folder') return 'folder';
if (rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') return 'page';
if (assetType) return assetType;
if (assetId) return 'asset';
return rowKind || 'unknown';
}
function commandContextDirtyState(value) {
var state = String(value || '').trim();
if (!state) return 'clean';
var lowered = state.toLowerCase();
if (lowered === 'dirty' || lowered === 'stale' || lowered === 'deleted') return lowered;
if (lowered === 'externalmodified' || lowered === 'external_modified' || lowered === 'external-change-conflict') return 'external_modified';
if (lowered === 'hasexternalconflict' || lowered === 'has_external_conflict') return 'has_external_conflict';
return lowered === 'clean' || lowered === 'saved' ? 'clean' : lowered;
}
function commandContextIsDirtyState(value) {
return commandContextDirtyState(value) !== 'clean';
}
function commandContextEditorMatchesRow(editor, row) {
if (!editor || !(row instanceof HTMLElement)) return false;
var rowDocumentId = String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
if (rowDocumentId && String(editor.documentId || '').trim() === rowDocumentId) return true;
var rowAssetId = String(row.getAttribute('data-asset-id') || '').trim();
if (rowAssetId && String(editor.assetId || editor.workspacePath && editor.workspacePath.assetId || '').trim() === rowAssetId) return true;
var rowRelativePath = String(row.getAttribute('data-local-relative-path') || '').trim();
var editorRelativePath = String(editor.workspacePath && (editor.workspacePath.relativePath || editor.workspacePath.localRelativePath) || '').trim();
return Boolean(rowRelativePath && editorRelativePath && rowRelativePath === editorRelativePath);
}
function commandContextDirtyStateForRows(rows, openEditorsSnapshot, bufferState) {
if (bufferState && typeof bufferState === 'object') {
var directState = bufferState.dirtyState || bufferState.state || bufferState.status || '';
if (commandContextIsDirtyState(directState)) return commandContextDirtyState(directState);
}
var editors = [];
if (openEditorsSnapshot && Array.isArray(openEditorsSnapshot.editors)) editors = editors.concat(openEditorsSnapshot.editors);
if (openEditorsSnapshot && Array.isArray(openEditorsSnapshot.resourceEditors)) editors = editors.concat(openEditorsSnapshot.resourceEditors);
for (var r = 0; r < rows.length; r += 1) {
var row = rows[r];
for (var i = 0; i < editors.length; i += 1) {
var editor = editors[i];
if (!commandContextEditorMatchesRow(editor, row)) continue;
var editorState = editor.dirtyState || editor.dirtyGuard || '';
if (commandContextIsDirtyState(editorState)) return commandContextDirtyState(editorState);
}
}
return 'clean';
}
function selectedRowsFromDeps(deps, fallbackRow) {
var selection = deps && deps.selection ? deps.selection : {};
var rowIds = Array.isArray(selection.selectedRowIds)
? selection.selectedRowIds
: Array.from(selection.selectedRowIds || []);
var rows = [];
var queryRowById = deps && typeof deps.queryRowById === 'function'
? deps.queryRowById
: function() { return null; };
for (var i = 0; i < rowIds.length; i += 1) {
var row = queryRowById(rowIds[i]);
if (row instanceof HTMLElement) rows.push(row);
}
if (!rows.length && fallbackRow instanceof HTMLElement) rows.push(fallbackRow);
return rows;
}
function buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState, deps) {
deps = deps || {};
var rows = selectedRowsFromDeps(deps, row);
var resourceKinds = {};
for (var j = 0; j < rows.length; j += 1) {
resourceKinds[commandContextResourceKindForRow(rows[j])] = true;
}
var rkKeys = Object.keys(resourceKinds);
var currentSourceKind = typeof deps.currentSourceKind === 'function'
? deps.currentSourceKind
: function() { return ''; };
var readonly = typeof deps.workspaceReadonly === 'function'
? deps.workspaceReadonly()
: false;
var dirtyState = commandContextDirtyStateForRows(rows, openEditorsSnapshot, bufferState);
var editorDirty = commandContextIsDirtyState(dirtyState);
var targetKind = commandContextResourceKindForRow(row);
return {
'workspace.sourceKind': currentSourceKind() || '',
'workspace.readonly': readonly === true,
'tree.focusKind': deps.focusKind || 'file_tree',
'tree.selectionCount': rows.length,
'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : rows.length ? 'mixed' : targetKind,
'tree.targetResourceKind': targetKind,
'tree.targetRowKind': row instanceof HTMLElement ? String(row.getAttribute('data-row-kind') || '').trim() : '',
'tree.targetIsFolder': targetKind === 'folder',
'tree.targetIsAsset': Boolean(row instanceof HTMLElement && String(row.getAttribute('data-asset-id') || '').trim()),
'editor.dirty': editorDirty,
'editor.dirtyState': dirtyState,
'editor.hasSelection': Boolean(deps.editorHasSelection),
'ai.canWrite': readonly !== true && !editorDirty,
};
}
function buildSidebarFileTreeContext(kind, deps) {
var targetRow = deps && deps.targetRow instanceof HTMLElement ? deps.targetRow : null;
return buildFileTreeCommandContext(targetRow, deps && deps.openEditorsSnapshot, deps && deps.bufferState, Object.assign({}, deps || {}, {
focusKind: kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind,
}));
}
function buildLegacySidebarFileTreeContext(kind, deps) {
var selection = deps && deps.selection ? deps.selection : {};
var rowIds = Array.isArray(selection.selectedRowIds)
? selection.selectedRowIds
@@ -181,7 +296,9 @@ function tokenizeWhenExpression(expr) {
}
window.__mnoteFileTreeContextMenuRuntime = {
buildFileTreeCommandContext: buildFileTreeCommandContext,
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
buildLegacySidebarFileTreeContext: buildLegacySidebarFileTreeContext,
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
tokenizeWhenExpression: tokenizeWhenExpression
};
@@ -42,6 +42,10 @@ function handleFileTreeKeyDown(event, deps) {
if (event.key === 'F2') {
event.preventDefault();
var renameCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(renameCtx, '!workspace.readonly && !editor.dirty')) {
return true;
}
beginFileTreeInlineRename(fileTreeRowForKey);
return true;
}
@@ -61,6 +65,10 @@ function handleFileTreeKeyDown(event, deps) {
}
if (shortcutKey === 'v') {
event.preventDefault();
var pasteCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(pasteCtx, '!workspace.readonly')) {
return true;
}
if (typeof deps.pasteSidebarFileTreeClipboard === 'function') {
void deps.pasteSidebarFileTreeClipboard(fileTreeRowForKey);
}
@@ -70,8 +78,8 @@ function handleFileTreeKeyDown(event, deps) {
if (event.key === 'Delete' || event.key === 'Backspace') {
event.preventDefault();
var delCtx = buildSidebarFileTreeContext('filetree');
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')) {
var delCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly && !editor.dirty')) {
return true;
}
void deleteSelectedSidebarFileTreeRows(fileTreeRowForKey);
@@ -70,6 +70,57 @@ function fileTreeRowLocalUploadTargetRelativePath(row, deps) {
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
}
function parseObjectIdentityAttr(value) {
var raw = String(value || '').trim();
if (!raw) return null;
try {
var parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : { raw: raw };
} catch (_) {
return { raw: raw };
}
}
function readWorkspacePathFromRow(row, deps) {
if (!(row instanceof HTMLElement)) return null;
deps = deps || {};
var currentSourceKind = typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
var currentRootUri = typeof deps.currentRootUri === 'function' ? deps.currentRootUri : function() { return ''; };
var resolveWorkspaceId = typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
var rowTitle = typeof deps.rowTitle === 'function' ? deps.rowTitle : function(target) {
return String(target && target.textContent || '').trim();
};
var objectIdentityRaw = String(row.getAttribute('data-object-identity') || '').trim();
var objectIdentity = parseObjectIdentityAttr(objectIdentityRaw);
var objectKind = String(row.getAttribute('data-object-kind') || (objectIdentity && objectIdentity.objectKind) || '').trim();
var relativePath = fileTreeRowLocalRelativePath(row, deps);
var assetId = fileTreeRowAssetId(row, deps);
var documentId = fileTreeRowDocumentId(row);
var rowId = String(row.getAttribute('data-row-id') || '').trim();
var rowKind = fileTreeRowKind(row);
var sourceKind = String(row.getAttribute('data-source-kind') || currentSourceKind() || '').trim();
var rootUri = String(row.getAttribute('data-root-uri') || currentRootUri() || '').trim();
return {
schema: 'mnote.workspace_path.v1',
workspaceId: String(row.getAttribute('data-workspace-id') || resolveWorkspaceId(row) || '').trim(),
sourceKind: sourceKind,
rootUri: rootUri,
relativePath: relativePath,
localRelativePath: relativePath,
objectIdentity: objectIdentity,
objectIdentityRaw: objectIdentityRaw,
objectKind: objectKind,
resourceKind: objectKind || rowKind,
rowId: rowId,
rowKind: rowKind,
documentId: documentId,
assetId: assetId,
title: rowTitle(row),
href: String(row.querySelector?.('.tree-link')?.getAttribute?.('href') || '').trim(),
isLocalFolder: sourceKind === 'local_folder' || (!sourceKind && (rowId.indexOf('local:') === 0 || documentId.indexOf('local-md:') === 0 || documentId.indexOf('local-dir:') === 0))
};
}
function isFileTreeDownloadableAssetRow(row, deps) {
if (!(row instanceof HTMLElement)) return false;
var kind = fileTreeRowKind(row);
@@ -98,6 +149,7 @@ function fileTreeAssetDownloadDetail(row, deps) {
rowKind: fileTreeRowKind(row),
assetId: assetId,
localRelativePath: relativePath,
workspacePath: readWorkspacePathFromRow(row, deps),
title: title,
fileName: title,
workspaceId: resolveWorkspaceId(row)
@@ -274,6 +326,49 @@ function revealFileTreeAssetRow(assetId, deps) {
return true;
}
function localUploadedAssetRelativePath(asset, deps) {
deps = deps || {};
var direct = String(asset && (asset.rootRelativePath || asset.root_relative_path || asset.relativePath || asset.relative_path) || '').trim();
if (direct) return direct;
var localFilePathFromAssetId = typeof deps.localFilePathFromAssetId === 'function'
? deps.localFilePathFromAssetId
: function(assetId) {
var value = String(assetId || '').trim();
return value.indexOf('local-file:') === 0
? value.slice('local-file:'.length)
: value.indexOf('local:asset:') === 0
? value.slice('local:asset:'.length)
: '';
};
return localFilePathFromAssetId(String(asset && asset.id || '').trim());
}
function parentRelativePathForUploadedAsset(asset, relativePath) {
var target = String(asset && (asset.targetRelativePath || asset.target_relative_path) || '').trim();
if (target || Object.prototype.hasOwnProperty.call(asset || {}, 'targetRelativePath')) return target;
var normalized = String(relativePath || '').trim().replace(/^\/+|\/+$/g, '');
var lastSlash = normalized.lastIndexOf('/');
return lastSlash >= 0 ? normalized.slice(0, lastSlash) : '';
}
function findLocalFileTreeParentRowForAsset(asset, relativePath, deps) {
deps = deps || {};
var cssEscape = typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
var currentSourceKind = typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
if (currentSourceKind() !== 'local_folder') return null;
var parentRelativePath = parentRelativePathForUploadedAsset(asset, relativePath);
var selector = '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="' + cssEscape(parentRelativePath) + '"]';
if (parentRelativePath) {
var parentRow = document.querySelector(selector);
if (parentRow instanceof HTMLElement) return parentRow;
}
if (!parentRelativePath) {
var root = document.querySelector('#sidebar-file-tree-root .tree-root');
return root instanceof HTMLElement ? null : null;
}
return null;
}
function appendUploadedAssetRow(asset, documentId, deps) {
deps = deps || {};
var cssEscape = typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
@@ -295,8 +390,10 @@ function appendUploadedAssetRow(asset, documentId, deps) {
if (!objectKind && (String(asset && (asset.asset_type || asset.assetType) || '').trim() === 'mindmap' || /\.mindmap\.json$/i.test(assetId))) {
objectKind = 'mindmap';
}
var targetDocumentId = String(documentId || asset.document_id || asset.documentId || currentDocumentId() || '').trim();
var parentRow = targetDocumentId
var localRelativePath = localUploadedAssetRelativePath(asset, deps);
var targetDocumentId = String(documentId || asset.document_id || asset.documentId || asset.ownerDocumentId || currentDocumentId() || '').trim();
var parentRow = findLocalFileTreeParentRowForAsset(asset, localRelativePath, deps);
if (!parentRow) parentRow = targetDocumentId
? document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + targetDocumentId) + '"]')
: null;
if (!parentRow && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false;
@@ -325,16 +422,20 @@ function appendUploadedAssetRow(asset, documentId, deps) {
objectKind: objectKind || 'attachment',
documentId: targetDocumentId || null,
blockId: null,
assetId: assetId
assetId: assetId,
rootUri: String(asset && (asset.rootUri || asset.root_uri) || '').trim() || null,
relativePath: localRelativePath || null
};
li.setAttribute('data-node-id', 'asset:' + assetId);
var title = uploadedAssetTitle(asset);
var iconKind = fileTreeIconKindForFileName(title) || uploadedAssetType(asset) || 'file';
if (objectKind === 'mindmap') iconKind = 'mindmap';
var ownerDocumentId = String(asset && (asset.ownerDocumentId || asset.owner_document_id) || targetDocumentId || '').trim();
var rootUri = String(asset && (asset.rootUri || asset.root_uri) || '').trim();
li.innerHTML =
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(localRelativePath) + '" data-root-uri="' + escapeHtml(rootUri) + '" data-source-kind="' + escapeHtml(rootUri ? 'local_folder' : '') + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
'<span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span>' +
'<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
'<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(localRelativePath) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
'<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
container.appendChild(li);
revealFileTreeRow(li.querySelector('.tree-row'));
@@ -584,6 +685,7 @@ window.__mnoteFileTreeRuntime = {
decodeLocalEncodedPath: decodeLocalEncodedPath,
fileTreeRowLocalRelativePath: fileTreeRowLocalRelativePath,
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
readWorkspacePathFromRow: readWorkspacePathFromRow,
isFileTreeDownloadableAssetRow: isFileTreeDownloadableAssetRow,
isFileTreeDownloadableRow: isFileTreeDownloadableRow,
fileTreeAssetDownloadDetail: fileTreeAssetDownloadDetail,
@@ -55,15 +55,91 @@ function dispatchUploadedEditorChange(editorRoot, editor, deps) {
}));
}
function currentConflictDetectionKey() {
var script = document.getElementById('__MNOTE_PAGE_AGGREGATE__');
if (!script) return '';
try {
var body = JSON.parse(script.textContent || 'null');
return String(body && (body.conflictDetectionKey || body.conflict_detection_key || body.fileVersion || body.file_version) || '').trim();
} catch (_) {
return '';
function firstNonEmptyString(values) {
for (var i = 0; i < values.length; i += 1) {
var value = String(values[i] || '').trim();
if (value) return value;
}
return '';
}
function makeSaveOperationToken(prefix, documentId) {
var doc = String(documentId || 'unknown').replace(/[^a-zA-Z0-9._:-]+/g, '-').slice(0, 80);
var random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
return prefix + ':' + doc + ':' + random;
}
function currentConflictDetectionKey(documentId, rootUri) {
try {
if (typeof window.__mnoteDebugDocumentSessions?.snapshot === 'function') {
var snapshot = window.__mnoteDebugDocumentSessions.snapshot();
var sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : [];
var targetDocumentId = String(documentId || '').trim();
var targetRootUri = String(rootUri || '').trim();
for (var i = 0; i < sessions.length; i += 1) {
var session = sessions[i] || {};
if (targetDocumentId && String(session.documentId || '').trim() !== targetDocumentId) continue;
if (targetRootUri && String(session.rootUri || '').trim() !== targetRootUri) continue;
var sessionKey = firstNonEmptyString([
session.lastExternalConflictDetectionKey,
session.conflictDetectionKey,
session.fileVersion
]);
if (sessionKey) return sessionKey;
}
}
} catch (_) {
// 继续尝试从 page aggregate script 读取版本。
}
var script = document.getElementById('__MNOTE_PAGE_AGGREGATE__');
if (script) {
try {
var aggregate = JSON.parse(script.textContent || 'null');
var body = aggregate && typeof aggregate.body === 'object' ? aggregate.body : null;
var key = firstNonEmptyString([
body && body.conflictDetectionKey,
body && body.conflict_detection_key,
body && body.fileVersion,
body && body.file_version,
aggregate && aggregate.conflictDetectionKey,
aggregate && aggregate.conflict_detection_key,
aggregate && aggregate.fileVersion,
aggregate && aggregate.file_version
]);
if (key) return key;
} catch (_) {
// ignore
}
}
return '';
}
function localUploadSessionConflict(documentId, rootUri) {
try {
if (typeof window.__mnoteDebugDocumentSessions?.snapshot !== 'function') return null;
var snapshot = window.__mnoteDebugDocumentSessions.snapshot();
var sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : [];
var targetDocumentId = String(documentId || '').trim();
var targetRootUri = String(rootUri || '').trim();
for (var i = 0; i < sessions.length; i += 1) {
var session = sessions[i] || {};
if (targetDocumentId && String(session.documentId || '').trim() !== targetDocumentId) continue;
if (targetRootUri && String(session.rootUri || '').trim() !== targetRootUri) continue;
var status = String(session.status || '').trim();
if (status === 'external-change-conflict' || session.hasExternalConflict === true || session.lastExternalConflictEnvelope) {
return {
status: status || 'external-change-conflict',
dirtyState: String(session.dirtyState || ''),
message: String(session.lastExternalConflictEnvelope?.message || '当前文档存在文件冲突,请先处理冲突后再上传附件')
};
}
}
} catch (_) {
return null;
}
return null;
}
function persistLocalFolderSelfChangeSuppression(documentId, expiresAt) {
@@ -101,7 +177,9 @@ async function persistUploadedEditorChange(editor, asset, deps) {
documentId: documentId,
sourceKind: 'local_folder',
rootUri: rootUri,
expectedFileVersion: currentConflictDetectionKey(),
expectedFileVersion: currentConflictDetectionKey(documentId, rootUri),
writeIntentId: makeSaveOperationToken('intent:local-upload', documentId),
saveOperationId: makeSaveOperationToken('save:local-upload', documentId),
contentFormat: 'editorBlocks',
editorSource: 'local-upload-runtime',
editorDocument: editorDocument,
@@ -120,10 +198,32 @@ async function persistUploadedEditorChange(editor, asset, deps) {
if (!response.ok || !result || result.ok !== true) {
var message = result && (result.message || (result.error && result.error.message)) || ('save_failed_' + response.status);
document.documentElement.setAttribute('data-mnote-last-upload-save-error', message);
if (response.status === 409 && asset && asset.id) {
document.documentElement.setAttribute('data-mnote-last-upload-orphaned-asset-id', String(asset.id || ''));
document.documentElement.setAttribute('data-mnote-last-upload-orphaned-policy', 'asset-kept-reference-unsaved-retry-required');
}
throw new Error(message);
}
var saved = result.result && typeof result.result === 'object' ? result.result : {};
var fileVersion = String(saved.fileVersion || saved.conflictDetectionKey || saved.conflict_detection_key || '').trim();
try {
window.dispatchEvent(new CustomEvent('mnote:local-upload-editor-save-completed', {
detail: {
documentId: documentId,
rootUri: rootUri,
fileVersion: fileVersion,
writeIntentId: String(saved.writeIntentId || savePayload.writeIntentId || '').trim(),
saveOperationId: String(saved.saveOperationId || savePayload.saveOperationId || '').trim(),
revision: Number.isInteger(saved.revision) ? saved.revision : null,
serialized: JSON.stringify(tiptapDocument),
source: 'local-upload-runtime'
}
}));
} catch (_) {}
document.documentElement.setAttribute('data-mnote-last-upload-saved', 'true');
document.documentElement.removeAttribute('data-mnote-last-upload-save-error');
document.documentElement.removeAttribute('data-mnote-last-upload-orphaned-asset-id');
document.documentElement.removeAttribute('data-mnote-last-upload-orphaned-policy');
}
function currentRootUri() {
@@ -391,7 +491,7 @@ async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed');
}
window.setTimeout(function() {
window.setTimeout(function annotateUploadedLink(attempt) {
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
if (typeof deps.enhanceEditorAttachmentLinks === 'function') deps.enhanceEditorAttachmentLinks();
var cssEscape = typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value || '').replace(/"/g, '\\"'); };
@@ -401,12 +501,22 @@ async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
var link = targetRoot instanceof HTMLElement
? targetRoot.querySelector(selector)
: document.querySelector(selector);
if (!(link instanceof HTMLElement) && assetId) {
var scope = targetRoot instanceof HTMLElement ? targetRoot : document;
link = Array.from(scope.querySelectorAll('.editor-surface .ProseMirror a')).find(function(candidate) {
var href = String(candidate.getAttribute('href') || '');
try { href = decodeURIComponent(href); } catch (_) {}
return href.indexOf(assetId) >= 0;
}) || null;
}
if (link instanceof HTMLElement) {
link.setAttribute('data-mnote-attachment-link', 'true');
if (assetId) link.setAttribute('data-asset-id', assetId);
if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
} else if (Number(attempt) < 20) {
window.setTimeout(function() { annotateUploadedLink(Number(attempt) + 1); }, 50);
}
}, 0);
}, 0, 0);
return inserted;
}
} catch (error) {
@@ -434,6 +544,18 @@ async function uploadFileToMediaAsset(file, plan, options, deps) {
if (getCurrentSourceKind() === 'local_folder') {
var rootUri = getCurrentRootUri();
var documentId = String(plan && plan.targetDocumentId || getCurrentDocumentId() || '').trim();
if (String(plan && plan.uploadIntent || '').trim() === 'editor.markdown.attach' && documentId) {
var conflict = localUploadSessionConflict(documentId, rootUri);
if (conflict) {
document.documentElement.setAttribute('data-mnote-last-upload-blocked-conflict', 'true');
document.documentElement.setAttribute('data-mnote-last-upload-save-error', conflict.message || '当前文档存在文件冲突,请先处理冲突后再上传附件');
throw new Error(conflict.message || '当前文档存在文件冲突,请先处理冲突后再上传附件');
}
document.documentElement.removeAttribute('data-mnote-last-upload-blocked-conflict');
}
if (String(plan && plan.uploadIntent || '').trim() === 'editor.markdown.attach' && documentId) {
persistLocalFolderSelfChangeSuppression(documentId, Date.now() + 8000);
}
var localResult = await uploadLocalFolderAsset(file, plan, {
rootUri: rootUri,
documentId: documentId,
@@ -446,7 +568,7 @@ async function uploadFileToMediaAsset(file, plan, options, deps) {
if (options.insertIntoEditor) {
await insertIntoEditor(localAsset, resolveEditorRoot(options));
}
if (typeof deps.refreshLocalFolderSidebarSnapshot === 'function') {
if (!options.insertIntoEditor && typeof deps.refreshLocalFolderSidebarSnapshot === 'function') {
void deps.refreshLocalFolderSidebarSnapshot();
}
dispatch(new EventCtor('wolai:local-assets-changed', {
@@ -528,6 +650,9 @@ function fileTreeIconKindForFileName(fileName) {
if (ext === 'md' || ext === 'markdown') return 'markdown';
if (ext === 'pdf') return 'pdf';
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image';
if (['html', 'htm', 'css', 'scss', 'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'vue', 'svelte', 'astro'].indexOf(ext) >= 0) return 'web';
if (['json', 'jsonc', 'json5', 'toml', 'yaml', 'yml', 'ini', 'env', 'xml', 'lock', 'hcl', 'tf', 'tfvars', 'nix', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop', 'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'].indexOf(ext) >= 0 || ['.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc', 'dockerfile', 'containerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'].indexOf(name) >= 0) return 'config';
if (['rs', 'py', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj', 'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd', 'psm1', 'psd1', 'proto', 'graphql', 'gql', 'prisma', 'cmake', 'bazel', 'bzl'].indexOf(ext) >= 0) return 'code';
return '';
}
@@ -23,7 +23,50 @@ function buildLocalFileOpenUrl(relativePath, download) {
return url.toString();
}
function buildPdfPreviewOpenUrl(fileUrl, fileName) {
var rawUrl = String(fileUrl || '').trim();
if (!rawUrl) return '';
var url = new URL('/pdf-preview', window.location.origin);
url.searchParams.set('fileUrl', rawUrl);
if (fileName) url.searchParams.set('fileName', fileName);
return url.toString();
}
function shouldUseOfficePreview(fileType, mode) {
var normalizedMode = String(mode || 'view').trim().toLowerCase();
if (normalizedMode === 'edit') return false;
var ext = String(fileType || '').trim().toLowerCase();
return ['docx', 'xlsx', 'xls', 'csv', 'pptx'].indexOf(ext) >= 0;
}
function buildOfficePreviewOpenUrl(input) {
var target = new URL('/office-preview', window.location.origin);
var workspacePath = input && input.workspacePath && typeof input.workspacePath === 'object' ? input.workspacePath : {};
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);
if (input.documentId) target.searchParams.set('documentId', input.documentId);
if (input.userId) target.searchParams.set('userId', input.userId);
var workspaceId = String(input.workspaceId || workspacePath.workspaceId || '').trim();
var sourceKind = String(input.sourceKind || workspacePath.sourceKind || currentSourceKind() || '').trim();
var rootUri = String(input.rootUri || workspacePath.rootUri || currentRootUri() || '').trim();
if (workspaceId) target.searchParams.set('workspaceId', workspaceId);
if (sourceKind) target.searchParams.set('sourceKind', sourceKind);
if (rootUri) target.searchParams.set('rootUri', rootUri);
return target.toString();
}
function buildOnlyOfficeOpenPath(input) {
if (shouldUseOfficePreview(input.fileType, input.mode)) {
return buildOfficePreviewOpenUrl(input);
}
var params = new URLSearchParams();
params.set('fileUrl', input.fileUrl || '');
params.set('fileName', input.fileName || '未命名资源');
@@ -47,8 +90,19 @@ function currentRootUri() {
: '';
}
function currentSourceKind() {
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('sourceKind') || '').trim();
if (fromUrl) return fromUrl;
return document.body instanceof HTMLElement
? (document.body.getAttribute('data-mnote-source-kind') || '').trim()
: '';
}
window.__mnoteResourceOpenRuntime = {
localFilePathFromAssetId: localFilePathFromAssetId,
buildLocalFileOpenUrl: buildLocalFileOpenUrl,
buildPdfPreviewOpenUrl: buildPdfPreviewOpenUrl,
buildOfficePreviewOpenUrl: buildOfficePreviewOpenUrl,
buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath
};
@@ -5,12 +5,14 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
buildLocalOnlyOfficeOpenUrl,
buildOnlyOfficeOpenPath,
buildOnlyOfficeOpenUrl,
buildPdfPreviewOpenUrl: injectedBuildPdfPreviewOpenUrl,
closestAction: injectedClosestAction,
currentDocumentId,
currentRootUri,
currentWorkspaceSourcePayload,
fileTreeIconKindForFileName,
healLegacyOfficeAttachmentParagraphs,
hydrateEditorAttachmentMeta: injectedHydrateEditorAttachmentMeta,
inferCodeAttachmentLanguage,
inferOnlyOfficeFileType,
isCodeAttachmentFileName,
@@ -28,9 +30,15 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
var node = target && target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
return node && typeof node.closest === 'function' ? node.closest(selector) : null;
};
const hydrateEditorAttachmentMeta = typeof injectedHydrateEditorAttachmentMeta === 'function'
? injectedHydrateEditorAttachmentMeta
: async function() {};
var activeEditorAttachmentLink = null;
var attachmentActionsHideTimer = 0;
var editorAttachmentMissingByKey = new Map();
var editorAttachmentRefreshSeqByKey = new Map();
var editorAttachmentLinkSelector = '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"], .editor-surface .ProseMirror a[href*="/office-preview"]';
function attachmentQueryParams(href) {
try {
@@ -60,6 +68,13 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
}
}
function localFileOpenKeyFromHref(href) {
var localFilePath = localFileOpenPathFromHref(href);
if (!localFilePath) return '';
var rootUri = localFileOpenRootUriFromHref(href) || currentRootUri() || '';
return String(rootUri || '').trim() + '\n' + localFilePath;
}
function buildLocalFileStatusUrl(relativePath, rootUri) {
var effectiveRootUri = String(rootUri || currentRootUri() || '').trim();
if (!effectiveRootUri || !relativePath) return '';
@@ -69,13 +84,26 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
return url.toString();
}
function buildPdfPreviewOpenUrl(fileUrl, fileName) {
if (typeof injectedBuildPdfPreviewOpenUrl === 'function') {
return injectedBuildPdfPreviewOpenUrl(fileUrl, fileName);
}
var rawUrl = String(fileUrl || '').trim();
if (!rawUrl) return '';
var url = new URL('/pdf-preview', window.location.origin);
url.searchParams.set('fileUrl', rawUrl);
if (fileName) url.searchParams.set('fileName', fileName);
return url.toString();
}
function setEditorAttachmentMissingState(link, missing) {
if (!(link instanceof HTMLAnchorElement)) return;
var value = Boolean(missing);
if (value) {
link.setAttribute('data-mnote-attachment-missing', 'true');
if (link.getAttribute('data-mnote-attachment-missing') === 'true' && link.classList.contains('mnote-uploaded-attachment-missing')) return;
setAttributeIfChanged(link, 'data-mnote-attachment-missing', 'true');
link.classList.add('mnote-uploaded-attachment-missing');
link.setAttribute('aria-label', (link.textContent || '附件') + '(文件不存在)');
setAttributeIfChanged(link, 'aria-label', (link.textContent || '附件') + '(文件不存在)');
} else {
if (link.getAttribute('data-mnote-attachment-missing') !== 'true') return;
link.removeAttribute('data-mnote-attachment-missing');
@@ -84,9 +112,35 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
}
}
function setAttributeIfChanged(element, name, value) {
var nextValue = String(value || '');
if (element.getAttribute(name) === nextValue) return;
element.setAttribute(name, nextValue);
}
function setEditorAttachmentMissingStateByHref(href, missing) {
var targetKey = localFileOpenKeyFromHref(href);
if (!targetKey) return;
if (missing) {
editorAttachmentMissingByKey.set(targetKey, true);
} else {
editorAttachmentMissingByKey.delete(targetKey);
}
document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(candidate) {
if (!(candidate instanceof HTMLAnchorElement)) return;
var candidateHref = candidate.getAttribute('href') || candidate.href || '';
if (localFileOpenKeyFromHref(candidateHref) !== targetKey) return;
setEditorAttachmentMissingState(candidate, missing);
});
}
async function refreshLocalAttachmentExistence(link) {
if (!(link instanceof HTMLAnchorElement)) return;
var href = link.getAttribute('href') || link.href || '';
var attachmentKey = localFileOpenKeyFromHref(href);
if (!attachmentKey) return;
var refreshSeq = (editorAttachmentRefreshSeqByKey.get(attachmentKey) || 0) + 1;
editorAttachmentRefreshSeqByKey.set(attachmentKey, refreshSeq);
var localFilePath = localFileOpenPathFromHref(href);
if (!localFilePath) return;
var statusUrl = buildLocalFileStatusUrl(localFilePath, localFileOpenRootUriFromHref(href));
@@ -94,11 +148,32 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
try {
var response = await fetch(statusUrl, { headers: { accept: 'application/json' }, cache: 'no-store' });
var payload = await response.json().catch(function() { return null; });
if (editorAttachmentRefreshSeqByKey.get(attachmentKey) !== refreshSeq) return;
if (!response.ok || !payload || payload.ok !== true || !payload.result) {
setEditorAttachmentStatusErrorByHref(href, String(response.status || 'stat_failed'));
return;
}
var exists = Boolean(response.ok && payload && payload.ok === true && payload.result && payload.result.exists === true);
setEditorAttachmentMissingState(link, !exists);
setEditorAttachmentStatusErrorByHref(href, '');
setEditorAttachmentMissingStateByHref(href, !exists);
} catch (_) {}
}
function setEditorAttachmentStatusErrorByHref(href, status) {
var targetKey = localFileOpenKeyFromHref(href);
if (!targetKey) return;
document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(candidate) {
if (!(candidate instanceof HTMLAnchorElement)) return;
var candidateHref = candidate.getAttribute('href') || candidate.href || '';
if (localFileOpenKeyFromHref(candidateHref) !== targetKey) return;
if (status) {
setAttributeIfChanged(candidate, 'data-mnote-attachment-status-error', status);
} else {
candidate.removeAttribute('data-mnote-attachment-status-error');
}
});
}
function refreshEditorLocalAttachmentExistence() {
document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(link) {
if (link instanceof HTMLAnchorElement) void refreshLocalAttachmentExistence(link);
@@ -106,6 +181,12 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
}
window.__mnoteRefreshEditorLocalAttachmentExistence = refreshEditorLocalAttachmentExistence;
function scheduleEditorLocalAttachmentExistenceRefresh() {
[120, 500, 1200].forEach(function(delayMs) {
window.setTimeout(refreshEditorLocalAttachmentExistence, delayMs);
});
}
function fileNameFromPath(path) {
var value = String(path || '').trim();
return value.indexOf('/') >= 0 ? value.split('/').pop() : value;
@@ -225,20 +306,21 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|| isOfficeFileName(fileName)
|| Boolean(localFilePath);
if (!shouldEnhance) return;
link.setAttribute('data-mnote-attachment-link', 'true');
setAttributeIfChanged(link, 'data-mnote-attachment-link', 'true');
var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || (localFilePath ? 'local-file:' + localFilePath : '');
if (assetId) link.setAttribute('data-asset-id', assetId);
if (assetId) setAttributeIfChanged(link, 'data-asset-id', assetId);
attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
if (name) link.classList.add(name);
});
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer nofollow');
setAttributeIfChanged(link, 'target', '_blank');
setAttributeIfChanged(link, 'rel', 'noopener noreferrer nofollow');
if (localFilePath && !isOnlyOfficeAttachmentHref(href)) {
setEditorAttachmentMissingState(link, editorAttachmentMissingByKey.has(localFileOpenKeyFromHref(href)));
void refreshLocalAttachmentExistence(link);
return;
}
if (isOnlyOfficeAttachmentHref(href)) {
link.setAttribute('href', buildOnlyOfficeOpenPath({
setAttributeIfChanged(link, 'href', buildOnlyOfficeOpenPath({
fileUrl: params.get('fileUrl') || '',
fileName: fileName || '未命名附件',
fileType: params.get('fileType') || inferOnlyOfficeFileType(fileName, ''),
@@ -265,6 +347,14 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
enhanceEditorAttachmentLinks();
if (attachmentInitialEnhanceAttempts >= 120) window.clearInterval(attachmentInitialEnhanceTimer);
}, 500);
var attachmentExistenceRefreshTimer = window.setInterval(function() {
refreshEditorLocalAttachmentExistence();
}, 2500);
window.addEventListener('beforeunload', function() {
if (attachmentExistenceRefreshTimer) window.clearInterval(attachmentExistenceRefreshTimer);
attachmentExistenceRefreshTimer = 0;
});
var lastEditorAttachmentMouseOpen = { at: 0, href: '' };
function ensureAttachmentActions() {
var existing = document.querySelector('[data-testid="mnote-attachment-actions"]');
@@ -308,18 +398,21 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
var localFilePath = localFilePathFromAssetId(detail.assetId);
if (localFilePath) {
if (await openLocalOfficeFileInActiveTab(detail, 'view')) return;
var localFileName = detail.fileName || localFilePath.split('/').pop() || localFilePath;
var localFileUrl = buildLocalFileOpenUrl(localFilePath, false);
var localOpenUrl = isPdfAttachmentFileName(localFileName) ? buildPdfPreviewOpenUrl(localFileUrl, localFileName) : localFileUrl;
// 非 Office 本地文件:用对应图标类型打开 active tab,失败则新窗口
void openLocalResourceInActiveTab({
path: localFilePath,
title: detail.fileName || localFilePath.split('/').pop() || localFilePath,
kind: fileTreeIconKindForFileName(detail.fileName || localFilePath.split('/').pop() || localFilePath),
title: localFileName,
kind: fileTreeIconKindForFileName(localFileName),
assetId: detail.assetId,
documentId: detail.documentId || currentDocumentId() || '',
workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '',
href: buildLocalFileOpenUrl(localFilePath, false),
href: localOpenUrl,
paneRole: detail.paneRole || 'primary'
}).then(function(opened) {
if (!opened) window.open(buildLocalFileOpenUrl(localFilePath, false) || detail.href, '_blank', 'noopener,noreferrer');
if (!opened) window.open(localOpenUrl || detail.href, '_blank', 'noopener,noreferrer');
});
return;
}
@@ -357,7 +450,8 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
var localFileName = localFilePath.split('/').pop() || localFilePath;
var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, requestedMode);
var localFileUrl = buildLocalFileOpenUrl(localFilePath, false);
window.open(localOfficeUrl || localFileUrl || detail.href, '_blank', 'noopener,noreferrer');
var localPreviewUrl = isPdfAttachmentFileName(localFileName) ? buildPdfPreviewOpenUrl(localFileUrl, localFileName) : '';
window.open(localOfficeUrl || localPreviewUrl || localFileUrl || detail.href, '_blank', 'noopener,noreferrer');
return;
}
var href = detail.href || detail.fileUrl;
@@ -464,7 +558,23 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
async function openPdfEditorAttachment(detail) {
try {
var resolved = await resolveEditorAttachmentUrl(detail);
window.open(resolved.url, '_blank', 'noopener,noreferrer');
var title = String(detail.fileName || resolved.asset.file_name || 'PDF').trim() || 'PDF';
var previewUrl = buildPdfPreviewOpenUrl(resolved.url, title);
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
var didOpenPdfTab = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:pdf:' + (detail.documentId || '') + ':' + (detail.assetId || previewUrl || resolved.url),
assetId: detail.assetId || '',
title: title,
fileName: title,
kind: 'pdf',
href: previewUrl,
documentId: detail.documentId || '',
workspaceId: detail.workspaceId || '',
paneRole: detail.paneRole || 'primary'
});
if (didOpenPdfTab) return;
}
window.open(previewUrl || resolved.url, '_blank', 'noopener,noreferrer');
} catch (error) {
window.alert(error && error.message ? error.message : '打开 PDF 失败');
}
@@ -612,6 +722,8 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
window.__mnoteEnhanceEditorAttachmentLinks();
scheduleEditorAttachmentEnhance();
});
window.addEventListener('tree:delta', scheduleEditorLocalAttachmentExistenceRefresh);
window.addEventListener('tree:resync', scheduleEditorLocalAttachmentExistenceRefresh);
[
'mnote:leptos-tiptap-spike:ready',
'mnote:leptos-tiptap-spike:change',
@@ -634,27 +746,35 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
});
function interceptEditorAttachmentLink(event) {
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var editorAttachmentLink = closestAction(event.target, editorAttachmentLinkSelector);
if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return;
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
var href = editorAttachmentLink.href || editorAttachmentLink.getAttribute('href') || '';
if (lastEditorAttachmentMouseOpen.href === href && Date.now() - lastEditorAttachmentMouseOpen.at < 900) return;
openEditorAttachmentLink(editorAttachmentLink);
}
function suppressEditorAttachmentLinkDefault(event) {
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var editorAttachmentLink = closestAction(event.target, editorAttachmentLinkSelector);
if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return;
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
if (Number(event.button || 0) !== 0) return;
lastEditorAttachmentMouseOpen = {
at: Date.now(),
href: editorAttachmentLink.href || editorAttachmentLink.getAttribute('href') || ''
};
openEditorAttachmentLink(editorAttachmentLink);
}
window.addEventListener('mousedown', suppressEditorAttachmentLinkDefault, true);
window.addEventListener('click', interceptEditorAttachmentLink, true);
document.addEventListener('mouseover', function(event) {
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var link = closestAction(event.target, editorAttachmentLinkSelector);
if (!(link instanceof HTMLAnchorElement)) return;
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
enhanceEditorAttachmentLink(link);
@@ -662,7 +782,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
});
document.addEventListener('mouseout', function(event) {
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var link = closestAction(event.target, editorAttachmentLinkSelector);
if (!(link instanceof HTMLAnchorElement)) return;
var next = event.relatedTarget;
var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
@@ -330,13 +330,18 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
}
function fileTreeCopyLocalAbsolutePath(detail, trigger) {
var rootUri = typeof currentRootUri === 'function' ? currentRootUri() : '';
var workspacePath = detail && detail.workspacePath && typeof detail.workspacePath === 'object' ? detail.workspacePath : null;
var rootUri = String(workspacePath && workspacePath.rootUri || '').trim() || (typeof currentRootUri === 'function' ? currentRootUri() : '');
if (!rootUri && trigger && typeof trigger.closest === 'function') {
var row = trigger.closest('.tree-row[data-shell-mode="filetree"]');
if (row instanceof HTMLElement) rootUri = row.getAttribute('data-root-uri') || '';
}
if (!rootUri && document.body) rootUri = document.body.getAttribute('data-mnote-root-uri') || '';
var relativePath = String(detail && detail.localRelativePath || '').trim();
var relativePath = String(
workspacePath && (workspacePath.relativePath || workspacePath.localRelativePath)
|| detail && detail.localRelativePath
|| ''
).trim();
if (!relativePath && detail && detail.assetId) relativePath = localFilePathFromAssetId(detail.assetId);
if (!relativePath && detail && detail.documentId) {
relativePath = String(detail.documentId || '')
@@ -609,8 +614,13 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
button.setAttribute('role', 'menuitem');
button.setAttribute('data-action', item.action);
button.setAttribute('data-command-id', item.commandId || item.action || '');
if (item.when) button.setAttribute('data-command-when', item.when);
if (item.danger || item.destructive) button.setAttribute('data-destructive', 'true');
if (item.requiresApproval) button.setAttribute('data-requires-approval', 'true');
button.disabled = item.disabled === true;
if (item.title) button.title = item.title;
if (item.disabled === true && item.title) button.setAttribute('data-disabled-reason', item.title);
var icon = document.createElement('span');
icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
icon.setAttribute('aria-hidden', 'true');
@@ -636,10 +646,30 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
function buildSidebarFileTreeContext(kind) {
function currentOpenEditorsSnapshotForCommandContext() {
try {
if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === 'function') {
return window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot();
}
} catch (_) {}
return window.__mnoteOpenEditorsSnapshot || null;
}
function commandContextTargetRow(detail) {
if (detail && detail.targetRow instanceof HTMLElement) return detail.targetRow;
var rowId = String(detail && detail.rowId || '').trim();
if (rowId) {
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
if (row instanceof HTMLElement) return row;
}
return null;
}
function buildSidebarFileTreeContext(kind, detail) {
var runtime = window.__mnoteFileTreeContextMenuRuntime;
if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') {
try {
var targetRow = commandContextTargetRow(detail);
return runtime.buildSidebarFileTreeContext(kind, {
selection: sidebarFileTreeSelection,
currentSourceKind: currentSourceKind,
@@ -648,7 +678,10 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
},
queryRowById: function(rowId) {
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
}
},
targetRow: targetRow,
openEditorsSnapshot: currentOpenEditorsSnapshotForCommandContext(),
bufferState: detail && detail.bufferState,
});
} catch (_) {}
}
@@ -800,10 +833,17 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
var isAttachment = kind === 'attachment';
var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
var isFileTreeDownload = kind === 'filetree' && detail.downloadable;
var ctx = buildSidebarFileTreeContext(kind);
var ctx = buildSidebarFileTreeContext(kind, detail);
menu.setAttribute('data-command-context-source-kind', String(ctx['workspace.sourceKind'] || ''));
menu.setAttribute('data-command-context-readonly', String(ctx['workspace.readonly'] === true));
menu.setAttribute('data-command-context-selection-count', String(ctx['tree.selectionCount'] || 0));
menu.setAttribute('data-command-context-resource-kind', String(ctx['tree.selectionResourceKind'] || ''));
menu.setAttribute('data-command-context-target-kind', String(ctx['tree.targetResourceKind'] || ''));
menu.setAttribute('data-command-context-editor-dirty', String(ctx['editor.dirty'] === true));
menu.setAttribute('data-command-context-ai-can-write', String(ctx['ai.canWrite'] === true));
var items = isAttachment ? [
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制链接' },
{ action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' },
@@ -825,9 +865,9 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' },
{ action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' },
{ action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' 个项目' : '下载' },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly && !editor.dirty' },
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' },
{ separator: true },
{ action: 'copy-path', icon: 'content_copy', label: 'Copy Path' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
@@ -849,9 +889,9 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
{ action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly && !editor.dirty' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' }
] : [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ separator: true },
@@ -859,13 +899,18 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名', when: '!workspace.readonly' },
{ action: 'rename', icon: 'edit', label: '重命名', when: '!workspace.readonly && !editor.dirty' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' }
];
items.forEach(function(item) {
if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) {
item = Object.assign({}, item, { disabled: true, title: item.title || '当前上下文不支持此操作' });
var reason = ctx['workspace.readonly'] === true
? '当前工作区只读'
: ctx['editor.dirty'] === true
? '当前目标有未保存修改'
: '当前上下文不支持此操作';
item = Object.assign({}, item, { disabled: true, title: item.title || reason });
}
appendTreeContextMenuButton(menu, item, detail, trigger);
});
@@ -895,12 +940,15 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row);
var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow);
var workspacePath = readWorkspacePathFromRow(row);
openTreeContextMenu('filetree', {
documentId: documentId,
rowId: row.getAttribute('data-row-id') || '',
rowKind: row.getAttribute('data-row-kind') || '',
assetId: row.getAttribute('data-asset-id') || '',
localRelativePath: fileTreeRowLocalRelativePath(row),
workspacePath: workspacePath,
targetRow: row,
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row),
downloadable: isFileTreeDownloadableRow(row),
@@ -1243,6 +1291,32 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
}
function readWorkspacePathFromRow(row) {
var runtimeFn = fileTreeRuntimeFunction('readWorkspacePathFromRow');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return null;
var relativePath = fileTreeRowLocalRelativePath(row);
return {
schema: 'mnote.workspace_path.v1',
workspaceId: resolveWorkspaceId(row),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
relativePath: relativePath,
localRelativePath: relativePath,
objectIdentity: null,
objectIdentityRaw: row.getAttribute('data-object-identity') || '',
objectKind: row.getAttribute('data-object-kind') || '',
resourceKind: row.getAttribute('data-object-kind') || fileTreeRowKind(row),
rowId: row.getAttribute('data-row-id') || '',
rowKind: fileTreeRowKind(row),
documentId: fileTreeRowDocumentId(row),
assetId: fileTreeRowAssetId(row),
title: rowTitle(row),
href: '',
isLocalFolder: currentSourceKind() === 'local_folder'
};
}
function fileTreeRowKind(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind');
if (runtimeFn) return runtimeFn(row);
@@ -1281,6 +1355,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
rowKind: fileTreeRowKind(row),
assetId: assetId,
localRelativePath: relativePath,
workspacePath: readWorkspacePathFromRow(row),
title: title,
fileName: title,
workspaceId: resolveWorkspaceId(row)
@@ -1728,6 +1803,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
decodeLocalEncodedPath,
fileTreeRowLocalRelativePath,
fileTreeRowLocalUploadTargetRelativePath,
readWorkspacePathFromRow,
fileTreeRowKind,
isFileTreeDownloadableAssetRow,
isFileTreeDownloadableRow,
@@ -3,16 +3,19 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
copyWorkspaceSourceParams,
currentDocumentId,
currentRootUri,
currentSourceKind,
fileTreeIconKindForFileName,
getNavigationInFlight,
isCodeAttachmentFileName,
isNonOfficeAttachmentName,
isPdfAttachmentFileName,
openCodeEditorAttachment,
resolveWorkspaceId,
setNavigationInFlight,
shouldOpenLocalResourceInNewWindow,
uploadedFileSize,
} = dependencies;
const currentSourceKindFn = typeof currentSourceKind === 'function' ? currentSourceKind : function() { return ''; };
function inferOnlyOfficeFileType(fileName, mimeType) {
var name = String(fileName || '').trim().toLowerCase();
@@ -29,6 +32,9 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
}
function buildOnlyOfficeOpenUrl(input) {
if (shouldUseOfficePreview(input.fileType, input.mode)) {
return buildOfficePreviewOpenUrl(input);
}
var target = new URL('/onlyoffice', window.location.origin);
var fileUrl = String(input.fileUrl || '').trim();
if (fileUrl) {
@@ -46,11 +52,49 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
return target.toString();
}
function shouldUseOfficePreview(fileType, mode) {
var normalizedMode = String(mode || 'view').trim().toLowerCase();
if (normalizedMode === 'edit') return false;
var ext = String(fileType || '').trim().toLowerCase();
return ['docx', 'xlsx', 'xls', 'csv', 'pptx'].indexOf(ext) >= 0;
}
function buildOfficePreviewOpenUrl(input) {
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.buildOfficePreviewOpenUrl === 'function') {
return _rto_.buildOfficePreviewOpenUrl(input);
}
var target = new URL('/office-preview', window.location.origin);
var workspacePath = input && input.workspacePath && typeof input.workspacePath === 'object' ? input.workspacePath : {};
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);
if (input.documentId) target.searchParams.set('documentId', input.documentId);
if (input.userId) target.searchParams.set('userId', input.userId);
var workspaceId = String(input.workspaceId || workspacePath.workspaceId || '').trim();
var sourceKind = String(input.sourceKind || workspacePath.sourceKind || currentSourceKindFn() || '').trim();
var rootUri = String(input.rootUri || workspacePath.rootUri || currentRootUri() || '').trim();
if (workspaceId) target.searchParams.set('workspaceId', workspaceId);
if (sourceKind) target.searchParams.set('sourceKind', sourceKind);
if (rootUri) target.searchParams.set('rootUri', rootUri);
return target.toString();
}
function buildOnlyOfficeOpenPath(input) {
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.buildOnlyOfficeOpenPath === 'function') {
return _rto_.buildOnlyOfficeOpenPath(input);
}
if (shouldUseOfficePreview(input.fileType, input.mode)) {
return buildOfficePreviewOpenUrl(input);
}
var params = new URLSearchParams();
params.set('fileUrl', input.fileUrl || '');
params.set('fileName', input.fileName || '未命名资源');
@@ -77,12 +121,17 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
assetId: assetId || ('local-file:' + relativePath),
documentId: documentId || currentDocumentId() || '',
userId: '',
mode: mode || 'view'
mode: mode || 'view',
workspaceId: resolveWorkspaceId(document.body) || '',
sourceKind: currentSourceKindFn() || 'local_folder',
rootUri: currentRootUri() || ''
});
}
async function openLocalOfficeFileInActiveTab(detail, mode) {
var localFilePath = localFilePathFromAssetId(detail.assetId);
var workspacePath = workspacePathFromDetail(detail);
if (!localFilePath && workspacePath) localFilePath = String(workspacePath.relativePath || workspacePath.localRelativePath || '').trim();
if (!localFilePath) return false;
var localFileName = localFilePath.split('/').pop() || localFilePath;
var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, mode || 'view');
@@ -96,6 +145,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '',
href: buildLocalFileOpenUrl(localFilePath, false),
officeUrl: localOfficeUrl,
workspacePath: workspacePath,
paneRole: detail.paneRole || 'primary'
});
if (!opened) window.open(localOfficeUrl, '_blank', 'noopener,noreferrer');
@@ -168,6 +218,36 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length) : '';
}
function workspacePathFromDetail(detail) {
var workspacePath = detail && detail.workspacePath && typeof detail.workspacePath === 'object' ? detail.workspacePath : null;
return workspacePath && workspacePath.schema === 'mnote.workspace_path.v1' ? workspacePath : null;
}
function workspacePathObjectIdentity(workspacePath) {
var objectIdentity = workspacePath && workspacePath.objectIdentity && typeof workspacePath.objectIdentity === 'object'
? workspacePath.objectIdentity
: null;
return objectIdentity || {};
}
function resourceObjectIdentityFromWorkspacePath(input) {
input = input || {};
var workspacePath = input.workspacePath && typeof input.workspacePath === 'object' ? input.workspacePath : null;
var structuredIdentity = workspacePathObjectIdentity(workspacePath);
var objectKind = String(input.objectKind || workspacePath && (workspacePath.objectKind || workspacePath.resourceKind) || structuredIdentity.objectKind || '').trim();
var documentId = String(input.documentId || workspacePath && workspacePath.documentId || structuredIdentity.documentId || currentDocumentId() || '').trim();
var assetId = String(input.assetId || workspacePath && workspacePath.assetId || structuredIdentity.assetId || '').trim();
var relativePath = String(input.path || workspacePath && (workspacePath.relativePath || workspacePath.localRelativePath) || '').trim();
var rootUri = String(input.rootUri || workspacePath && workspacePath.rootUri || currentRootUri() || '').trim();
var fallback = String(input.fallback || '').trim();
if (objectKind === 'mindmap' && assetId) return 'resource:mindmap:' + documentId + ':' + assetId;
if ((objectKind === 'only_office' || objectKind === 'office') && assetId) return 'resource:onlyoffice:' + documentId + ':' + assetId;
if (objectKind === 'pdf' && assetId) return 'resource:pdf:' + documentId + ':' + assetId;
if (relativePath && rootUri) return 'resource:file:' + rootUri + ':' + relativePath;
if (assetId && documentId) return 'resource:' + (objectKind || 'attachment') + ':' + documentId + ':' + assetId;
return fallback || assetId || '';
}
function buildLocalFileOpenUrl(relativePath, download) {
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.buildLocalFileOpenUrl === 'function') {
@@ -182,6 +262,19 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
return url.toString();
}
function buildPdfPreviewOpenUrl(fileUrl, fileName) {
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.buildPdfPreviewOpenUrl === 'function') {
return _rto_.buildPdfPreviewOpenUrl(fileUrl, fileName);
}
var rawUrl = String(fileUrl || '').trim();
if (!rawUrl) return '';
var url = new URL('/pdf-preview', window.location.origin);
url.searchParams.set('fileUrl', rawUrl);
if (fileName) url.searchParams.set('fileName', fileName);
return url.toString();
}
async function openLocalResourceInActiveTab(input) {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab !== 'function') return false;
var relativePath = String(input && input.path || '').trim();
@@ -189,7 +282,15 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
if (!relativePath || !rootUri) return false;
var title = String(input && input.title || '').trim() || relativePath.split('/').pop() || relativePath;
var kind = String(input && input.kind || fileTreeIconKindForFileName(title) || '').trim();
var objectIdentity = 'resource:file:' + rootUri + ':' + relativePath;
var workspacePath = input && input.workspacePath && typeof input.workspacePath === 'object' ? input.workspacePath : null;
var objectIdentity = resourceObjectIdentityFromWorkspacePath({
workspacePath: workspacePath,
objectKind: kind,
documentId: input && input.documentId,
assetId: input && input.assetId,
path: relativePath,
rootUri: rootUri
});
return await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: objectIdentity,
assetId: String(input && input.assetId || '').trim() || 'local-file:' + relativePath,
@@ -202,6 +303,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
officeUrl: String(input && input.officeUrl || '').trim(),
documentId: String(input && input.documentId || currentDocumentId() || '').trim(),
workspaceId: String(input && input.workspaceId || resolveWorkspaceId(document.body) || '').trim(),
workspacePath: workspacePath,
paneRole: String(input && input.paneRole || 'primary').trim() || 'primary'
});
}
@@ -240,15 +342,26 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
var forceEditMode = openTarget === 'edit-mode';
if (openTarget === 'side') {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceAsSideTarget === 'function') {
var sideWorkspacePath = workspacePathFromDetail(detail);
var sideLocalFilePath = localFilePathFromAssetId(assetId);
if (!sideLocalFilePath && sideWorkspacePath) sideLocalFilePath = String(sideWorkspacePath.relativePath || sideWorkspacePath.localRelativePath || '').trim();
var sideRootUri = String(detail.rootUri || currentRootUri() || '').trim();
var sideFileName = String(detail.title || detail.fileName || (sideLocalFilePath ? sideLocalFilePath.split('/').pop() : '') || assetId || '').trim();
var sideKind = String(detail.iconKind || detail.assetType || fileTreeIconKindForFileName(sideFileName) || 'file').trim();
var sideHref = sideLocalFilePath ? buildLocalFileOpenUrl(sideLocalFilePath, false) : '';
var sideOfficeUrl = sideLocalFilePath ? buildLocalOnlyOfficeOpenUrl(sideLocalFilePath, sideFileName, String(detail.documentId || currentDocumentId() || '').trim(), assetId, 'view') : '';
if (sideOfficeUrl) sideKind = 'office';
if (!sideOfficeUrl && isPdfAttachmentFileName(sideFileName)) sideHref = buildPdfPreviewOpenUrl(sideHref, sideFileName);
void window.__mnoteDocumentPaneRuntime.openResourceAsSideTarget({
objectIdentity: sideLocalFilePath && sideRootUri ? 'resource:file:' + sideRootUri + ':' + sideLocalFilePath : String(detail.objectIdentity || detail.assetId || ''),
objectIdentity: resourceObjectIdentityFromWorkspacePath({
workspacePath: sideWorkspacePath,
objectKind: sideKind,
documentId: detail.documentId,
assetId: assetId,
path: sideLocalFilePath,
rootUri: sideRootUri,
fallback: String(detail.objectIdentity || detail.assetId || '')
}),
assetId: assetId,
title: sideFileName,
fileName: sideFileName,
@@ -258,12 +371,15 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
href: sideHref,
officeUrl: sideOfficeUrl,
documentId: String(detail.documentId || currentDocumentId() || ''),
workspaceId: String(detail.workspaceId || resolveWorkspaceId(document.body) || '')
workspaceId: String(detail.workspaceId || resolveWorkspaceId(document.body) || ''),
workspacePath: sideWorkspacePath
});
}
return;
}
var detailWorkspacePath = workspacePathFromDetail(detail);
var localFilePath = localFilePathFromAssetId(assetId);
if (!localFilePath && detailWorkspacePath) localFilePath = String(detailWorkspacePath.relativePath || detailWorkspacePath.localRelativePath || '').trim();
if (localFilePath) {
var localFileName = localFilePath.split('/').pop() || localFilePath;
if (isMindmapAssetDetail(detail) || String(detail && detail.assetType || '').trim() === 'mindmap') {
@@ -271,14 +387,21 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:mindmap:' + String(detail && detail.documentId || currentDocumentId() || '').trim() + ':' + assetId,
objectIdentity: resourceObjectIdentityFromWorkspacePath({
workspacePath: detailWorkspacePath,
objectKind: 'mindmap',
documentId: detail && detail.documentId,
assetId: assetId,
path: localFilePath
}),
assetId: assetId,
mindmapId: assetId,
title: String(detail && detail.title || localFileName || '思维导图').trim(),
fileName: localFileName,
kind: 'mindmap',
documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(),
workspaceId: String(detail.workspaceId || '').trim()
workspaceId: String(detail.workspaceId || '').trim(),
workspacePath: detailWorkspacePath
});
return;
}
@@ -297,13 +420,15 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
assetId: assetId,
documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(),
workspaceId: String(detail.workspaceId || '').trim(),
officeUrl: localOfficeUrl
officeUrl: localOfficeUrl,
workspacePath: detailWorkspacePath
})) return;
window.open(localOfficeUrl, '_blank', 'noopener,noreferrer');
return;
}
var localFileUrl = buildLocalFileOpenUrl(localFilePath, false);
if (localFileUrl) {
var localOpenUrl = isPdfAttachmentFileName(localFileName) ? buildPdfPreviewOpenUrl(localFileUrl, localFileName) : localFileUrl;
var shouldOpenInNewWindow = forceNewWindow || shouldOpenLocalResourceInNewWindow(localFileName);
if (!shouldOpenInNewWindow && await openLocalResourceInActiveTab({
path: localFilePath,
@@ -312,9 +437,10 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
assetId: assetId,
documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(),
workspaceId: String(detail.workspaceId || '').trim(),
href: localFileUrl
href: localOpenUrl,
workspacePath: detailWorkspacePath
})) return;
window.open(localFileUrl, '_blank', 'noopener,noreferrer');
window.open(localOpenUrl, '_blank', 'noopener,noreferrer');
}
return;
}
@@ -324,14 +450,20 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:mindmap:' + documentId + ':' + assetId,
objectIdentity: resourceObjectIdentityFromWorkspacePath({
workspacePath: detailWorkspacePath,
objectKind: 'mindmap',
documentId: documentId,
assetId: assetId
}),
assetId: assetId,
mindmapId: assetId,
title: String(detail.title || '思维导图').trim(),
fileName: String(detail.title || '思维导图').trim(),
kind: 'mindmap',
documentId: documentId,
workspaceId: String(detail.workspaceId || '').trim()
workspaceId: String(detail.workspaceId || '').trim(),
workspacePath: detailWorkspacePath
});
return;
}
@@ -367,20 +499,50 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
});
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
var didOpen = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:onlyoffice:' + String(asset.document_id || detail.documentId || '').trim() + ':' + assetId,
objectIdentity: resourceObjectIdentityFromWorkspacePath({
workspacePath: detailWorkspacePath,
objectKind: 'only_office',
documentId: String(asset.document_id || detail.documentId || '').trim(),
assetId: assetId
}),
assetId: assetId,
title: fileName,
fileName: fileName,
kind: 'office',
officeUrl: officeUrl,
documentId: String(asset.document_id || detail.documentId || '').trim(),
workspaceId: String(detail.workspaceId || '').trim()
workspaceId: String(detail.workspaceId || '').trim(),
workspacePath: detailWorkspacePath
});
if (didOpen) return;
}
window.open(officeUrl, '_blank', 'noopener,noreferrer');
return;
}
if (isPdfAttachmentFileName(fileName)) {
var pdfPreviewUrl = buildPdfPreviewOpenUrl(fileUrl, fileName);
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
var didOpenPdf = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: resourceObjectIdentityFromWorkspacePath({
workspacePath: detailWorkspacePath,
objectKind: 'pdf',
documentId: String(asset.document_id || detail.documentId || '').trim(),
assetId: assetId
}),
assetId: assetId,
title: fileName,
fileName: fileName,
kind: 'pdf',
href: pdfPreviewUrl,
documentId: String(asset.document_id || detail.documentId || '').trim(),
workspaceId: String(detail.workspaceId || '').trim(),
workspacePath: detailWorkspacePath
});
if (didOpenPdf) return;
}
window.open(pdfPreviewUrl || fileUrl, '_blank', 'noopener,noreferrer');
return;
}
if (isCodeAttachmentFileName(fileName) && String(asset.document_id || detail.documentId || '').trim() === currentDocumentId()) {
await openCodeEditorAttachment({
href: fileUrl,
@@ -407,8 +569,10 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
buildLocalFileOpenUrl,
buildLocalOnlyOfficeOpenUrl,
buildMindmapOpenPath,
buildOfficePreviewOpenUrl,
buildOnlyOfficeOpenPath,
buildOnlyOfficeOpenUrl,
buildPdfPreviewOpenUrl,
fetchCurrentOnlyOfficeUserId,
inferOnlyOfficeFileType,
isMindmapAssetDetail,
@@ -418,5 +582,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
openLocalOfficeFileInActiveTab,
openLocalResourceInActiveTab,
readFileTreeObjectIdentity,
resourceObjectIdentityFromWorkspacePath,
workspacePathFromDetail,
};
};
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,25 @@ export function createSidebarPageSettingsRuntime(context) {
searchText,
} = context;
const MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = context.globalShowHeadingNumbersKey || 'mnote.global.showHeadingNumbers';
const PAGE_WIDTH_TYPES = ['default', 'markdown', 'word', 'pdf', 'excel', 'ppt', 'mindmap'];
const PAGE_WIDTH_LABELS = {
default: '默认',
markdown: 'Markdown',
word: 'Word',
pdf: 'PDF',
excel: 'Excel',
ppt: 'PPT',
mindmap: 'Mindmap'
};
const DEFAULT_PAGE_WIDTH_PREFERENCES = {
default: { mode: 'comfortable', resolvedMode: 'comfortable', cssMaxWidth: '980px', source: 'system' },
markdown: { mode: 'readable', resolvedMode: 'readable', cssMaxWidth: '760px', source: 'system' },
word: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
pdf: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
excel: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' },
ppt: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
mindmap: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' }
};
function defaultPageOptions() {
return {
@@ -44,6 +63,93 @@ export function createSidebarPageSettingsRuntime(context) {
return pageUiState.pageOptions;
}
function currentPageWidthPreferences() {
return Object.assign({}, DEFAULT_PAGE_WIDTH_PREFERENCES, pageUiState.pageWidthPreferences || {});
}
function pageWidthModeLabel(mode) {
if (mode === 'inherit') return '继承默认';
if (mode === 'readable') return '阅读';
if (mode === 'comfortable') return '舒适';
if (mode === 'wide') return '宽版';
if (mode === 'full') return '全宽';
return '舒适';
}
function normalizePageWidthPreferences(value) {
var input = value && typeof value === 'object' ? value : {};
var output = {};
PAGE_WIDTH_TYPES.forEach(function(type) {
var base = DEFAULT_PAGE_WIDTH_PREFERENCES[type];
var current = input[type] && typeof input[type] === 'object' ? input[type] : {};
var mode = String(current.mode || base.mode || 'comfortable');
var resolvedMode = String(current.resolvedMode || current.resolved_mode || (mode === 'inherit' ? (input.default && input.default.resolvedMode) || 'comfortable' : mode));
output[type] = {
mode: mode,
custom: current.custom || null,
resolvedMode: resolvedMode,
resolvedCustom: current.resolvedCustom || current.resolved_custom || null,
cssMaxWidth: String(current.cssMaxWidth || current.css_max_width || pageWidthCssMaxWidth(resolvedMode)),
source: String(current.source || base.source || 'system')
};
});
return output;
}
function pageWidthCssMaxWidth(mode) {
if (mode === 'readable') return '760px';
if (mode === 'comfortable') return '980px';
if (mode === 'wide') return '1180px';
if (mode === 'full') return 'none';
return '980px';
}
function markdownPageWidthPreference(options) {
var preferences = currentPageWidthPreferences();
var markdown = preferences.markdown || DEFAULT_PAGE_WIDTH_PREFERENCES.markdown;
if (markdown.source === 'system' && options && options.wideLayout) {
return {
mode: 'comfortable',
resolvedMode: 'comfortable',
cssMaxWidth: '980px',
source: 'wideLayout'
};
}
return markdown;
}
function activeResourceWidthType() {
var raw = document.documentElement.getAttribute('data-mnote-active-resource-width-type') || '';
var type = String(raw).trim();
if (type === 'sheet') return 'excel';
if (PAGE_WIDTH_TYPES.indexOf(type) >= 0 && type !== 'default') return type;
return '';
}
function activeResourceWidthPreference(options) {
var type = activeResourceWidthType();
if (!type || type === 'markdown') return markdownPageWidthPreference(options);
var preferences = currentPageWidthPreferences();
return preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
}
function notifyPageWidthPreferenceChanged(type) {
var detail = {
type: String(type || ''),
preferences: currentPageWidthPreferences()
};
window.dispatchEvent(new CustomEvent('mnote:page-width-preference-changed', { detail: detail }));
document.querySelectorAll('iframe.mnote-resource-tab-frame').forEach(function(frame) {
if (!(frame instanceof HTMLIFrameElement) || !frame.contentWindow) return;
try {
frame.contentWindow.postMessage({
type: 'mnote:page-width-preference-changed',
contentType: detail.type
}, window.location.origin);
} catch (_) {}
});
}
function readGlobalShowHeadingNumbers() {
return Boolean(currentPageOptions().showHeadingNumbers);
}
@@ -157,13 +263,19 @@ export function createSidebarPageSettingsRuntime(context) {
var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
if (shell instanceof HTMLElement) {
var widthPreference = activeResourceWidthPreference(options);
var cssMaxWidth = String(widthPreference.cssMaxWidth || pageWidthCssMaxWidth(widthPreference.resolvedMode));
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
shell.setAttribute('data-page-width-content-type', activeResourceWidthType() || 'markdown');
shell.setAttribute('data-page-width-mode', String(widthPreference.mode || 'readable'));
shell.setAttribute('data-page-width-resolved-mode', String(widthPreference.resolvedMode || widthPreference.mode || 'readable'));
shell.setAttribute('data-page-width-source', String(widthPreference.source || 'system'));
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(showHeadingNumbers));
shell.style.width = '100%';
shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
shell.style.maxWidth = cssMaxWidth === 'none' ? 'none' : cssMaxWidth;
}
if (editorRoot instanceof HTMLElement) {
editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
@@ -177,6 +289,10 @@ export function createSidebarPageSettingsRuntime(context) {
editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
}
document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
var rootWidthPreference = activeResourceWidthPreference(options);
document.documentElement.setAttribute('data-page-width-content-type', activeResourceWidthType() || 'markdown');
document.documentElement.setAttribute('data-page-width-mode', String(rootWidthPreference.mode || 'readable'));
document.documentElement.setAttribute('data-page-width-resolved-mode', String(rootWidthPreference.resolvedMode || rootWidthPreference.mode || 'readable'));
document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
@@ -235,11 +351,58 @@ export function createSidebarPageSettingsRuntime(context) {
'</label>';
}
function createPageWidthSelectRow(type) {
var options = type === 'default'
? [
['readable', '阅读'],
['comfortable', '舒适'],
['wide', '宽版'],
['full', '全宽']
]
: [
['inherit', '继承默认'],
['readable', '阅读'],
['comfortable', '舒适'],
['wide', '宽版'],
['full', '全宽']
];
return '' +
'<label class="wolai-page-setting-row" data-page-width-row="' + type + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(PAGE_WIDTH_LABELS[type] || type) + '</span>' +
'<span class="wolai-page-setting-hint" data-page-width-hint="' + type + '"></span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-width-select="' + type + '">' +
options.map(function(option) {
return '<option value="' + option[0] + '">' + option[1] + '</option>';
}).join('') +
'</select>' +
'</label>';
}
function createPageWidthRows() {
return '' +
'<div class="wolai-page-settings-global-note">页面宽度</div>' +
PAGE_WIDTH_TYPES.map(createPageWidthSelectRow).join('');
}
function renderGlobalOptions(popover) {
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
input.checked = globalHeadingNumbers;
});
var preferences = currentPageWidthPreferences();
popover.querySelectorAll('[data-page-width-select]').forEach(function(select) {
var type = select.getAttribute('data-page-width-select') || '';
var preference = preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
select.value = String(preference.mode || 'comfortable');
});
popover.querySelectorAll('[data-page-width-hint]').forEach(function(node) {
var type = node.getAttribute('data-page-width-hint') || '';
var preference = preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
var resolvedMode = String(preference.resolvedMode || preference.mode || 'comfortable');
node.textContent = pageWidthModeLabel(resolvedMode) + ' · ' + String(preference.cssMaxWidth || pageWidthCssMaxWidth(resolvedMode));
});
}
function createPageFontRow() {
@@ -410,6 +573,7 @@ export function createSidebarPageSettingsRuntime(context) {
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
createGlobalHeadingNumbersRow() +
createPageWidthRows() +
'</div>' +
'<div class="wolai-page-settings-actions">' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
@@ -657,6 +821,71 @@ export function createSidebarPageSettingsRuntime(context) {
}
}
async function loadPageWidthPreferences() {
if (!currentDocumentId()) return;
var params = new URLSearchParams();
params.set('documentId', currentDocumentId());
params.set('workspaceId', resolveWorkspaceId(document.body));
var sourcePayload = currentWorkspaceSourcePayload();
Object.keys(sourcePayload || {}).forEach(function(key) {
if (sourcePayload[key] != null && sourcePayload[key] !== '') params.set(key, sourcePayload[key]);
});
try {
var response = await fetch('/api/ui/preferences/effective?' + params.toString(), {
headers: { accept: 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) return;
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
applyPageOptionsToShell();
if (isPageSettingsOpen()) renderPageSettingsPopover();
} catch (_) {}
}
async function persistPageWidthPreference(type, mode) {
if (PAGE_WIDTH_TYPES.indexOf(type) < 0) return;
var previous = pageUiState.pageWidthPreferences;
var next = normalizePageWidthPreferences(previous);
next[type] = Object.assign({}, next[type], {
mode: mode,
resolvedMode: mode === 'inherit' ? next.default.resolvedMode : mode,
cssMaxWidth: pageWidthCssMaxWidth(mode === 'inherit' ? next.default.resolvedMode : mode),
source: 'optimistic'
});
pageUiState.pageWidthPreferences = next;
applyPageOptionsToShell();
renderPageSettingsPopover();
notifyPageWidthPreferenceChanged(type);
try {
var updates = {};
updates['pageWidth.' + type] = { mode: mode, custom: null };
var response = await fetch('/api/ui/preferences', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
updates: updates
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_width_save_failed_' + response.status);
}
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
applyPageOptionsToShell();
renderPageSettingsPopover();
notifyPageWidthPreferenceChanged(type);
document.documentElement.setAttribute('data-mnote-page-width-saved', 'true');
} catch (error) {
pageUiState.pageWidthPreferences = previous;
applyPageOptionsToShell();
renderPageSettingsPopover();
document.documentElement.setAttribute('data-mnote-page-width-error', error instanceof Error ? error.message : String(error));
}
}
function isPageSettingsOpen() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
@@ -684,6 +913,10 @@ export function createSidebarPageSettingsRuntime(context) {
else openPageSettingsPopover();
}
window.addEventListener('mnote:active-resource-tab-changed', function() {
applyPageOptionsToShell();
});
return {
applyPageOptionsToShell,
closePageHistoryDrawer,
@@ -700,11 +933,13 @@ export function createSidebarPageSettingsRuntime(context) {
openPageShareDialog,
pageOptionIsSupported,
persistPageOptionsPatch,
persistPageWidthPreference,
recordPageHistorySnapshot,
renderPageSettingsPopover,
setActivePageSettingsTab,
togglePageSettingsPopover,
updatePageSettingsTriggerState,
writeGlobalShowHeadingNumbers,
loadPageWidthPreferences,
};
}
@@ -67,7 +67,7 @@ export const createSidebarPageTreeRuntime = (dependencies = {}) => {
window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
documentId: nodeId,
workspaceId: workspaceId || '',
sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
sourceKind: targetUrl.searchParams.get('sourceKind') || 'local_folder',
rootUri: targetUrl.searchParams.get('rootUri') || '',
url: targetUrl,
}).then(function(){
@@ -418,11 +418,32 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
function applyLocalFolderWatchBatch(payload) {
var batch = payload && (payload.payload || payload);
if (batch && (batch.fallbackResync === true || batch.requiresResync === true)) {
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-fallback', String(batch.revision || 'resync'));
void refreshLocalFolderSidebarSnapshot();
return true;
}
var affectedParents = Array.isArray(batch && batch.affectedParents)
? batch.affectedParents
: Array.isArray(batch && batch.affected_parents)
? batch.affected_parents
: [];
if (!affectedParents.length) {
var changedPaths = Array.isArray(batch && batch.changedPaths)
? batch.changedPaths
: Array.isArray(batch && batch.changed_paths)
? batch.changed_paths
: [];
affectedParents = changedPaths.map(function(item) {
var relativePath = String(item && (item.relativePath || item.relative_path) || '').trim();
return { relativePath: parentRelativePathForPath(relativePath), reason: 'derived-from-changed-path' };
}).filter(function(parent, index, list) {
return index === list.findIndex(function(candidate) { return candidate.relativePath === parent.relativePath; });
});
if (affectedParents.length) {
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-derived-parents', String(affectedParents.length));
}
}
if (!affectedParents.length) {
setTreeLiveApplyError('local_folder_watch_batch_missing_affected_parents');
return false;
@@ -663,7 +684,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
return {
userId: currentActorStorageId(),
workspaceId: String(currentWorkspaceId() || '').trim() || (rootUri ? 'local:' + rootUri.replace(/^file:\/\//, '').replace(/[^A-Za-z0-9]+/g, '_') : 'default'),
sourceKind: String(currentSourceKind() || 'convex_workspace').trim() || 'convex_workspace',
sourceKind: String(currentSourceKind() || 'local_folder').trim() || 'local_folder',
treeKind: normalizedTreeKind,
rootUri: rootUri,
scope: scope
@@ -685,7 +706,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
return SIDEBAR_TREE_VIEW_STATE_KEY
.replace('{userId}', encodeURIComponent(scope.userId || 'anonymous'))
.replace('{workspaceId}', encodeURIComponent(scope.workspaceId || 'default'))
.replace('{sourceKind}', encodeURIComponent(scope.sourceKind || 'convex_workspace'))
.replace('{sourceKind}', encodeURIComponent(scope.sourceKind || 'local_folder'))
.replace('{treeKind}', encodeURIComponent(scope.treeKind || 'filetree'))
.replace('{rootUriHash}', stableTreeViewStateHash(scope.rootUri || ''))
.replace('{scopeHash}', stableTreeViewStateHash(scope.scope || 'root'));
@@ -29,6 +29,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var activeTreeContextMenu = null;
var pageUiState = {
pageOptions: null,
pageWidthPreferences: null,
historySnapshots: [],
pageSettingsOpen: false,
pageAiOpen: false,
@@ -244,12 +245,14 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
const persistPageWidthPreference = (...args) => sidebarPageSettings.persistPageWidthPreference(...args);
const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args);
const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args);
const togglePageSettingsPopover = (...args) => sidebarPageSettings.togglePageSettingsPopover(...args);
const updatePageSettingsTriggerState = (...args) => sidebarPageSettings.updatePageSettingsTriggerState(...args);
const writeGlobalShowHeadingNumbers = (...args) => sidebarPageSettings.writeGlobalShowHeadingNumbers(...args);
const loadPageWidthPreferences = (...args) => sidebarPageSettings.loadPageWidthPreferences(...args);
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
@@ -413,6 +416,57 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return root instanceof HTMLElement ? String(root.getAttribute('data-mnote-filetree-scope') || '').trim() : '';
}
function recordNavigationRecent(payload) {
var body = Object.assign({
sourceKind: currentSourceKind() || 'local_folder',
rootUri: currentRootUri() || '',
}, payload || {});
if (!body.kind || !body.rootUri || !body.title) return Promise.resolve(false);
return fetch('/api/navigation/recent', {
method: 'POST',
credentials: 'include',
headers: {
accept: 'application/json',
'content-type': 'application/json'
},
body: JSON.stringify(body)
}).then(function(response) {
return response.ok;
}).catch(function(error) {
console.warn('[mnote navigation] record recent failed', error);
return false;
});
}
function openNavigationPageForFolder(rootUri, relativePath, workspaceId, title, options) {
rootUri = String(rootUri || '').trim();
relativePath = String(relativePath || '').trim().replace(/^\/+|\/+$/g, '');
workspaceId = String(workspaceId || '').trim();
title = String(title || relativePath || '本地文件夹').trim();
if (!rootUri) return false;
void recordNavigationRecent({
kind: 'folder',
sourceKind: 'local_folder',
rootUri: rootUri,
relativePath: relativePath,
title: title,
workspaceId: workspaceId
});
var targetUrl = new URL('/', window.location.origin);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
targetUrl.searchParams.set('sourceKind', 'local_folder');
targetUrl.searchParams.set('rootUri', rootUri);
targetUrl.searchParams.set('treeView', 'filetree');
if (relativePath) targetUrl.searchParams.set('fileTreeScope', relativePath);
var url = targetUrl.pathname + targetUrl.search;
if (options && options.newTab) {
window.open(url, '_blank', 'noopener');
} else {
window.location.assign(url);
}
return true;
}
function currentTopbarTitle() {
var title = document.querySelector('[data-page-title-current="true"]');
return title && title.textContent ? title.textContent.trim() : '无标题';
@@ -749,6 +803,14 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
pageRoot.setAttribute('data-mnote-page-tree-scope', relativePath);
}
persistStarredFolderScope(workspaceId, rootUri, relativePath);
void recordNavigationRecent({
kind: 'folder',
sourceKind: 'local_folder',
rootUri: rootUri,
relativePath: relativePath,
title: row.textContent ? row.textContent.trim() : relativePath,
workspaceId: workspaceId
});
document.documentElement.setAttribute('data-mnote-filetree-scope', relativePath);
var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin);
sidebarUrl.searchParams.set('workspaceId', workspaceId);
@@ -805,10 +867,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
copyWorkspaceSourceParams,
currentDocumentId,
currentRootUri,
currentSourceKind,
fileTreeIconKindForFileName: (...args) => fileTreeIconKindForFileName(...args),
getNavigationInFlight: () => mnoteNavigationInFlight,
isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args),
isNonOfficeAttachmentName: (...args) => isNonOfficeAttachmentName(...args),
isPdfAttachmentFileName: (...args) => isPdfAttachmentFileName(...args),
openCodeEditorAttachment: (...args) => openCodeEditorAttachment(...args),
resolveWorkspaceId,
setNavigationInFlight: (value) => { mnoteNavigationInFlight = String(value || ''); },
@@ -818,6 +882,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const inferOnlyOfficeFileType = (...args) => sidebarFileTreeOpen.inferOnlyOfficeFileType(...args);
const buildOnlyOfficeOpenUrl = (...args) => sidebarFileTreeOpen.buildOnlyOfficeOpenUrl(...args);
const buildOnlyOfficeOpenPath = (...args) => sidebarFileTreeOpen.buildOnlyOfficeOpenPath(...args);
const buildPdfPreviewOpenUrl = (...args) => sidebarFileTreeOpen.buildPdfPreviewOpenUrl(...args);
const buildLocalOnlyOfficeOpenUrl = (...args) => sidebarFileTreeOpen.buildLocalOnlyOfficeOpenUrl(...args);
const openLocalOfficeFileInActiveTab = (...args) => sidebarFileTreeOpen.openLocalOfficeFileInActiveTab(...args);
const buildMindmapOpenPath = (...args) => sidebarFileTreeOpen.buildMindmapOpenPath(...args);
@@ -925,6 +990,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
if (ext === 'md' || ext === 'markdown') return 'markdown';
if (ext === 'pdf') return 'pdf';
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image';
if (['html', 'htm', 'css', 'scss', 'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'vue', 'svelte', 'astro'].indexOf(ext) >= 0) return 'web';
if (['json', 'jsonc', 'json5', 'toml', 'yaml', 'yml', 'ini', 'env', 'xml', 'lock', 'hcl', 'tf', 'tfvars', 'nix', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop', 'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'].indexOf(ext) >= 0 || ['.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc', 'dockerfile', 'containerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'].indexOf(name) >= 0) return 'config';
if (['rs', 'py', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj', 'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd', 'psm1', 'psd1', 'proto', 'graphql', 'gql', 'prisma', 'cmake', 'bazel', 'bzl'].indexOf(ext) >= 0) return 'code';
return '';
}
@@ -1720,7 +1788,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed');
}
window.setTimeout(function() {
window.setTimeout(function annotateUploadedLink(attempt) {
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
enhanceEditorAttachmentLinks();
var selector = assetId
@@ -1729,12 +1797,22 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var link = targetRoot instanceof HTMLElement
? targetRoot.querySelector(selector)
: document.querySelector(selector);
if (!(link instanceof HTMLElement) && assetId) {
var scope = targetRoot instanceof HTMLElement ? targetRoot : document;
link = Array.from(scope.querySelectorAll('.editor-surface .ProseMirror a')).find(function(candidate) {
var href = String(candidate.getAttribute('href') || '');
try { href = decodeURIComponent(href); } catch (_) {}
return href.indexOf(assetId) >= 0;
}) || null;
}
if (link instanceof HTMLElement) {
link.setAttribute('data-mnote-attachment-link', 'true');
if (assetId) link.setAttribute('data-asset-id', assetId);
if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
} else if (Number(attempt) < 20) {
window.setTimeout(function() { annotateUploadedLink(Number(attempt) + 1); }, 50);
}
}, 0);
}, 0, 0);
return inserted;
}
} catch (error) {
@@ -1801,7 +1879,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options));
}
void refreshLocalFolderSidebarSnapshot();
if (!(options && options.insertIntoEditor)) void refreshLocalFolderSidebarSnapshot();
window.dispatchEvent(new CustomEvent('wolai:local-assets-changed', {
detail: { docId: documentId, asset: localPayload.asset, assetIds: [localPayload.asset.id] }
}));
@@ -1844,6 +1922,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
window.addEventListener('wolai:assets-changed', function(event) {
applyAssetsChangedToFileTree(event.detail || {});
});
window.addEventListener('wolai:local-assets-changed', function(event) {
applyAssetsChangedToFileTree(event.detail || {});
});
installMindmapAssetFetchObserver();
async function uploadFilesWithResolvedTarget(files, detail, options) {
@@ -2022,6 +2103,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const decodeLocalEncodedPath = (...args) => sidebarFileTreeCommand.decodeLocalEncodedPath(...args);
const fileTreeRowLocalRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalRelativePath(...args);
const fileTreeRowLocalUploadTargetRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalUploadTargetRelativePath(...args);
const readWorkspacePathFromRow = (...args) => sidebarFileTreeCommand.readWorkspacePathFromRow(...args);
const fileTreeRowKind = (...args) => sidebarFileTreeCommand.fileTreeRowKind(...args);
const isFileTreeDownloadableAssetRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableAssetRow(...args);
const isFileTreeDownloadableRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableRow(...args);
@@ -2300,6 +2382,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const pageAiLoadSkills = (...args) => sidebarPageAi.pageAiLoadSkills(...args);
const pageAiSwitchProfile = (...args) => sidebarPageAi.pageAiSwitchProfile(...args);
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
const pageAiSetAgentId = (...args) => sidebarPageAi.pageAiSetAgentId(...args);
const pageAiToggleContextRef = (...args) => sidebarPageAi.pageAiToggleContextRef(...args);
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
@@ -2308,12 +2392,14 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
buildLocalOnlyOfficeOpenUrl,
buildOnlyOfficeOpenPath,
buildOnlyOfficeOpenUrl,
buildPdfPreviewOpenUrl,
closestAction,
currentDocumentId,
currentRootUri,
currentWorkspaceSourcePayload,
fileTreeIconKindForFileName,
healLegacyOfficeAttachmentParagraphs,
hydrateEditorAttachmentMeta,
inferCodeAttachmentLanguage: (...args) => inferCodeAttachmentLanguage(...args),
inferOnlyOfficeFileType,
isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args),
@@ -2366,7 +2452,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"], .editor-surface .ProseMirror a[href*="/office-preview"]');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
e.preventDefault();
openEditorAttachmentLink(editorAttachmentLink);
@@ -2503,6 +2589,20 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var pageAiAgent = closestAction(e.target, '[data-page-ai-agent-id]');
if (pageAiAgent) {
e.preventDefault();
pageAiSetAgentId(pageAiAgent.getAttribute('data-page-ai-agent-id') || 'reasonix');
return;
}
var pageAiContextRef = closestAction(e.target, '[data-page-ai-context-ref]');
if (pageAiContextRef) {
e.preventDefault();
pageAiToggleContextRef(pageAiContextRef.getAttribute('data-page-ai-context-ref') || '');
return;
}
var pageAiMemorySave = closestAction(e.target, '[data-page-ai-memory-save]');
if (pageAiMemorySave) {
e.preventDefault();
@@ -2820,14 +2920,24 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
var objectIdentity = readFileTreeObjectIdentity(fileRow);
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
var workspacePath = readWorkspacePathFromRow(fileRow);
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspacePath: workspacePath, workspaceId: resolveWorkspaceId(fileRow) });
if (e.shiftKey || e.ctrlKey || e.metaKey) {
return;
}
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && documentId) {
void recordNavigationRecent({
kind: 'page',
sourceKind: currentSourceKind() || 'local_folder',
rootUri: currentRootUri(),
relativePath: String(fileRow.getAttribute('data-local-relative-path') || '').trim(),
documentId: documentId,
title: fileTreeRowTitleForShortcut(fileRow, documentId),
workspaceId: resolveWorkspaceId(fileRow)
});
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree', fileTreeScope: currentFileTreeScope() });
} else if (assetId) {
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow), openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab' });
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspacePath: workspacePath, workspaceId: resolveWorkspaceId(fileRow), openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab' });
}
return;
}
@@ -2859,7 +2969,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
});
document.addEventListener('contextmenu', function(event) {
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"], .editor-surface .ProseMirror a[href*="/office-preview"]');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
event.preventDefault();
event.stopPropagation();
@@ -2904,6 +3014,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
// inline fallback
if (event.key === 'F2') {
event.preventDefault();
var renameCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(renameCtx, '!workspace.readonly && !editor.dirty')) {
return;
}
beginFileTreeInlineRename(fileTreeRowForKey);
return;
}
@@ -2921,14 +3035,18 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
if (shortcutKey === 'v') {
event.preventDefault();
var pasteCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(pasteCtx, '!workspace.readonly')) {
return;
}
void pasteSidebarFileTreeClipboard(fileTreeRowForKey);
return;
}
}
if (event.key === 'Delete' || event.key === 'Backspace') {
event.preventDefault();
var delCtx = buildSidebarFileTreeContext('filetree');
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')) {
var delCtx = buildSidebarFileTreeContext('filetree', { targetRow: fileTreeRowForKey });
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly && !editor.dirty')) {
return;
}
void deleteSelectedSidebarFileTreeRows(fileTreeRowForKey);
@@ -3044,12 +3162,20 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
if (Object.keys(next).length) {
void persistPageOptionsPatch(next);
}
return;
}
var pageWidthSelect = closestAction(event.target, '[data-page-width-select]');
if (pageWidthSelect instanceof HTMLSelectElement) {
var pageWidthType = pageWidthSelect.getAttribute('data-page-width-select') || '';
void persistPageWidthPreference(pageWidthType, pageWidthSelect.value);
}
});
function initializePageUiSurfaces() {
pageUiState.pageOptions = null;
pageUiState.pageWidthPreferences = null;
applyPageOptionsToShell();
void loadPageWidthPreferences();
updatePageSettingsTriggerState();
updatePageAiTriggerState();
ensureHistorySnapshotsSeeded();
@@ -103,7 +103,7 @@ export const createSidebarWorkspaceRuntime = (dependencies = {}) => {
) {
return 'local_folder';
}
return 'convex_workspace';
return 'local_folder';
}
function currentRootUri() {
@@ -2,7 +2,7 @@
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
var fallback = {
schema: 'mnote.tree_live_bootstrap.v1',
transport: 'convex-command-log-sse',
transport: 'tree-live-sse',
endpoint: '/api/tree/events',
rootIds: [],
initialRevision: null
@@ -34,7 +34,7 @@
}
function applyTransport(transport) {
document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse');
document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'tree-live-sse');
}
function closeActiveSource() {
@@ -169,7 +169,7 @@
}
function startWithSseFallback(bootstrap, workspaceId) {
applyTransport('convex-command-log-sse');
applyTransport('tree-live-sse');
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
url.searchParams.set('workspaceId', workspaceId);
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
@@ -208,9 +208,13 @@
var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
// Prefer WebSocket transport when available
var preferWs = bootstrap.transport === 'convex-command-log-ws' && 'WebSocket' in window;
var requestedTransport = bootstrap.transport || 'tree-live-sse';
if (requestedTransport === 'convex-command-log-sse') requestedTransport = 'tree-live-sse';
if (requestedTransport === 'convex-command-log-ws') requestedTransport = 'tree-live-ws';
var preferWs = requestedTransport === 'tree-live-ws' && 'WebSocket' in window;
if (preferWs) {
applyTransport('convex-command-log-ws');
applyTransport('tree-live-ws');
startWithWebSocket(bootstrap, workspaceId, bootstrap.wsEndpoint || '/api/realtime/ws');
return;
}
@@ -220,7 +224,7 @@
applyStatus('unsupported');
return;
}
applyTransport(bootstrap.transport || 'convex-command-log-sse');
applyTransport(requestedTransport || 'tree-live-sse');
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
url.searchParams.set('workspaceId', workspaceId);
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
@@ -303,7 +303,7 @@ export function createTreeShellFileTreeDndRuntime(context) {
`目标: ${targetLabel}`,
context.sourceKind === "local_folder"
? "目标可写时复制进本地文件夹;命名冲突使用递增命名。"
: "Convex 目标交给宿主上传到对象存储;命名冲突由上传 executor 处理。",
: "legacy cloud 目标交给宿主上传到对象存储;命名冲突由上传 executor 处理。",
],
};
}
@@ -63,11 +63,11 @@ export function buildTreeShellFileTreeContextMenuProfile(context, target) {
}),
createFileTreeMenuItem("newFolder", "新建文件夹", {
disabled: !canCreateFolder,
reason: convexSource ? "Convex workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
reason: convexSource ? "legacy cloud workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
}),
createFileTreeMenuItem("upload", "上传/导入", {
disabled: true,
reason: localSource ? "外部文件请拖入 Explorer" : "Convex 上传 executor 尚未接入",
reason: localSource ? "外部文件请拖入 Explorer" : "legacy cloud 上传 executor 尚未接入",
}),
createFileTreeMenuItem("refresh", "刷新", { separatorBefore: true }),
createFileTreeMenuItem("collapseAll", "全部折叠"),
@@ -95,7 +95,7 @@ export function buildTreeShellFileTreeContextMenuProfile(context, target) {
}));
items.push(createFileTreeMenuItem("newFolder", "新建文件夹", {
disabled: !canCreateFolder,
reason: convexSource ? "Convex workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
reason: convexSource ? "legacy cloud workspace 当前未暴露 folder capability" : "当前 source 不支持新建文件夹",
}));
items.push(createFileTreeMenuItem("paste", "粘贴", {
disabled: !canPaste,
@@ -133,13 +133,20 @@ export function getFileTreeRowIconKind(item) {
if (iconHint === "table") return "table";
if (iconHint === "index") return "index";
if (iconHint === "page") return "page";
if (iconHint === "pdf") return "pdf";
if (iconHint === "office" || iconHint === "onlyoffice") return "office";
if (iconHint === "word" || iconHint === "ppt" || iconHint === "sheet") return iconHint;
if (iconHint === "image") return "image";
if (iconHint === "code" || iconHint === "web" || iconHint === "config") return iconHint;
if (item.rowKind === "document") return "page";
if (item.rowKind === "folder") return "folder";
if (item.rowKind === "markdown") return "file";
if (item.rowKind === "markdown") return "markdown";
if (item.rowKind === "index") return "index";
if (item.rowKind === "asset_folder") return "mindmap";
if (item.resourceMeta?.resourceKind === "table") return "table";
if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap";
if (item.resourceMeta?.resourceKind === "office") return "office";
if (item.resourceMeta?.resourceKind === "pdf") return "pdf";
return "file";
}
@@ -108,7 +108,7 @@ function startTreeShellRuntime() {
const sourceKind =
typeof state.sourceKind === "string" && state.sourceKind.trim()
? state.sourceKind.trim()
: "convex_workspace";
: "local_folder";
const rootUri =
typeof state.rootUri === "string" && state.rootUri.trim()
? state.rootUri.trim()