Files
mnote/rust/crates/mnote-web/browser/sidebar-workspace-runtime.js
T

1025 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const createSidebarWorkspaceRuntime = (dependencies = {}) => {
const {
cssEscape,
currentDocumentId,
escapeHtml,
parseJsonScript,
setCommandPending,
sidebarShellRuntimeFunction,
} = dependencies;
const MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
const MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:';
const MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
function normalizeSidebarTreeMode(value) {
var mode = String(value || '').trim();
return mode === 'filetree' ? 'filetree' : 'page';
}
function readStoredSidebarTreeMode() {
var runtimeFn = sidebarShellRuntimeFunction('readStoredSidebarTreeMode');
if (runtimeFn) return runtimeFn();
var params = new URLSearchParams(window.location.search);
var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
try {
var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
return normalizeSidebarTreeMode(stored);
} catch (_) {
return 'page';
}
}
function persistSidebarTreeMode(mode) {
var runtimeFn = sidebarShellRuntimeFunction('persistSidebarTreeMode');
if (runtimeFn) return runtimeFn(mode);
var normalized = normalizeSidebarTreeMode(mode);
document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
try {
if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
} catch (_) {}
return normalized;
}
function activeSidebarTreeMode() {
var runtimeFn = sidebarShellRuntimeFunction('activeSidebarTreeMode');
if (runtimeFn) return runtimeFn();
var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
if (active instanceof HTMLElement) {
return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
}
return readStoredSidebarTreeMode();
}
function resolveWorkspaceId(trigger) {
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
if (direct) return direct;
var root = trigger.closest('[data-workspace-id]');
if (root) {
var value = (root.getAttribute('data-workspace-id') || '').trim();
if (value) return value;
}
var documentId = currentDocumentId();
if (documentId) {
var shell = document.querySelector('.document-shell[data-document-id="' + cssEscape(documentId) + '"][data-workspace-id]');
if (shell instanceof HTMLElement) {
var shellWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim();
if (shellWorkspaceId) return shellWorkspaceId;
}
}
var activePane = document.querySelector('[data-pane-visible="true"][data-pane-workspace-id]');
if (activePane instanceof HTMLElement) {
var paneWorkspaceId = (activePane.getAttribute('data-pane-workspace-id') || '').trim();
if (paneWorkspaceId) return paneWorkspaceId;
}
var anyDocumentShell = document.querySelector('.document-shell[data-workspace-id]');
if (anyDocumentShell instanceof HTMLElement) {
var documentWorkspaceId = (anyDocumentShell.getAttribute('data-workspace-id') || '').trim();
if (documentWorkspaceId) return documentWorkspaceId;
}
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var domWorkspaceId = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (domWorkspaceId) return domWorkspaceId;
}
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
}
function currentWorkspaceId() {
return resolveWorkspaceId(document.body);
}
function currentSourceKind() {
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('sourceKind') || '').trim();
if (fromUrl) return fromUrl;
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-source-kind') || '').trim() : '';
if (fromBody) return fromBody;
if (
window.location.pathname === '/' &&
!(params.get('workspaceId') || '').trim() &&
!(params.get('rootUri') || '').trim() &&
!(params.get('pageId') || '').trim()
) {
return 'local_folder';
}
return 'local_folder';
}
function currentRootUri() {
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('rootUri') || '').trim();
if (fromUrl) return fromUrl;
return document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : '';
}
function rememberCloudWorkspaceId(workspaceId) {
var normalized = String(workspaceId || '').trim();
if (!normalized || normalized === 'local-folder' || normalized === 'default' || normalized.indexOf('local-ws:') === 0) return;
try {
if (window.localStorage) window.localStorage.setItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY, normalized);
} catch (_) {}
}
function readCurrentCloudWorkspaceIdFromPage() {
if (currentSourceKind() === 'local_folder') return '';
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('workspaceId') || '').trim();
if (fromUrl && fromUrl !== 'default' && fromUrl.indexOf('local-ws:') !== 0) return fromUrl;
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (value && value !== 'local-folder' && value !== 'default' && value.indexOf('local-ws:') !== 0) return value;
}
return '';
}
function rememberCurrentCloudWorkspaceId() {
rememberCloudWorkspaceId(readCurrentCloudWorkspaceIdFromPage());
}
function readLastCloudWorkspaceId() {
try {
var stored = window.localStorage ? window.localStorage.getItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY) : '';
if (stored && stored.trim() && stored.trim() !== 'default' && stored.trim().indexOf('local-ws:') !== 0) return stored.trim();
} catch (_) {}
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('workspaceId') || '').trim();
if (fromUrl && currentSourceKind() !== 'local_folder' && fromUrl.indexOf('local-ws:') !== 0) return fromUrl;
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (value && value !== 'local-folder' && value.indexOf('local-ws:') !== 0) return value;
}
return '';
}
function copyWorkspaceSourceParams(targetUrl) {
var params = new URLSearchParams(window.location.search);
['sourceKind', 'rootUri', 'fileTreeScope', 'secondaryDocumentId', 'secondarySourceKind', 'secondaryRootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) targetUrl.searchParams.set(name, value);
});
var sourceKind = currentSourceKind();
var rootUri = currentRootUri();
if (sourceKind && !targetUrl.searchParams.get('sourceKind')) targetUrl.searchParams.set('sourceKind', sourceKind);
if (rootUri && !targetUrl.searchParams.get('rootUri')) targetUrl.searchParams.set('rootUri', rootUri);
}
function currentWorkspaceSourcePayload() {
var params = new URLSearchParams(window.location.search);
var payload = {};
['sourceKind', 'rootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) payload[name] = value;
});
var sourceKind = currentSourceKind();
var rootUri = currentRootUri();
if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind;
if (rootUri && !payload.rootUri) payload.rootUri = rootUri;
return payload;
}
function currentActorStorageId() {
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-actor-id') || '').trim() : '';
if (fromBody && fromBody !== 'anonymous') return fromBody;
return '';
}
function recentLocalRootsStorageKey() {
var actorId = currentActorStorageId();
return actorId ? MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX + encodeURIComponent(actorId) : '';
}
function readRecentLocalRoots() {
try {
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return [];
var raw = window.localStorage ? window.localStorage.getItem(storageKey) : '';
var parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter(function(value) {
return typeof value === 'string' && value.trim();
}).slice(0, 10) : [];
} catch (_) {
return [];
}
}
function rememberLocalRoot(rootUri) {
try {
if (!window.localStorage) return;
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return;
var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
roots.unshift(rootUri);
window.localStorage.setItem(storageKey, JSON.stringify(roots.slice(0, 10)));
} catch (_) {}
}
function pathToFileRootUri(value) {
var trimmed = String(value || '').trim();
if (!trimmed) return '';
if (/^file:\/\//i.test(trimmed)) return trimmed;
if (trimmed.charAt(0) !== '/') return '';
return 'file://' + trimmed.split('/').map(function(part, index) {
return index === 0 ? '' : encodeURIComponent(part);
}).join('/');
}
function fileRootUriToPathInput(rootUri) {
var value = String(rootUri || '').replace(/^file:\/\//, '');
try {
return decodeURIComponent(value);
} catch (_) {
return value;
}
}
function recentLocalRootLabel(rootUri) {
var label = fileRootUriToPathInput(rootUri);
return label || '本地文件夹';
}
function normalizeGrantedLocalFolderRootUri(grant) {
var rootUri = String(grant && grant.rootUri || '').trim();
if (rootUri && /^file:\/\//i.test(rootUri)) return rootUri;
var rootPath = String(grant && grant.rootPath || '').trim();
if (rootPath) return pathToFileRootUri(rootPath);
if (rootUri) return pathToFileRootUri(rootUri);
return '';
}
function isDefaultWorkspaceAutoGrant(grant) {
var source = String(grant && grant.source || '').trim();
var workspaceId = String(grant && grant.workspaceId || '').trim();
var rootUri = String(grant && grant.rootUri || '').trim();
var permission = String(grant && grant.permission || '').trim();
var createdBy = String(grant && (grant.createdBy || grant.ownerUserId) || '').trim();
var targetUser = String(grant && (grant.userId || grant.targetUserId) || '').trim();
return source === 'auto'
&& workspaceId
&& permission === 'write'
&& createdBy === targetUser
&& /^local:\/\/users\/.+\/workspaces\/my-space$/.test(rootUri);
}
function openLocalFolderRoot(rootUri) {
if (currentSourceKind() !== 'local_folder') {
rememberCurrentCloudWorkspaceId();
}
rememberLocalRoot(rootUri);
var targetUrl = new URL('/', window.location.origin);
targetUrl.searchParams.set('treeView', 'filetree');
targetUrl.searchParams.set('sourceKind', 'local_folder');
targetUrl.searchParams.set('rootUri', rootUri);
window.location.href = targetUrl.toString();
}
function switchToCloudWorkspace() {
var targetUrl = new URL('/', window.location.origin);
targetUrl.searchParams.set('treeView', activeSidebarTreeMode() || 'filetree');
targetUrl.searchParams.set('mnoteHome', '1');
window.location.href = targetUrl.toString();
}
function closeTrashModal() {
var modal = document.querySelector('[data-testid="mnote-trash-modal"]');
if (!modal) return;
var workbench = modal.querySelector('[data-testid="mnote-trash-workbench"]');
if (workbench && workbench.__mnoteTrashEventSource && typeof workbench.__mnoteTrashEventSource.close === 'function') {
workbench.__mnoteTrashEventSource.close();
}
var fileScrollTop = Number(modal.getAttribute('data-file-scroll-top') || '0');
var fileRoot = document.getElementById('sidebar-file-tree-root');
if (fileRoot instanceof HTMLElement && Number.isFinite(fileScrollTop)) fileRoot.scrollTop = fileScrollTop;
if (modal.parentElement) modal.parentElement.removeChild(modal);
document.documentElement.removeAttribute('data-mnote-trash-modal-open');
}
function executeTrashWorkbenchScripts(parsed) {
parsed.querySelectorAll('script').forEach(function(script) {
var nextScript = document.createElement('script');
Array.from(script.attributes || []).forEach(function(attr) {
nextScript.setAttribute(attr.name, attr.value);
});
nextScript.textContent = script.textContent || '';
document.body.appendChild(nextScript);
if (nextScript.parentElement) nextScript.parentElement.removeChild(nextScript);
});
}
function renderLocalFolderTrashPlaceholder(content, rootUri) {
content.innerHTML = [
'<section class="mnote-trash-workbench" data-testid="mnote-trash-workbench" data-trash-source-kind="local_folder">',
'<header class="mnote-trash-header"><h1>本地文件夹垃圾箱</h1>',
'<p>本地删除项保存在当前目录的 <code>.mnote/trash</code> 与 <code>.mnote/trash-index.json</code> 中。</p></header>',
'<section class="mnote-trash-section"><div class="mnote-trash-empty">当前弹窗已保持在本地文件夹上下文:' + escapeHtml(rootUri || '未选择本地目录') + '</div></section>',
'</section>'
].join('');
}
function openTrashModal(trigger) {
var existing = document.querySelector('[data-testid="mnote-trash-modal"]');
if (existing) return;
var fileRoot = document.getElementById('sidebar-file-tree-root');
var activeRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-selected="true"], #sidebar-tree-root .tree-row[data-active="true"]');
var overlay = document.createElement('div');
overlay.className = 'mnote-trash-modal';
overlay.setAttribute('data-testid', 'mnote-trash-modal');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', '垃圾箱');
overlay.setAttribute('data-file-scroll-top', fileRoot instanceof HTMLElement ? String(fileRoot.scrollTop) : '0');
if (activeRow instanceof HTMLElement) {
overlay.setAttribute('data-active-row-id', activeRow.getAttribute('data-row-id') || activeRow.getAttribute('data-node-id') || '');
}
overlay.innerHTML = '<div class="mnote-trash-modal__backdrop" data-mnote-trash-modal-close="true"></div>' +
'<section class="mnote-trash-modal__panel">' +
'<button type="button" class="mnote-trash-modal__close" data-testid="mnote-trash-modal-close" data-mnote-trash-modal-close="true" aria-label="关闭垃圾箱">×</button>' +
'<div class="mnote-trash-modal__content" data-testid="mnote-trash-modal-content"><div class="mnote-trash-empty">正在加载垃圾箱...</div></div>' +
'</section>';
document.body.appendChild(overlay);
document.documentElement.setAttribute('data-mnote-trash-modal-open', 'true');
var content = overlay.querySelector('[data-testid="mnote-trash-modal-content"]');
var sourceKind = currentSourceKind();
var workspaceId = resolveWorkspaceId(trigger || document.body);
var url = new URL('/trash', window.location.origin);
if (sourceKind === 'local_folder') {
var rootUri = currentRootUri();
if (!rootUri) {
renderLocalFolderTrashPlaceholder(content, '');
return;
}
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
} else if (workspaceId) {
url.searchParams.set('workspaceId', workspaceId);
}
fetch(url.toString(), { headers: { 'x-mnote-trash-modal': '1' } }).then(function(response) {
return response.text().then(function(html) {
if (!response.ok) throw new Error('trash_modal_load_failed_' + response.status);
var parsed = new DOMParser().parseFromString(html, 'text/html');
var workbench = parsed.querySelector('[data-testid="mnote-trash-workbench"]');
if (!workbench) throw new Error('trash_modal_missing_workbench');
content.innerHTML = '';
content.appendChild(workbench);
executeTrashWorkbenchScripts(parsed);
});
}).catch(function(error) {
content.innerHTML = '<div class="mnote-trash-empty" data-testid="mnote-trash-modal-error">' + escapeHtml(error && error.message ? error.message : String(error)) + '</div>';
});
}
function closeLocalFolderDialog() {
var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
}
function readPathFromDirectoryFiles(files) {
var first = files && files.length ? files[0] : null;
if (!first) return '';
var rawPath = typeof first.path === 'string' ? first.path : '';
var relative = typeof first.webkitRelativePath === 'string' ? first.webkitRelativePath : '';
if (rawPath && relative) {
var suffix = relative.split('/').filter(Boolean).join('/');
if (suffix && rawPath.endsWith(suffix)) {
return rawPath.slice(0, rawPath.length - suffix.length).replace(/[\/\\]$/, '');
}
}
if (rawPath) return rawPath;
return '';
}
function requestBrowserFolderChoice(statusNode) {
return new Promise(function(resolve) {
var input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.addEventListener('change', function() {
var selectedPath = readPathFromDirectoryFiles(input.files || []);
if (input.parentElement) input.parentElement.removeChild(input);
if (!selectedPath && statusNode instanceof HTMLElement) {
statusNode.textContent = '当前浏览器没有暴露本机绝对路径,请在下方输入路径。';
}
resolve(selectedPath);
}, { once: true });
document.body.appendChild(input);
input.click();
});
}
async function requestNativeFolderChoice(statusNode) {
var desktopPicker = window.__mnoteDesktop && typeof window.__mnoteDesktop.selectLocalFolder === 'function'
? window.__mnoteDesktop.selectLocalFolder
: null;
if (desktopPicker) {
var selected = await desktopPicker();
return typeof selected === 'string' ? selected : '';
}
if (typeof window.showDirectoryPicker === 'function') {
var handle = await window.showDirectoryPicker({ mode: 'read' });
var handlePath = handle && (handle.path || handle.mnotePath || handle.nativePath);
if (typeof handlePath === 'string' && handlePath.trim()) return handlePath;
if (statusNode instanceof HTMLElement) {
statusNode.textContent = '已选择“' + (handle && handle.name ? handle.name : '文件夹') + '”,但浏览器没有暴露本机绝对路径,请在下方确认路径。';
}
return '';
}
return requestBrowserFolderChoice(statusNode);
}
function openLocalFolderDialog(initialMessage) {
closeLocalFolderDialog();
var recent = readRecentLocalRoots();
var dialog = document.createElement('div');
dialog.className = 'mnote-local-folder-dialog';
dialog.setAttribute('data-testid', 'mnote-local-folder-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.style.position = 'fixed';
dialog.style.inset = '0';
dialog.style.zIndex = '2147483646';
dialog.style.background = 'rgba(15, 23, 42, 0.28)';
dialog.style.display = 'flex';
dialog.style.alignItems = 'center';
dialog.style.justifyContent = 'center';
var card = document.createElement('div');
card.className = 'mnote-local-folder-dialog__card';
card.style.width = 'min(640px, calc(100vw - 32px))';
card.style.background = '#fff';
card.style.border = '1px solid rgba(27, 28, 28, 0.12)';
card.style.borderRadius = '8px';
card.style.padding = '16px';
card.style.boxShadow = '0 18px 48px rgba(15, 23, 42, 0.22)';
card.style.display = 'grid';
card.style.gap = '12px';
var title = document.createElement('h2');
title.textContent = '打开本地文件夹';
title.style.margin = '0';
title.style.fontSize = '18px';
title.style.lineHeight = '1.4';
var status = document.createElement('p');
status.className = 'mnote-local-folder-dialog__status';
status.setAttribute('data-testid', 'mnote-local-folder-status');
status.textContent = initialMessage || '选择一个本机文件夹,或输入绝对路径。';
status.style.margin = '0';
status.style.color = '#4b5563';
status.style.fontSize = '13px';
var input = document.createElement('input');
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
input.setAttribute('data-testid', 'mnote-local-folder-path-input');
input.value = recent.length ? fileRootUriToPathInput(recent[0]) : '';
input.style.width = '100%';
input.style.boxSizing = 'border-box';
input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
input.style.borderRadius = '6px';
input.style.padding = '10px 12px';
input.style.fontSize = '14px';
var actions = document.createElement('div');
actions.className = 'mnote-local-folder-dialog__actions';
actions.style.display = 'flex';
actions.style.gap = '8px';
actions.style.justifyContent = 'flex-end';
var choose = document.createElement('button');
choose.type = 'button';
choose.textContent = '选择文件夹';
choose.setAttribute('data-testid', 'mnote-local-folder-native-picker');
var cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = '取消';
var confirm = document.createElement('button');
confirm.type = 'button';
confirm.textContent = '打开';
confirm.setAttribute('data-testid', 'mnote-local-folder-open-confirm');
[choose, cancel, confirm].forEach(function(button) {
button.style.border = '1px solid rgba(27, 28, 28, 0.14)';
button.style.borderRadius = '6px';
button.style.padding = '8px 12px';
button.style.background = '#fff';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
});
function submit() {
var rootUri = pathToFileRootUri(input.value);
if (!rootUri) {
status.textContent = '请输入以 / 开头的绝对路径,或 file:// URI。';
input.focus();
return;
}
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
}
choose.addEventListener('click', function(event) {
event.preventDefault();
requestNativeFolderChoice(status).then(function(selectedPath) {
if (selectedPath) {
input.value = selectedPath;
submit();
}
}).catch(function(error) {
status.textContent = error && error.name === 'AbortError'
? '已取消选择。'
: '无法打开系统文件夹选择器,请输入路径。';
});
});
cancel.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
});
confirm.addEventListener('click', function(event) {
event.preventDefault();
submit();
});
input.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
});
actions.appendChild(choose);
actions.appendChild(cancel);
actions.appendChild(confirm);
card.appendChild(title);
card.appendChild(status);
card.appendChild(input);
var authorizedSection = document.createElement('div');
authorizedSection.className = 'mnote-local-folder-dialog__authorized';
authorizedSection.setAttribute('data-testid', 'mnote-local-folder-authorized-roots');
authorizedSection.hidden = true;
card.appendChild(authorizedSection);
fetch('/api/user/access-policy', {
method: 'GET',
headers: { 'accept': 'application/json' },
credentials: 'include'
}).then(function(response) {
return response.ok ? response.json() : null;
}).then(function(payload) {
var grants = payload && Array.isArray(payload.grants) ? payload.grants : [];
var activeGrants = grants.filter(function(grant) {
return grant && grant.active !== false && grant.status !== 'revoked';
});
if (!activeGrants.length) return;
authorizedSection.hidden = false;
var authorizedTitle = document.createElement('div');
authorizedTitle.style.fontSize = '12px';
authorizedTitle.style.color = '#6b7280';
authorizedTitle.textContent = '已授权文件夹';
var authorizedList = document.createElement('div');
authorizedList.className = 'mnote-local-folder-dialog__recent';
activeGrants.slice(0, 8).forEach(function(grant) {
if (isDefaultWorkspaceAutoGrant(grant)) return;
var rootUri = normalizeGrantedLocalFolderRootUri(grant);
if (!rootUri) return;
var button = document.createElement('button');
button.type = 'button';
button.setAttribute('data-testid', 'mnote-local-folder-authorized-root');
button.textContent = fileRootUriToPathInput(rootUri);
button.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
});
authorizedList.appendChild(button);
});
if (!authorizedList.childElementCount) return;
authorizedSection.appendChild(authorizedTitle);
authorizedSection.appendChild(authorizedList);
}).catch(function() {});
if (recent.length) {
var recentTitle = document.createElement('div');
recentTitle.style.fontSize = '12px';
recentTitle.style.color = '#6b7280';
recentTitle.textContent = '最近使用';
card.appendChild(recentTitle);
var recentList = document.createElement('div');
recentList.className = 'mnote-local-folder-dialog__recent';
recent.slice(0, 5).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.textContent = recentLocalRootLabel(rootUri);
button.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
});
recentList.appendChild(button);
});
card.appendChild(recentList);
}
card.appendChild(actions);
dialog.appendChild(card);
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closeLocalFolderDialog();
});
document.body.appendChild(dialog);
input.focus();
input.select();
}
function requestOpenLocalFolder() {
openLocalFolderDialog('');
}
async function createDefaultLocalWorkspace(trigger) {
setCommandPending(trigger, true);
try {
var response = await fetch('/api/local-folder/workspaces/default', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error((payload && payload.message) || ('local_workspace_create_failed_' + response.status));
}
var workspace = payload && payload.workspace || {};
var rootUri = String(workspace.rootUri || '').trim();
if (!rootUri) throw new Error('local_workspace_create_missing_root_uri');
openLocalFolderRoot(rootUri);
} catch (error) {
var status = document.querySelector('[data-testid="mnote-local-folder-status"]');
if (status instanceof HTMLElement) {
status.textContent = error && error.message ? error.message : String(error);
} else {
openLocalFolderDialog(error && error.message ? error.message : String(error));
}
} finally {
setCommandPending(trigger, false);
}
}
function autoOpenRecentLocalRootOnHome() {
if (window.location.pathname !== '/') return false;
var params = new URLSearchParams(window.location.search);
if ((params.get('sourceKind') || '').trim()) return false;
if ((params.get('rootUri') || '').trim()) return false;
if ((params.get('workspaceId') || '').trim()) return false;
if ((params.get('pageId') || '').trim()) return false;
if ((params.get('mnoteHome') || '').trim() === '1') return false;
var recent = readRecentLocalRoots();
if (!recent.length) return false;
openLocalFolderRoot(recent[0]);
return true;
}
function closeWorkspaceSourceMenu() {
var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.querySelectorAll('[data-testid="mnote-workspace-source-trigger"]').forEach(function(trigger) {
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
});
}
function workspaceSourceLabel(rootUri) {
var label = String(rootUri || '').replace(/^file:\/\//, '');
try {
label = decodeURIComponent(label);
} catch (_) {}
return label || '本地文件夹';
}
function openWorkspaceSourceMenu(trigger) {
closeWorkspaceSourceMenu();
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('aria-expanded', 'true');
var menu = document.createElement('div');
menu.className = 'mnote-workspace-source-menu';
menu.setAttribute('data-testid', 'mnote-workspace-source-menu');
menu.setAttribute('role', 'menu');
var cloudButton = document.createElement('button');
cloudButton.type = 'button';
cloudButton.className = 'mnote-workspace-source-menu__item';
cloudButton.setAttribute('data-testid', 'mnote-switch-cloud-workspace');
cloudButton.setAttribute('role', 'menuitem');
cloudButton.textContent = '我的空间';
cloudButton.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
switchToCloudWorkspace();
});
menu.appendChild(cloudButton);
var recent = readRecentLocalRoots();
if (recent.length) {
var label = document.createElement('div');
label.className = 'mnote-workspace-source-menu__label';
label.textContent = '最近本地文件夹';
menu.appendChild(label);
recent.slice(0, 8).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'mnote-workspace-source-menu__item';
button.setAttribute('data-testid', 'mnote-recent-local-root');
button.setAttribute('data-root-uri', rootUri);
button.setAttribute('role', 'menuitem');
button.textContent = workspaceSourceLabel(rootUri);
button.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
openLocalFolderRoot(rootUri);
});
menu.appendChild(button);
});
}
var openOther = document.createElement('button');
openOther.type = 'button';
openOther.className = 'mnote-workspace-source-menu__item mnote-workspace-source-menu__item--primary';
openOther.setAttribute('data-testid', 'mnote-open-other-local-folder');
openOther.setAttribute('role', 'menuitem');
openOther.textContent = '打开其他本地文件夹';
openOther.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
requestOpenLocalFolder();
});
menu.appendChild(openOther);
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
}
function closeAccountMenu() {
var existing = document.querySelector('[data-testid="mnote-account-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.querySelectorAll('[data-testid="mnote-account-menu-trigger"]').forEach(function(trigger) {
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
});
}
function closeProfileDialog() {
var existing = document.querySelector('[data-testid="mnote-profile-dialog"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
}
function closeAdminAccessPolicyDialog() {
var existing = document.querySelector('[data-testid="mnote-admin-access-policy-modal"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.documentElement.removeAttribute('data-mnote-admin-access-policy-modal-open');
}
function accountMenuFallbackSession() {
var body = document.body instanceof HTMLElement ? document.body : null;
var actorId = body ? String(body.getAttribute('data-mnote-actor-id') || '').trim() : '';
return {
ok: true,
name: actorId || '当前用户',
email: '',
userId: actorId || 'anonymous',
actorType: actorId && actorId !== 'anonymous' ? 'user' : 'anonymous',
authMode: 'browser'
};
}
function renderAccountInfo(menu, session) {
var nameNode = menu.querySelector('[data-account-info="name"]');
var emailNode = menu.querySelector('[data-account-info="email"]');
var idNode = menu.querySelector('[data-account-info="user-id"]');
var typeNode = menu.querySelector('[data-account-info="actor-type"]');
if (nameNode) nameNode.textContent = session.name || session.userId || '当前用户';
if (emailNode) emailNode.textContent = session.email || '未提供邮箱';
if (idNode) {
idNode.textContent = session.userId || 'anonymous';
idNode.setAttribute('title', session.userId || 'anonymous');
}
if (typeNode) typeNode.textContent = session.actorType || session.authMode || 'unknown';
var copyButton = menu.querySelector('[data-account-copy-user-id]');
if (copyButton instanceof HTMLElement) {
copyButton.dataset.copyValue = session.userId || 'anonymous';
copyButton.setAttribute('title', '复制用户 ID');
}
}
function sessionIsAdmin(session) {
return String(session && session.actorType || '').trim() === 'admin';
}
async function loadAccountInfo(menu) {
try {
var response = await fetch('/api/auth/session', {
method: 'GET',
headers: { 'accept': 'application/json' },
credentials: 'include'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
renderAccountInfo(menu, payload);
} catch (_) {
renderAccountInfo(menu, accountMenuFallbackSession());
}
}
async function fetchAccountSession() {
try {
var response = await fetch('/api/auth/session', {
method: 'GET',
headers: { 'accept': 'application/json' },
credentials: 'include'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
return payload;
} catch (_) {
return accountMenuFallbackSession();
}
}
function openProfileDialog(session) {
closeProfileDialog();
closeAccountMenu();
var dialog = document.createElement('div');
dialog.className = 'mnote-profile-dialog';
dialog.setAttribute('data-testid', 'mnote-profile-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('aria-labelledby', 'mnote-profile-dialog-title');
var userId = escapeHtml(session.userId || 'anonymous');
dialog.innerHTML =
'<div class="mnote-profile-dialog__backdrop" data-profile-dialog-close></div>' +
'<section class="mnote-profile-dialog__panel">' +
'<header class="mnote-profile-dialog__header">' +
'<div>' +
'<div class="mnote-profile-dialog__eyebrow">个人信息</div>' +
'<h2 id="mnote-profile-dialog-title">个人信息</h2>' +
'</div>' +
'<button type="button" class="mnote-profile-dialog__close" data-profile-dialog-close aria-label="关闭"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</header>' +
'<div class="mnote-profile-dialog__identity">' +
'<div class="mnote-profile-dialog__avatar">' + escapeHtml(String(session.name || session.email || session.userId || '用').slice(0, 1).toUpperCase()) + '</div>' +
'<div><strong>' + escapeHtml(session.name || session.userId || '当前用户') + '</strong><span>' + escapeHtml(session.email || '未提供邮箱') + '</span></div>' +
'</div>' +
'<dl class="mnote-profile-dialog__list">' +
'<div><dt>用户名</dt><dd>' + escapeHtml(session.name || '未设置') + '</dd></div>' +
'<div><dt>邮箱</dt><dd>' + escapeHtml(session.email || '未提供邮箱') + '</dd></div>' +
'<div><dt>用户 ID</dt><dd><code title="' + userId + '">' + userId + '</code><button type="button" data-profile-copy-user-id data-copy-value="' + userId + '"><span class="material-symbols-outlined" data-icon="content_copy" aria-hidden="true"></span></button></dd></div>' +
'<div><dt>身份</dt><dd>' + escapeHtml(session.actorType || session.authMode || 'unknown') + '</dd></div>' +
'</dl>' +
'</section>';
dialog.querySelectorAll('[data-profile-dialog-close]').forEach(function(button) {
button.addEventListener('click', function(event) {
event.preventDefault();
closeProfileDialog();
});
});
var copyButton = dialog.querySelector('[data-profile-copy-user-id]');
if (copyButton) {
copyButton.addEventListener('click', function(event) {
event.preventDefault();
void copyTreeContextValue(copyButton.getAttribute('data-copy-value') || '', 'profile-user-id');
});
}
document.body.appendChild(dialog);
var closeButton = dialog.querySelector('.mnote-profile-dialog__close');
if (closeButton instanceof HTMLElement) closeButton.focus();
}
function openAdminAccessPolicyDialog(trigger) {
closeAdminAccessPolicyDialog();
closeAccountMenu();
if (!(trigger instanceof HTMLElement)) return;
var role = trigger.getAttribute('data-access-policy-role') === 'admin' ? 'admin' : 'user';
var template = document.querySelector('[data-testid="mnote-admin-access-policy-template-' + role + '"]');
var dialog = document.createElement('div');
dialog.className = 'mnote-admin-policy-modal';
dialog.setAttribute('data-testid', 'mnote-admin-access-policy-modal');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('aria-labelledby', 'mnote-admin-policy-dialog-title');
dialog.innerHTML =
'<div class="mnote-admin-policy-modal__backdrop" data-admin-access-policy-close></div>' +
'<section class="mnote-admin-policy-modal__panel">' +
'<button type="button" class="mnote-admin-policy-modal__close" data-admin-access-policy-close aria-label="关闭授权管理"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'<div class="mnote-admin-policy-modal__content" data-testid="mnote-admin-access-policy-modal-content"></div>' +
'</section>';
var content = dialog.querySelector('[data-testid="mnote-admin-access-policy-modal-content"]');
if (content) {
content.innerHTML = template ? template.innerHTML : '<div class="mnote-admin-policy-empty">授权管理面板加载失败</div>';
}
if (typeof window.MNOTEInitAccessPolicyPanel === 'function') {
window.MNOTEInitAccessPolicyPanel(dialog);
}
dialog.querySelectorAll('[data-admin-access-policy-close]').forEach(function(button) {
button.addEventListener('click', function(event) {
event.preventDefault();
closeAdminAccessPolicyDialog();
});
});
document.body.appendChild(dialog);
document.documentElement.setAttribute('data-mnote-admin-access-policy-modal-open', 'true');
var closeButton = dialog.querySelector('.mnote-admin-policy-modal__close');
if (closeButton instanceof HTMLElement) closeButton.focus();
}
async function signOutAccount(trigger) {
setCommandPending(trigger, true);
try {
var response = await fetch('/api/auth', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action: 'auth:signOut', args: {} })
});
var payload = await response.json().catch(function() { return {}; });
payload = payload || {};
if (!response.ok || payload.error) {
throw new Error(payload.error || '退出登录失败');
}
window.location.assign('/auth');
} catch (error) {
var menu = document.querySelector('[data-testid="mnote-account-menu"]');
var errorNode = menu ? menu.querySelector('[data-account-error]') : null;
if (errorNode instanceof HTMLElement) {
errorNode.textContent = error && error.message ? error.message : '退出登录失败';
errorNode.hidden = false;
}
setCommandPending(trigger, false);
}
}
function openAccountMenu(trigger) {
closeAccountMenu();
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('aria-expanded', 'true');
var menu = document.createElement('div');
menu.className = 'mnote-workspace-source-menu mnote-account-menu';
menu.setAttribute('data-testid', 'mnote-account-menu');
menu.setAttribute('role', 'menu');
menu.innerHTML =
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-profile" role="menuitem"><span class="material-symbols-outlined" data-icon="account_circle" aria-hidden="true"></span><span>个人信息</span></button>' +
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-access-policy" role="menuitem" hidden><span class="material-symbols-outlined" data-icon="admin_panel_settings" aria-hidden="true"></span><span>授权管理</span></button>' +
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-ai-management" role="menuitem" hidden><span class="material-symbols-outlined" data-icon="smart_toy" aria-hidden="true"></span><span>AI 管理</span></button>' +
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__logout" data-testid="mnote-account-sign-out" role="menuitem">退出登录</button>' +
'<div class="mnote-account-menu__error" data-account-error hidden></div>';
var sessionPromise = fetchAccountSession();
var profileButton = menu.querySelector('[data-testid="mnote-account-profile"]');
if (profileButton) {
profileButton.addEventListener('click', function(event) {
event.preventDefault();
sessionPromise.then(openProfileDialog);
});
}
var accessPolicyLink = menu.querySelector('[data-testid="mnote-account-access-policy"]');
sessionPromise.then(function(session) {
if (!(accessPolicyLink instanceof HTMLElement)) return;
accessPolicyLink.hidden = false;
accessPolicyLink.setAttribute('data-access-policy-role', sessionIsAdmin(session) ? 'admin' : 'user');
});
if (accessPolicyLink) {
accessPolicyLink.addEventListener('click', function(event) {
event.preventDefault();
openAdminAccessPolicyDialog(accessPolicyLink);
});
}
var aiManagementLink = menu.querySelector('[data-testid="mnote-account-ai-management"]');
sessionPromise.then(function(session) {
if (!(aiManagementLink instanceof HTMLElement)) return;
aiManagementLink.hidden = false;
aiManagementLink.setAttribute('data-ai-management-role', sessionIsAdmin(session) ? 'admin' : 'user');
});
if (aiManagementLink) {
aiManagementLink.addEventListener('click', function(event) {
event.preventDefault();
sessionPromise.then(function(session) {
window.location.assign(sessionIsAdmin(session) ? '/admin/ai' : '/user/ai');
});
});
}
var signOutButton = menu.querySelector('[data-testid="mnote-account-sign-out"]');
if (signOutButton) {
signOutButton.addEventListener('click', function(event) {
event.preventDefault();
void signOutAccount(signOutButton);
});
}
var host = trigger.closest('[data-testid="wolai-sidebar-quick-actions"]') || trigger.parentElement;
if (host) host.appendChild(menu);
}
return {
activeSidebarTreeMode,
autoOpenRecentLocalRootOnHome,
closeAccountMenu,
closeLocalFolderDialog,
closeTrashModal,
closeWorkspaceSourceMenu,
copyWorkspaceSourceParams,
createDefaultLocalWorkspace,
currentRootUri,
currentSourceKind,
currentWorkspaceId,
currentWorkspaceSourcePayload,
normalizeSidebarTreeMode,
openAccountMenu,
openTrashModal,
openWorkspaceSourceMenu,
persistSidebarTreeMode,
readStoredSidebarTreeMode,
requestOpenLocalFolder,
resolveWorkspaceId,
};
};