feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user