2026-05-26 02:20:46 +08:00
export const createSidebarFileTreeCommandRuntime = ( dependencies = {}) => {
const {
applyRemoveDocumentDelta ,
createFileTreeFolder ,
createPage ,
cssEscape ,
currentDocumentId ,
2026-05-27 11:31:12 +08:00
currentRootUri ,
2026-05-26 02:20:46 +08:00
currentSourceKind ,
deleteSingleFileTreeAsset ,
dispatchSidebarEvent ,
dispatchTreeCommand ,
ensureFileTreeWritableTarget ,
fileTreeRuntimeDeps ,
fileTreeRuntimeFunction ,
fileTreeSelectionRuntimeDeps ,
fileTreeSelectionRuntimeFunction ,
isFileTreePageRow ,
localFilePathFromAssetId ,
normalizeFileTreePageRenameTitle ,
openConvexAssetFromFileTree ,
openEditorAttachmentDetail ,
openEditorAttachmentDownload ,
openEditorAttachmentEditTab ,
openEditorAttachmentNewWindow ,
2026-06-01 10:07:42 +08:00
openLocalResourceInActiveTab ,
2026-07-15 23:12:15 +08:00
knowledgeHostUrl ,
2026-05-26 02:20:46 +08:00
refreshLocalFolderSidebarSnapshot ,
2026-05-27 11:31:12 +08:00
removeFileTreeAssetRow ,
revealFileTreeResource ,
2026-05-26 02:20:46 +08:00
resolveWorkspaceId ,
runtimeState ,
selectedSidebarFileTreeSelection ,
updateTitleEverywhere ,
validateFileTreeRename ,
} = dependencies ;
const sidebarFileTreeSelection = selectedSidebarFileTreeSelection ;
2026-05-27 11:31:12 +08:00
var fileTreeOperationBatchCounter = 0 ;
function nextFileTreeOperationBatchId ( action ) {
fileTreeOperationBatchCounter += 1 ;
return 'filetree-' + String ( action || 'operation' ) + '-' + Date . now () + '-' + fileTreeOperationBatchCounter ;
}
2026-05-26 02:20:46 +08:00
function rowTitle ( row ) {
var title = row ? row . querySelector ( ':scope > .tree-link > .tree-link-title' ) : null ;
return title && title . textContent ? title . textContent . trim () : '无标题' ;
}
function rowCenter ( row ) {
var rect = row . getBoundingClientRect ();
return { x : rect . left + Math . min ( rect . width - 12 , 180 ), y : rect . top + Math . min ( rect . height , 22 ) };
}
async function renameFileTreeAsset ( assetId , title ) {
var response = await fetch ( '/api/media/batch' , {
method : 'POST' ,
credentials : 'include' ,
headers : { 'content-type' : 'application/json' },
body : JSON . stringify ({ action : 'rename' , assetIds : [ assetId ], newName : title })
});
var payload = await response . json (). catch ( function (){ return null ; });
if ( ! response . ok ) throw new Error (( payload && payload . error ) || '重命名附件失败' );
return payload ;
}
function beginFileTreeInlineRename ( row ) {
if ( ! ( row instanceof HTMLElement )) return false ;
if ( row . querySelector ( '.tree-rename-input' )) return true ;
var rowKind = row . getAttribute ( 'data-row-kind' ) || '' ;
var documentId = row . getAttribute ( 'data-document-id' ) || row . getAttribute ( 'data-doc-id' ) || '' ;
var assetId = row . getAttribute ( 'data-asset-id' ) || '' ;
2026-05-27 11:31:12 +08:00
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
var localFolderSource = currentSourceKind () === 'local_folder' ;
var localFolderDirectory = localFolderSource && ( rowKind === 'folder' || rowKind === 'directory' );
var commandTargetId = documentId || ( localFolderSource ? ( assetId || rowId ) : '' );
if ( ! commandTargetId && ! assetId ) return false ;
if ( rowKind && [ 'document' , 'doc' , 'index' , 'markdown' , 'asset' ]. indexOf ( rowKind ) < 0 && ! assetId && ! localFolderDirectory ) return false ;
2026-05-26 02:20:46 +08:00
var link = row . querySelector ( ':scope > .tree-link' );
var title = rowTitle ( row );
if ( ! ( link instanceof HTMLElement )) return false ;
link . hidden = true ;
var input = document . createElement ( 'input' );
input . type = 'text' ;
input . className = 'tree-rename-input' ;
2026-05-27 11:31:12 +08:00
input . setAttribute ( 'data-rename-id' , rowId );
2026-05-26 02:20:46 +08:00
input . value = title ;
input . style . minWidth = '0' ;
input . style . flex = '1 1 auto' ;
input . style . height = '22px' ;
input . style . border = '1px solid #93c5fd' ;
input . style . borderRadius = '3px' ;
input . style . padding = '0 4px' ;
input . style . font = 'inherit' ;
input . style . background = '#fff' ;
input . style . color = '#1f2937' ;
var validation = document . createElement ( 'div' );
validation . setAttribute ( 'data-testid' , 'tree-rename-validation' );
validation . setAttribute ( 'data-mnote-rename-validation' , 'true' );
validation . style . fontSize = '12px' ;
validation . style . color = '#dc2626' ;
validation . style . padding = '2px 4px' ;
validation . hidden = true ;
var setValidation = function ( message ) {
var text = String ( message || '' ). trim ();
validation . textContent = text ;
validation . hidden = ! text ;
input . setAttribute ( 'aria-invalid' , text ? 'true' : 'false' );
};
var closed = false ;
var committing = false ;
var close = function () {
if ( closed ) return ;
closed = true ;
if ( input . parentElement ) input . parentElement . removeChild ( input );
if ( validation . parentElement ) validation . parentElement . removeChild ( validation );
link . hidden = false ;
};
var commit = function () {
if ( closed || committing ) return ;
var nextTitle = input . value . trim ();
if ( ! nextTitle || nextTitle === title ) {
close ();
return ;
}
var validationMessage = validateFileTreeRename ( row , nextTitle );
if ( validationMessage ) {
setValidation ( validationMessage );
return ;
}
setValidation ( '' );
var commandTitle = isFileTreePageRow ( row ) ? normalizeFileTreePageRenameTitle ( nextTitle ) : nextTitle ;
committing = true ;
input . disabled = true ;
2026-05-27 11:31:12 +08:00
var work = assetId && ! localFolderSource
2026-05-26 02:20:46 +08:00
? Promise . resolve (). then ( function (){
document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-asset-id="' + cssEscape ( assetId ) + '"] .tree-link-title' ). forEach ( function ( titleNode ) {
titleNode . textContent = commandTitle ;
});
close ();
return renameFileTreeAsset ( assetId , commandTitle );
}). then ( function (){ return null ; })
: dispatchTreeCommand ( row , {
action : 'rename' ,
workspaceId : resolveWorkspaceId ( row ),
2026-05-27 11:31:12 +08:00
documentId : commandTargetId ,
2026-05-26 02:20:46 +08:00
title : commandTitle
2026-05-27 11:31:12 +08:00
}). then ( function (){
if ( ! localFolderSource && documentId ) updateTitleEverywhere ( documentId , commandTitle );
});
2026-07-28 17:04:27 +08:00
void work . then ( close ). catch ( async function ( error ) {
2026-05-26 02:20:46 +08:00
committing = false ;
input . disabled = false ;
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( error && error . message ? error . message : '重命名失败' );
2026-05-26 02:20:46 +08:00
});
};
input . addEventListener ( 'click' , function ( event ) { event . stopPropagation (); });
input . addEventListener ( 'dblclick' , function ( event ) { event . stopPropagation (); });
input . addEventListener ( 'keydown' , function ( event ) {
event . stopPropagation ();
if ( event . key === 'Enter' ) {
event . preventDefault ();
commit ();
} else if ( event . key === 'Escape' ) {
event . preventDefault ();
close ();
}
});
input . addEventListener ( 'blur' , commit );
link . parentElement . insertBefore ( input , link . nextSibling );
link . parentElement . insertBefore ( validation , input . nextSibling );
window . requestAnimationFrame ( function () {
input . focus ();
input . select ();
});
return true ;
}
function closeTreeContextMenu () {
if ( runtimeState . activeTreeContextMenu && runtimeState . activeTreeContextMenu . parentElement ) {
runtimeState . activeTreeContextMenu . parentElement . removeChild ( runtimeState . activeTreeContextMenu );
}
runtimeState . activeTreeContextMenu = null ;
}
2026-07-24 11:36:06 +08:00
/**
* Product clipboard used by menu paste + keyboard paste.
* Keep keyboard-module clipboard and runtimeState in lockstep so
* 「粘贴到此处」enables after either Ctrl+C/X or menu cut/copy.
*/
function currentSidebarFileTreeClipboard () {
if ( runtimeState . sidebarFileTreeClipboard
&& Array . isArray ( runtimeState . sidebarFileTreeClipboard . rowIds )
&& runtimeState . sidebarFileTreeClipboard . rowIds . length ) {
return runtimeState . sidebarFileTreeClipboard ;
}
var kb = window . __mnoteFileTreeKeyboardRuntime ;
var fromKb = kb && typeof kb . getSidebarFileTreeClipboard === 'function'
? kb . getSidebarFileTreeClipboard ()
: ( kb && kb . sidebarFileTreeClipboard ) || null ;
if ( fromKb && Array . isArray ( fromKb . rowIds ) && fromKb . rowIds . length ) {
// Heal dual-clipboard drift (keyboard module wrote first).
runtimeState . sidebarFileTreeClipboard = fromKb ;
return fromKb ;
}
return null ;
}
function setSidebarFileTreeClipboard ( action , rowIds ) {
var next = action && Array . isArray ( rowIds ) && rowIds . length
? { action : action , rowIds : rowIds . slice () }
: null ;
runtimeState . sidebarFileTreeClipboard = next ;
var kb = window . __mnoteFileTreeKeyboardRuntime ;
if ( kb && typeof kb . setSidebarFileTreeClipboard === 'function' ) {
if ( next ) {
kb . setSidebarFileTreeClipboard ( next . action , next . rowIds );
} else if ( typeof kb . clearSidebarFileTreeClipboard === 'function' ) {
kb . clearSidebarFileTreeClipboard ();
}
} else if ( next ) {
document . documentElement . setAttribute ( 'data-mnote-filetree-clipboard-action' , next . action );
} else {
document . documentElement . removeAttribute ( 'data-mnote-filetree-clipboard-action' );
}
return next ;
}
2026-05-26 02:20:46 +08:00
function copyTreeContextValue ( value , actionName ) {
var text = String ( value || '' );
var done = function () {
document . documentElement . setAttribute ( 'data-mnote-tree-context-last-copy' , actionName || 'copy' );
};
if ( navigator . clipboard && typeof navigator . clipboard . writeText === 'function' ) {
return navigator . clipboard . writeText ( text ). then ( done ). catch ( function (){});
}
var textarea = document . createElement ( 'textarea' );
textarea . value = text ;
textarea . setAttribute ( 'readonly' , 'readonly' );
textarea . style . position = 'fixed' ;
textarea . style . left = '-9999px' ;
document . body . appendChild ( textarea );
textarea . select ();
try { document . execCommand ( 'copy' ); } catch ( _ ) {}
document . body . removeChild ( textarea );
done ();
return Promise . resolve ();
}
function triggerBrowserDownload ( url ) {
if ( ! url ) return false ;
var link = document . createElement ( 'a' );
link . href = url ;
link . target = '_blank' ;
link . rel = 'noopener noreferrer' ;
link . download = '' ;
link . style . position = 'fixed' ;
link . style . left = '-9999px' ;
link . style . top = '0' ;
document . body . appendChild ( link );
try {
link . click ();
} catch ( _ ) {
if ( typeof window . open === 'function' ) window . open ( url , '_blank' , 'noopener,noreferrer' );
}
window . setTimeout ( function () {
if ( link . parentElement ) link . parentElement . removeChild ( link );
}, 1000 );
return true ;
}
function recordFileTreeAction ( action , detail ) {
var normalized = String ( action || '' ). trim () || 'unknown' ;
var rowId = String ( detail && detail . rowId || '' ). trim ();
var documentId = String ( detail && detail . documentId || '' ). trim ();
var assetId = String ( detail && detail . assetId || '' ). trim ();
document . documentElement . setAttribute ( 'data-mnote-filetree-last-action' , normalized );
if ( rowId ) document . documentElement . setAttribute ( 'data-mnote-filetree-last-action-row-id' , rowId );
if ( documentId ) document . documentElement . setAttribute ( 'data-mnote-filetree-last-action-document-id' , documentId );
if ( assetId ) document . documentElement . setAttribute ( 'data-mnote-filetree-last-action-asset-id' , assetId );
window . dispatchEvent ( new CustomEvent ( 'tree.filetree.action' , {
detail : Object . assign ({}, detail || {}, { action : normalized })
}));
}
function recordFileTreeActionStatus ( status , detail ) {
var normalized = String ( status || '' ). trim () || 'unknown' ;
document . documentElement . setAttribute ( 'data-mnote-filetree-last-action-status' , normalized );
window . dispatchEvent ( new CustomEvent ( 'tree.filetree.action.status' , {
detail : Object . assign ({}, detail || {}, { status : normalized })
}));
}
function documentHref ( documentId , workspaceId ) {
var url = new URL ( '/documents/' + encodeURIComponent ( documentId ), window . location . origin );
if ( workspaceId ) url . searchParams . set ( 'workspaceId' , workspaceId );
copyWorkspaceSourceParams ( url );
return url . toString ();
}
2026-07-28 17:04:27 +08:00
async function convertToPreviousSiblingChild ( trigger , detail ) {
2026-05-26 02:20:46 +08:00
var documentId = detail . documentId || '' ;
var row = document . querySelector ( '#sidebar-tree-root .tree-row[data-node-id="' + cssEscape ( documentId ) + '"]' );
if ( ! ( row instanceof HTMLElement )) return ;
var parentId = row . getAttribute ( 'data-parent-id' ) || '' ;
var siblings = Array . from ( document . querySelectorAll ( '#sidebar-tree-root .tree-row[data-shell-mode="page"]' )). filter ( function ( candidate ) {
return ( candidate . getAttribute ( 'data-parent-id' ) || '' ) === parentId ;
});
var index = siblings . indexOf ( row );
if ( index <= 0 ) {
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( '当前页面前面没有同级页面。' );
2026-05-26 02:20:46 +08:00
return ;
}
var previous = siblings [ index - 1 ];
var previousId = previous . getAttribute ( 'data-node-id' ) || '' ;
var children = previous . parentElement ? previous . parentElement . querySelectorAll ( ':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]' ) : [];
void dispatchTreeCommand ( trigger || row , {
action : 'move' ,
workspaceId : detail . workspaceId || resolveWorkspaceId ( row ),
documentId : documentId ,
parentId : previousId ,
sortOrder : children . length
});
}
function fileTreeCopyPath ( detail , trigger ) {
// 对 local_folder 复制真实相对路径,不复制标题
if ( currentSourceKind () === 'local_folder' ) {
if ( detail . assetId ) {
var path = localFilePathFromAssetId ( detail . assetId );
if ( path ) {
try {
return decodeURIComponent ( path . replace ( /~2F/g , '/' ));
} catch ( _ ) {
return path . replace ( /~2F/g , '/' );
}
}
}
if ( detail . documentId ) {
var docPath = String ( detail . documentId || '' )
. replace ( /^local-md:/ , '' )
. replace ( /^local-dir:/ , '' )
. replace ( /~2F/g , '/' );
if ( docPath ) {
try {
return decodeURIComponent ( docPath );
} catch ( _ ) {
return docPath ;
}
}
}
}
return detail . title || '' ;
}
2026-05-27 11:31:12 +08:00
function localRootPathFromRootUri ( rootUri ) {
var value = String ( rootUri || '' ). trim ();
if ( ! value ) return '' ;
if ( ! /^file:\/\//i . test ( value )) return value . charAt ( 0 ) === '/' ? value : '' ;
var pathPart = value . replace ( /^file:\/\//i , '' );
if ( pathPart . indexOf ( 'localhost/' ) === 0 ) pathPart = pathPart . slice ( 'localhost' . length );
if ( pathPart . charAt ( 0 ) !== '/' ) pathPart = '/' + pathPart ;
try {
return decodeURIComponent ( pathPart );
} catch ( _ ) {
return pathPart ;
}
}
function joinLocalAbsolutePath ( rootUri , relativePath ) {
var rootPath = localRootPathFromRootUri ( rootUri );
2026-07-28 17:04:27 +08:00
var normalized = decodeLocalEncodedPath ( relativePath ). replace ( /^\/+/ , '' ). replace ( /\\/g , '/' );
2026-05-27 11:31:12 +08:00
if ( ! rootPath || ! normalized ) return rootPath || normalized ;
2026-07-28 17:04:27 +08:00
// Collapse . / empty and resolve .. without climbing above workspace root.
var parts = [];
normalized . split ( '/' ). forEach ( function ( seg ) {
if ( ! seg || seg === '.' ) return ;
if ( seg === '..' ) {
if ( parts . length ) parts . pop ();
return ;
}
parts . push ( seg );
});
var safeRel = parts . join ( '/' );
if ( ! safeRel ) return rootPath . replace ( /\/+$/g , '' );
return rootPath . replace ( /\/+$/g , '' ) + '/' + safeRel ;
2026-05-27 11:31:12 +08:00
}
function fileTreeCopyLocalAbsolutePath ( detail , trigger ) {
2026-05-28 22:01:44 +08:00
var workspacePath = detail && detail . workspacePath && typeof detail . workspacePath === 'object' ? detail . workspacePath : null ;
var rootUri = String ( workspacePath && workspacePath . rootUri || '' ). trim () || ( typeof currentRootUri === 'function' ? currentRootUri () : '' );
2026-05-27 11:31:12 +08:00
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' ) || '' ;
2026-05-28 22:01:44 +08:00
var relativePath = String (
workspacePath && ( workspacePath . relativePath || workspacePath . localRelativePath )
|| detail && detail . localRelativePath
|| ''
). trim ();
2026-05-27 11:31:12 +08:00
if ( ! relativePath && detail && detail . assetId ) relativePath = localFilePathFromAssetId ( detail . assetId );
if ( ! relativePath && detail && detail . documentId ) {
relativePath = String ( detail . documentId || '' )
. replace ( /^local-md:/ , '' )
. replace ( /^local-dir:/ , '' );
}
if ( ! relativePath && detail && detail . rowId ) {
relativePath = String ( detail . rowId || '' )
. replace ( /^local:asset:/ , '' )
. replace ( /^local:markdown:/ , '' )
. replace ( /^local:folder:/ , '' )
. replace ( /^local:node:/ , '' );
}
return joinLocalAbsolutePath ( rootUri , relativePath );
}
2026-05-26 02:20:46 +08:00
function fileTreeMenuTargetParentId ( detail , trigger ) {
var rowKind = String ( detail && detail . rowKind || '' ). trim ();
var rowId = String ( detail && detail . rowId || '' ). trim ();
var documentId = String ( detail && detail . documentId || '' ). trim ();
if ( rowKind === 'folder' && rowId ) return rowId ;
if ( rowKind === 'directory' && rowId ) return rowId ;
if ( documentId ) return documentId ;
if ( trigger && typeof trigger . getAttribute === 'function' ) {
var triggerKind = String ( trigger . getAttribute ( 'data-row-kind' ) || '' ). trim ();
var triggerRowId = String ( trigger . getAttribute ( 'data-row-id' ) || '' ). trim ();
if (( triggerKind === 'folder' || triggerKind === 'directory' ) && triggerRowId ) return triggerRowId ;
}
return '' ;
}
function withOfficeEditModeGuard ( callback ) {
document . documentElement . setAttribute ( 'data-mnote-last-office-edit-mode-requested' , 'true' );
document . documentElement . setAttribute ( 'data-mnote-last-office-edit-mode-guard' , 'silent' );
callback ();
}
2026-06-07 10:35:21 +08:00
function knowledgeRagSourceRelativePath ( detail ) {
2026-06-01 10:07:42 +08:00
var workspacePath = detail && detail . workspacePath && typeof detail . workspacePath === 'object' ? detail . workspacePath : null ;
var relativePath = String (
detail && detail . localRelativePath
|| workspacePath && ( workspacePath . relativePath || workspacePath . localRelativePath )
|| ''
). trim ();
if ( ! relativePath && detail && detail . assetId ) relativePath = localFilePathFromAssetId ( detail . assetId );
return relativePath . replace ( /^\/+/ , '' );
}
2026-06-07 10:35:21 +08:00
function knowledgeRagRootUri ( detail , trigger ) {
2026-06-01 10:07:42 +08:00
var workspacePath = detail && detail . workspacePath && typeof detail . workspacePath === 'object' ? detail . workspacePath : null ;
var rootUri = String (
detail && ( detail . localRootUri || detail . rootUri )
|| workspacePath && workspacePath . rootUri
|| ''
). trim ();
if ( ! rootUri && trigger && typeof trigger . closest === 'function' ) {
var row = trigger . closest ( '.tree-row[data-shell-mode="filetree"]' );
if ( row instanceof HTMLElement ) rootUri = String ( row . getAttribute ( 'data-root-uri' ) || '' ). trim ();
}
return rootUri || currentRootUri () || '' ;
}
2026-06-07 10:35:21 +08:00
function supportsKnowledgeRagSource ( detail ) {
var path = knowledgeRagSourceRelativePath ( detail );
return Boolean ( path && knowledgeRagRootUri ( detail , null ));
2026-06-07 01:10:31 +08:00
}
async function ingestKnowledgeRagForDetail ( detail , trigger ) {
2026-06-07 10:35:21 +08:00
var sourceRootRelativePath = knowledgeRagSourceRelativePath ( detail )
2026-06-07 01:10:31 +08:00
|| String ( detail && ( detail . localRelativePath || detail . path || detail . title ) || '' ). trim ();
2026-06-07 10:35:21 +08:00
var rootUri = knowledgeRagRootUri ( detail , trigger );
2026-06-07 01:10:31 +08:00
if ( ! sourceRootRelativePath || ! rootUri ) {
2026-07-03 23:20:16 +08:00
throw new Error ( '缺少知识库来源或 rootUri' );
2026-06-07 01:10:31 +08:00
}
var workspaceId = String ( detail && detail . workspaceId || resolveWorkspaceId ( document . body ) || '' ). trim ();
document . documentElement . setAttribute ( 'data-mnote-knowledge-rag-menu-status' , 'running' );
document . documentElement . setAttribute ( 'data-mnote-knowledge-rag-menu-path' , sourceRootRelativePath );
var response = await fetch ( '/api/knowledge-rag/ingest' , {
2026-06-01 10:07:42 +08:00
method : 'POST' ,
headers : { 'content-type' : 'application/json' , accept : 'application/json' },
2026-06-07 01:10:31 +08:00
body : JSON . stringify ({
workspaceId : workspaceId ,
rootUri : rootUri ,
2026-07-03 23:20:16 +08:00
sources : [{ sourcePath : sourceRootRelativePath }],
knowledgeBaseId : String ( detail && detail . kbId || '' ). trim () || undefined ,
providerKnowledgeBaseId : String ( detail && ( detail . providerKbId || detail . kbId ) || '' ). trim () || undefined
2026-06-07 01:10:31 +08:00
})
2026-06-01 10:07:42 +08:00
});
var payload = await response . json (). catch ( function () { return null ; });
2026-06-07 01:10:31 +08:00
if ( ! response . ok || ! payload || payload . ok === false ) {
2026-07-03 23:20:16 +08:00
var message = payload && payload . message ? payload . message : '知识库索引失败' ;
2026-06-07 01:10:31 +08:00
document . documentElement . setAttribute ( 'data-mnote-knowledge-rag-menu-status' , 'failed' );
2026-06-01 10:07:42 +08:00
throw new Error ( message );
}
2026-06-07 01:10:31 +08:00
document . documentElement . setAttribute ( 'data-mnote-knowledge-rag-menu-status' , payload . retryRequired ? 'retry' : 'done' );
window . dispatchEvent ( new CustomEvent ( 'mnote:knowledge-rag-source-updated' , {
detail : { rootUri : rootUri , workspaceId : workspaceId , sourceRootRelativePath : sourceRootRelativePath , result : payload }
2026-06-01 10:07:42 +08:00
}));
2026-06-07 01:10:31 +08:00
return payload ;
2026-06-01 10:07:42 +08:00
}
2026-07-28 17:04:27 +08:00
async function handleTreeContextMenuAction ( action , detail , trigger ) {
2026-05-26 02:20:46 +08:00
closeTreeContextMenu ();
detail = detail || {};
2026-06-07 01:10:31 +08:00
if ( action === 'knowledge-rag-index' ) {
recordFileTreeAction ( 'knowledge-rag-index' , detail );
recordFileTreeActionStatus ( 'pending' , detail );
void ingestKnowledgeRagForDetail ( detail , trigger ). then ( function () {
recordFileTreeActionStatus ( 'done' , detail );
2026-07-03 23:20:16 +08:00
if ( window . mnote && window . mnote . toast ) {
window . mnote . toast ( '已加入知识库' , { kind : 'success' });
}
2026-07-28 17:04:27 +08:00
}). catch ( async function ( error ) {
2026-07-03 23:20:16 +08:00
var msg = error && error . message ? error . message : '知识库索引失败' ;
2026-06-07 01:10:31 +08:00
recordFileTreeActionStatus ( 'failed' , Object . assign ({}, detail , { fallback : 'alert' }));
2026-07-03 23:20:16 +08:00
if ( window . mnote && window . mnote . toast ) {
window . mnote . toast ( msg , { kind : 'error' });
} else {
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( msg );
2026-07-03 23:20:16 +08:00
}
2026-06-01 10:07:42 +08:00
});
return ;
}
2026-07-03 23:20:16 +08:00
if ( action === 'open-knowledge-page' ) {
2026-07-15 23:12:15 +08:00
var knowledgeUrl = typeof knowledgeHostUrl === 'function'
? knowledgeHostUrl ({
workspaceId : String ( detail && detail . workspaceId || resolveWorkspaceId ( trigger || document . body ) || '' ). trim (),
sourceKind : currentSourceKind () || 'local_folder' ,
rootUri : knowledgeRagRootUri ( detail , trigger )
})
: '/knowledge' ;
2026-07-03 23:20:16 +08:00
if ( typeof window . open === 'function' ) {
window . open ( knowledgeUrl , '_blank' , 'noopener,noreferrer' );
} else {
window . location . href = knowledgeUrl ;
}
return ;
}
2026-05-26 02:20:46 +08:00
if ( detail . contextKind === 'attachment' ) {
if ( action === 'copy-link' ) {
void copyTreeContextValue ( detail . href || '' , 'attachment-copy-link' );
return ;
}
if ( action === 'download' ) {
openEditorAttachmentDownload ( detail );
return ;
}
if ( action === 'popup-preview' ) {
openEditorAttachmentDetail ( detail );
return ;
}
if ( action === 'new-window' ) {
openEditorAttachmentNewWindow ( detail );
return ;
}
if ( action === 'new-window-edit' ) {
withOfficeEditModeGuard ( function () {
openEditorAttachmentNewWindow ( detail , 'edit' );
});
return ;
}
if ( action === 'open-edit-mode' ) {
withOfficeEditModeGuard ( function () {
void openEditorAttachmentEditTab ( detail );
});
return ;
}
if ( action === 'right-preview' ) {
dispatchSidebarEvent ( 'tree.attachment.open-right' , detail );
return ;
}
if ( action === 'copy-id' ) {
void copyTreeContextValue ( detail . assetId || '' , 'attachment-copy-id' );
return ;
}
dispatchSidebarEvent ( 'tree.attachment.action' , { action : action , attachment : detail });
return ;
}
var documentId = detail . documentId || '' ;
var workspaceId = detail . workspaceId || resolveWorkspaceId ( trigger || document . body );
var title = detail . title || '无标题' ;
var isAsset = detail . contextKind === 'filetree' && detail . assetId && detail . rowKind !== 'document' && detail . rowKind !== 'index' ;
2026-05-27 11:31:12 +08:00
if ( detail . contextKind === 'filetree' && action === 'toggle-sidebar-folder-shortcut' ) {
dispatchSidebarEvent ( 'tree.sidebarShortcut.toggleFolder' , detail );
return ;
}
2026-05-26 02:20:46 +08:00
if ( detail . contextKind === 'filetree' && action === 'download' ) {
downloadSelectedFileTreeAssetRows ( detail , trigger );
return ;
}
if ( isAsset && action === 'new-window' ) {
recordFileTreeAction ( 'new-window' , detail );
void openConvexAssetFromFileTree ({ ... detail , openTarget : 'new-window' });
return ;
}
if ( isAsset && action === 'open-edit-mode' ) {
2026-07-28 17:04:27 +08:00
withOfficeEditModeGuard ( async function () {
2026-05-26 02:20:46 +08:00
recordFileTreeAction ( 'open-edit-mode' , detail );
void openConvexAssetFromFileTree ({ ... detail , openTarget : 'edit-mode' });
});
return ;
}
if ( isAsset && action === 'open-right' ) {
recordFileTreeAction ( 'open-right' , detail );
void openConvexAssetFromFileTree ({ ... detail , openTarget : 'side' });
return ;
}
if ( action === 'open-right' ) {
recordFileTreeAction ( 'open-right' , detail );
dispatchSidebarEvent ( 'tree.page.open-right' , detail );
return ;
}
if ( action === 'share' ) {
dispatchSidebarEvent ( 'tree.page.share' , detail );
return ;
}
if ( action === 'copy-link' ) {
void copyTreeContextValue ( documentHref ( documentId , workspaceId ), 'copy-link' );
return ;
}
if ( action === 'copy-link-title' ) {
void copyTreeContextValue ( title + ' ' + documentHref ( documentId , workspaceId ), 'copy-link-title' );
return ;
}
if ( action === 'copy-reference-inline' ) {
void copyTreeContextValue ( '((' + title + ' ' + documentId + '))' , 'copy-reference-inline' );
return ;
}
if ( action === 'copy-reference-embed' ) {
void copyTreeContextValue ( '{{' + title + ' ' + documentId + '}}' , 'copy-reference-embed' );
return ;
}
if ( action === 'copy-id' ) {
2026-05-27 11:31:12 +08:00
var copyIdValue = currentSourceKind () === 'local_folder'
? fileTreeCopyLocalAbsolutePath ( detail , trigger )
: '' ;
void copyTreeContextValue ( copyIdValue || documentId || detail . assetId || detail . rowId || '' , 'copy-id' );
2026-05-26 02:20:46 +08:00
return ;
}
if ( action === 'delete-trash' && detail . contextKind === 'filetree' ) {
var selectedFileTreeRows = selectedSidebarFileTreeRows ();
if ( selectedFileTreeRows . length > 1 ) {
void deleteSelectedSidebarFileTreeRows ( trigger || document . body );
return ;
}
}
if ( action === 'delete-trash' && isAsset ) {
2026-07-28 17:04:27 +08:00
if ( ! ( await window . mnote . confirm ( '确定要将“' + title + '”删除到垃圾桶吗?' ))) return ;
2026-05-26 02:20:46 +08:00
recordFileTreeAction ( 'delete-trash' , detail );
recordFileTreeActionStatus ( 'pending' , detail );
void deleteSingleFileTreeAsset ( detail , trigger ). then ( function () {
recordFileTreeActionStatus ( 'archived' , Object . assign ({}, detail , { undo : 'trash-modal' }));
if ( currentSourceKind () === 'local_folder' ) void refreshLocalFolderSidebarSnapshot ();
2026-07-28 17:04:27 +08:00
}). catch ( async function ( error ) {
2026-05-26 02:20:46 +08:00
recordFileTreeActionStatus ( 'failed' , Object . assign ({}, detail , { fallback : 'alert' }));
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( error && error . message ? error . message : '资源删除失败' );
2026-05-26 02:20:46 +08:00
});
return ;
}
if ( action === 'new-file' ) {
void createPage ( trigger || document . body , fileTreeMenuTargetParentId ( detail , trigger ) || null );
return ;
}
if ( action === 'new-folder' ) {
recordFileTreeAction ( 'new-folder' , detail );
void createFileTreeFolder ( trigger || document . body , fileTreeMenuTargetParentId ( detail , trigger ) || null ). then ( function ( ok ) {
recordFileTreeActionStatus ( ok ? 'created' : 'skipped' , detail );
2026-07-28 17:04:27 +08:00
}). catch ( async function ( error ) {
2026-05-26 02:20:46 +08:00
recordFileTreeActionStatus ( 'failed' , Object . assign ({}, detail , { fallback : 'alert' }));
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( error && error . message ? error . message : '新建文件夹失败' );
2026-05-26 02:20:46 +08:00
});
return ;
}
2026-07-24 11:36:06 +08:00
if ( action === 'cut' || action === 'copy' ) {
var clipboardRows = selectedSidebarFileTreeRows ();
if ( ! clipboardRows . length && trigger && trigger . closest ) {
var single = trigger . closest ( '.tree-row[data-shell-mode="filetree"]' );
if ( single instanceof HTMLElement ) clipboardRows = [ single ];
}
var clipboardRowIds = clipboardRows . map ( function ( row ) {
return String ( row . getAttribute ( 'data-row-id' ) || '' ). trim ();
}). filter ( Boolean );
if ( ! clipboardRowIds . length ) {
recordFileTreeActionStatus ( 'skipped' , Object . assign ({}, detail , { reason : 'empty-selection' }));
return ;
}
setSidebarFileTreeClipboard ( action , clipboardRowIds );
recordFileTreeAction ( action , Object . assign ({}, detail , {
sourceRowIds : clipboardRowIds ,
clipboardAction : action ,
}));
recordFileTreeActionStatus ( 'applied' , Object . assign ({}, detail , {
sourceRowIds : clipboardRowIds ,
clipboardAction : action ,
}));
return ;
}
2026-05-26 02:20:46 +08:00
if ( action === 'paste-into' ) {
var pasteRow = trigger && trigger . closest ? trigger . closest ( '.tree-row[data-shell-mode="filetree"]' ) : trigger ;
recordFileTreeAction ( 'paste-into' , detail );
void pasteSidebarFileTreeClipboard ( pasteRow ). then ( function ( ok ) {
recordFileTreeActionStatus ( ok ? 'applied' : 'skipped' , detail );
2026-07-28 17:04:27 +08:00
}). catch ( async function ( error ) {
2026-05-26 02:20:46 +08:00
recordFileTreeActionStatus ( 'failed' , Object . assign ({}, detail , { fallback : 'alert' }));
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( error && error . message ? error . message : '粘贴失败' );
2026-05-26 02:20:46 +08:00
});
return ;
}
if ( action === 'copy-path' ) {
void copyTreeContextValue ( fileTreeCopyPath ( detail , trigger ), 'copy-path' );
return ;
}
if ( action === 'refresh' ) {
document . documentElement . setAttribute ( 'data-mnote-filetree-refresh-requested' , 'true' );
dispatchSidebarEvent ( 'tree.filetree.refresh' , detail );
return ;
}
if ( action === 'collapse-all' ) {
document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[aria-expanded="true"]' ). forEach ( function ( row ) {
if ( ! ( row instanceof HTMLElement )) return ;
row . setAttribute ( 'aria-expanded' , 'false' );
var node = row . closest ( '.tree-node' );
var children = node ? node . querySelector ( ':scope > .tree-children' ) : null ;
if ( children ) children . classList . add ( 'tree-children--collapsed' );
});
return ;
}
if ( action === 'reveal' ) {
if ( trigger && typeof trigger . scrollIntoView === 'function' ) {
trigger . scrollIntoView ({ block : 'nearest' });
trigger . focus && trigger . focus ();
}
return ;
}
if ( action === 'duplicate' ) {
dispatchSidebarEvent ( 'tree.page.duplicate' , detail );
return ;
}
if ( action === 'rename' ) {
if ( detail . contextKind === 'filetree' && trigger ) {
var renameRow = trigger . closest ? trigger . closest ( '.tree-row[data-shell-mode="filetree"]' ) : trigger ;
if ( beginFileTreeInlineRename ( renameRow )) return ;
}
var nextTitle = window . prompt ( '重命名页面' , title );
if ( nextTitle && nextTitle . trim () && documentId ) {
void dispatchTreeCommand ( trigger || document . body , {
action : 'rename' ,
workspaceId : workspaceId ,
documentId : documentId ,
title : nextTitle . trim ()
2026-07-28 17:04:27 +08:00
}). then ( async function (){ updateTitleEverywhere ( documentId , nextTitle . trim ()); });
2026-05-26 02:20:46 +08:00
}
return ;
}
if ( action === 'create-child' ) {
void createPage ( trigger || document . body , documentId );
return ;
}
if ( action === 'convert-child' ) {
2026-07-28 17:04:27 +08:00
void convertToPreviousSiblingChild ( trigger , detail );
2026-05-26 02:20:46 +08:00
return ;
}
var deleteTargetId = documentId || String ( detail . rowId || '' ). trim ();
if ( action === 'delete-trash' && deleteTargetId ) {
2026-07-28 17:04:27 +08:00
if ( ! ( await window . mnote . confirm ( '确定要将“' + title + '”删除到垃圾桶吗?' ))) return ;
2026-05-26 02:20:46 +08:00
recordFileTreeAction ( 'delete-trash' , detail );
recordFileTreeActionStatus ( 'pending' , detail );
void dispatchTreeCommand ( trigger || document . body , {
action : 'archive' ,
workspaceId : workspaceId ,
documentId : deleteTargetId
}). then ( function () {
recordFileTreeActionStatus ( 'archived' , Object . assign ({}, detail , { undo : 'trash-modal' }));
if ( currentSourceKind () === 'local_folder' ) void refreshLocalFolderSidebarSnapshot ();
2026-07-28 17:04:27 +08:00
}). catch ( async function ( error ) {
2026-05-26 02:20:46 +08:00
recordFileTreeActionStatus ( 'failed' , Object . assign ({}, detail , { fallback : 'alert' }));
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( error && error . message ? error . message : '删除失败' );
2026-05-26 02:20:46 +08:00
});
}
}
function appendTreeContextMenuButton ( menu , item , detail , trigger ) {
if ( item . separator ) {
var sep = document . createElement ( 'div' );
sep . className = 'mnote-tree-context-menu__separator' ;
sep . setAttribute ( 'role' , 'separator' );
menu . appendChild ( sep );
return ;
}
2026-07-03 23:20:16 +08:00
// Submenu root: render parent button with child flyout
if ( item . children && Array . isArray ( item . children ) && item . children . length > 0 ) {
if ( typeof injectKnowledgeSubmenuStyles === 'function' ) injectKnowledgeSubmenuStyles ();
var rootBtn = document . createElement ( 'button' );
rootBtn . type = 'button' ;
rootBtn . className = 'mnote-tree-context-menu__item mnote-tree-context-menu__item--submenu-root' ;
rootBtn . setAttribute ( 'role' , 'menuitem' );
rootBtn . setAttribute ( 'data-action' , item . action || 'knowledge-rag-root' );
rootBtn . setAttribute ( 'data-command-id' , item . commandId || item . action || 'knowledge-rag-root' );
if ( item . when ) rootBtn . setAttribute ( 'data-command-when' , item . when );
if ( item . danger || item . destructive ) rootBtn . setAttribute ( 'data-destructive' , 'true' );
if ( item . requiresApproval ) rootBtn . setAttribute ( 'data-requires-approval' , 'true' );
rootBtn . disabled = item . disabled === true ;
if ( item . title ) rootBtn . title = item . title ;
if ( item . disabled === true && item . title ) rootBtn . setAttribute ( 'data-disabled-reason' , item . title );
var rootIcon = document . createElement ( 'span' );
rootIcon . className = 'material-symbols-outlined mnote-tree-context-menu__icon' ;
rootIcon . setAttribute ( 'aria-hidden' , 'true' );
rootIcon . setAttribute ( 'data-icon' , item . icon || 'radio_button_unchecked' );
var rootLabel = document . createElement ( 'span' );
rootLabel . className = 'mnote-tree-context-menu__label' ;
rootLabel . textContent = item . label ;
var arrow = document . createElement ( 'span' );
arrow . className = 'mnote-tree-context-menu__submenu-arrow' ;
arrow . textContent = '▶' ;
arrow . setAttribute ( 'aria-hidden' , 'true' );
rootBtn . appendChild ( rootIcon );
rootBtn . appendChild ( rootLabel );
rootBtn . appendChild ( arrow );
var submenu = document . createElement ( 'div' );
submenu . className = 'mnote-tree-context-menu__submenu' ;
submenu . setAttribute ( 'data-testid' , 'mnote-knowledge-submenu' );
submenu . setAttribute ( 'role' , 'menu' );
function renderSubmenuChildren ( children ) {
submenu . textContent = '' ;
children . forEach ( function ( child ) {
if ( child . separator ) {
var childSep = document . createElement ( 'div' );
childSep . className = 'mnote-tree-context-menu__separator' ;
childSep . setAttribute ( 'role' , 'separator' );
submenu . appendChild ( childSep );
return ;
}
var childBtn = document . createElement ( 'button' );
childBtn . type = 'button' ;
childBtn . className = child . danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item' ;
if ( child . heading ) childBtn . className += ' mnote-tree-context-menu__item--heading' ;
childBtn . setAttribute ( 'role' , 'menuitem' );
childBtn . setAttribute ( 'data-action' , child . action || '' );
childBtn . setAttribute ( 'data-command-id' , child . commandId || child . action || '' );
if ( child . when ) childBtn . setAttribute ( 'data-command-when' , child . when );
childBtn . disabled = child . disabled === true || child . heading === true ;
if ( child . title ) childBtn . title = child . title ;
if ( child . disabled === true && child . title ) childBtn . setAttribute ( 'data-disabled-reason' , child . title );
var childIcon = document . createElement ( 'span' );
childIcon . className = 'material-symbols-outlined mnote-tree-context-menu__icon' ;
childIcon . setAttribute ( 'aria-hidden' , 'true' );
childIcon . setAttribute ( 'data-icon' , child . icon || 'radio_button_unchecked' );
var childLabel = document . createElement ( 'span' );
childLabel . className = 'mnote-tree-context-menu__label' ;
childLabel . textContent = child . label ;
childBtn . appendChild ( childIcon );
childBtn . appendChild ( childLabel );
if ( child . shortcut ) {
var childShortcut = document . createElement ( 'span' );
childShortcut . className = 'mnote-tree-context-menu__shortcut' ;
childShortcut . textContent = child . shortcut ;
childBtn . appendChild ( childShortcut );
}
var childDetail = child . kbId ? Object . assign ({}, detail , { kbId : child . kbId , providerKbId : child . providerKbId || child . kbId }) : detail ;
( function ( act , actDetail , actTrigger ) {
childBtn . addEventListener ( 'click' , function ( event ) {
event . preventDefault ();
event . stopPropagation ();
if ( ! act || childBtn . disabled ) return ;
handleTreeContextMenuAction ( act , actDetail , actTrigger );
});
})( child . action , childDetail , trigger );
submenu . appendChild ( childBtn );
});
}
renderSubmenuChildren ( item . children );
rootBtn . appendChild ( submenu );
var submenuShown = false ;
var submenuHideTimer = null ;
var lastSubmenuPointerY = 0 ;
function knowledgeSubmenuPosition ( pointerY ) {
var btnRect = rootBtn . getBoundingClientRect ();
submenu . style . display = 'block' ;
submenu . style . visibility = 'hidden' ;
var smRect = submenu . getBoundingClientRect ();
var leftPos = btnRect . right + 4 ;
var anchorY = Number ( pointerY || lastSubmenuPointerY || 0 );
var topPos = anchorY > 0 ? anchorY - 18 : btnRect . top ;
if ( leftPos + smRect . width > window . innerWidth - 8 ) {
leftPos = Math . max ( 8 , btnRect . left - smRect . width - 4 );
}
if ( topPos + smRect . height > window . innerHeight - 8 ) {
topPos = Math . max ( 8 , window . innerHeight - smRect . height - 8 );
}
submenu . style . left = leftPos + 'px' ;
submenu . style . top = topPos + 'px' ;
submenu . style . visibility = 'visible' ;
}
function knowledgeSubmenuShow ( pointerY ) {
if ( Number ( pointerY || 0 ) > 0 ) lastSubmenuPointerY = pointerY ;
if ( submenuHideTimer ) { window . clearTimeout ( submenuHideTimer ); submenuHideTimer = null ; }
if ( ! submenuShown ) { knowledgeSubmenuPosition ( pointerY ); submenuShown = true ; rootBtn . classList . add ( 'mnote-tree-context-menu__item--submenu-open' ); }
}
function knowledgeSubmenuScheduleHide () {
if ( submenuHideTimer ) window . clearTimeout ( submenuHideTimer );
submenuHideTimer = window . setTimeout ( function () {
if ( submenuShown ) { submenu . style . display = 'none' ; submenuShown = false ; rootBtn . classList . remove ( 'mnote-tree-context-menu__item--submenu-open' ); }
submenuHideTimer = null ;
}, 150 );
}
rootBtn . addEventListener ( 'mouseenter' , function ( event ) { if ( ! rootBtn . disabled ) knowledgeSubmenuShow ( event . clientY ); });
rootBtn . addEventListener ( 'mousemove' , function ( event ) {
if ( rootBtn . disabled || ! submenuShown ) return ;
lastSubmenuPointerY = event . clientY ;
});
rootBtn . addEventListener ( 'mouseleave' , function ( event ) {
if ( event . relatedTarget && ( submenu . contains ( event . relatedTarget ) || rootBtn . contains ( event . relatedTarget ))) return ;
knowledgeSubmenuScheduleHide ();
});
submenu . addEventListener ( 'mouseenter' , function () {
if ( submenuHideTimer ) { window . clearTimeout ( submenuHideTimer ); submenuHideTimer = null ; }
});
submenu . addEventListener ( 'mouseleave' , function ( event ) {
if ( event . relatedTarget && ( rootBtn . contains ( event . relatedTarget ) || submenu . contains ( event . relatedTarget ))) return ;
knowledgeSubmenuScheduleHide ();
});
if ( typeof item . loadChildren === 'function' ) {
item . loadChildren (). then ( function ( children ) {
if ( ! Array . isArray ( children ) || children . length === 0 ) return ;
renderSubmenuChildren ( children );
if ( submenuShown ) knowledgeSubmenuPosition ( lastSubmenuPointerY );
}). catch ( function () {});
}
menu . appendChild ( rootBtn );
return ;
}
2026-05-26 02:20:46 +08:00
var button = document . createElement ( 'button' );
button . type = 'button' ;
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 );
2026-05-28 22:01:44 +08:00
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' );
2026-05-26 02:20:46 +08:00
button . disabled = item . disabled === true ;
if ( item . title ) button . title = item . title ;
2026-05-28 22:01:44 +08:00
if ( item . disabled === true && item . title ) button . setAttribute ( 'data-disabled-reason' , item . title );
2026-05-26 02:20:46 +08:00
var icon = document . createElement ( 'span' );
icon . className = 'material-symbols-outlined mnote-tree-context-menu__icon' ;
icon . setAttribute ( 'aria-hidden' , 'true' );
icon . setAttribute ( 'data-icon' , item . icon || 'radio_button_unchecked' );
var label = document . createElement ( 'span' );
label . className = 'mnote-tree-context-menu__label' ;
label . textContent = item . label ;
button . appendChild ( icon );
button . appendChild ( label );
if ( item . shortcut ) {
var shortcut = document . createElement ( 'span' );
shortcut . className = 'mnote-tree-context-menu__shortcut' ;
shortcut . textContent = item . shortcut ;
button . appendChild ( shortcut );
}
button . addEventListener ( 'click' , function ( event ) {
event . preventDefault ();
event . stopPropagation ();
handleTreeContextMenuAction ( item . action , detail , trigger );
});
menu . appendChild ( button );
}
2026-07-03 23:20:16 +08:00
// ── 知识库二级菜单 CSS 注入(仅一次) ──
var __mnoteKnowledgeSubmenuStylesInjected = false ;
function injectKnowledgeSubmenuStyles () {
if ( __mnoteKnowledgeSubmenuStylesInjected ) return ;
__mnoteKnowledgeSubmenuStylesInjected = true ;
var style = document . createElement ( 'style' );
style . id = 'mnote-knowledge-submenu-styles' ;
style . textContent = '.mnote-tree-context-menu__item--submenu-root{position:relative}.mnote-tree-context-menu__submenu{position:fixed;z-index:calc(var(--wolai-z-context-menu,9999) + 1);width:240px;min-width:220px;max-width:min(340px,calc(100vw - 16px));padding:6px;border:1px solid rgba(27,28,28,.08);border-radius:6px;background:#FFF;box-shadow:0 8px 20px rgba(27,28,28,.12);backdrop-filter:blur(24px);color:var(--atelier-text);display:none}.mnote-tree-context-menu__submenu-arrow{margin-left:auto;font-size:11px;color:#6D6A65;padding-left:12px}.mnote-tree-context-menu__item--submenu-open{background:#F4F3F3}.mnote-tree-context-menu__item--heading{font-size:12px;color:#6D6A65;background:transparent!important;cursor:default}' ;
document . head . appendChild ( style );
}
2026-07-28 17:04:27 +08:00
// ── 知识库列表获取:只读 MNote 后端 registry/status,不直连外部知识库 service key。 ──
2026-07-03 23:20:16 +08:00
function fetchKnowledgeBaseList ( detail ) {
var rootUri = knowledgeRagRootUri ( detail , null );
if ( ! rootUri ) return Promise . resolve ([]);
var workspaceId = String ( detail && detail . workspaceId || resolveWorkspaceId ( document . body ) || '' ). trim ();
var sourcePath = knowledgeRagSourceRelativePath ( detail );
var statusUrl = '/api/knowledge-rag/status?rootUri=' + encodeURIComponent ( rootUri );
if ( workspaceId ) statusUrl += '&workspaceId=' + encodeURIComponent ( workspaceId );
return fetch ( statusUrl , { credentials : 'include' , headers : { accept : 'application/json' } })
. then ( function ( response ) { return response . ok ? response . json () : null ; })
. then ( function ( payload ) {
// 构建 knowledgeBase → 已索引 sourcePath 快速查找
var indexedByKb = {};
var registry = payload && payload . registry ;
if ( registry && Array . isArray ( registry . entries ) && sourcePath ) {
registry . entries . forEach ( function ( entry ) {
var entryKbId = String ( entry . providerKnowledgeBaseId || entry . provider_kb_id || '' ). trim ();
var entryPath = String ( entry . sourceRootRelativePath || entry . source_root_relative_path || '' ). trim ();
if ( entryKbId && entryPath && entry . indexedAtMs && entryPath === sourcePath ) {
if ( ! indexedByKb [ entryKbId ]) indexedByKb [ entryKbId ] = {};
indexedByKb [ entryKbId ][ entryPath ] = true ;
}
});
}
var bases = payload && payload . knowledgeBases && Array . isArray ( payload . knowledgeBases . bases )
? payload . knowledgeBases . bases
: [];
return bases . filter ( function ( base ) {
return base && base . canWrite !== false && ( base . providerKbId || base . provider_kb_id || base . baseId || base . base_id );
}). map ( function ( base ) {
var providerKbId = String ( base . providerKbId || base . provider_kb_id || '' ). trim ();
var baseId = String ( base . baseId || base . base_id || providerKbId ). trim ();
var kbId = providerKbId || baseId ;
return {
id : baseId ,
providerKbId : kbId ,
name : String ( base . name || baseId || providerKbId || '未命名知识库' ). trim (),
sourceCount : Number ( base . sourceCount || base . source_count || 0 ) || 0 ,
sourceAlreadyIndexed : Boolean ( sourcePath && indexedByKb [ kbId ] && indexedByKb [ kbId ][ sourcePath ])
};
});
}). catch ( function () { return []; });
}
function buildKnowledgeRagSubmenuChildren ( detail , writableBases ) {
var children = [
{ action : 'knowledge-rag-index' , icon : 'travel_explore' , label : '索引到默认知识库' , when : '!workspace.readonly' },
{ separator : true },
{ heading : true , icon : 'history' , label : '最近使用知识库' }
];
if ( writableBases && writableBases . length > 0 ) {
writableBases . slice ( 0 , 3 ). forEach ( function ( base ) {
var indexed = base . sourceAlreadyIndexed ;
children . push ({
action : 'knowledge-rag-index' ,
icon : indexed ? 'check_circle' : 'database' ,
label : ( indexed ? '✓ ' : '' ) + base . name ,
kbId : base . id ,
providerKbId : base . providerKbId ,
title : indexed ? '重新索引' : ( base . sourceCount ? ( '已有 ' + base . sourceCount + ' 个来源' ) : '加入此知识库' ),
when : '!workspace.readonly'
});
});
} else {
children . push ({ action : '' , icon : 'database' , label : '暂无可写知识库' , disabled : true });
}
children . push ({ heading : true , icon : 'folder_managed' , label : '当前 workspace 知识库' });
( writableBases || []). slice ( 3 ). forEach ( function ( base ) {
var indexed = base . sourceAlreadyIndexed ;
children . push ({
action : 'knowledge-rag-index' ,
icon : indexed ? 'check_circle' : 'database' ,
label : ( indexed ? '✓ ' : '' ) + base . name ,
kbId : base . id ,
providerKbId : base . providerKbId ,
title : indexed ? '重新索引' : ( base . sourceCount ? ( '已有 ' + base . sourceCount + ' 个来源' ) : '加入此知识库' ),
when : '!workspace.readonly'
});
});
children . push (
{ separator : true },
{ action : 'open-knowledge-page' , icon : 'add' , label : '新建知识库...' },
{ action : 'open-knowledge-page' , icon : 'settings' , label : '管理知识库...' }
);
return children ;
}
2026-05-26 02:20:46 +08:00
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
2026-05-28 22:01:44 +08:00
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 ) {
2026-05-26 02:20:46 +08:00
var runtime = window . __mnoteFileTreeContextMenuRuntime ;
if ( runtime && typeof runtime . buildSidebarFileTreeContext === 'function' ) {
try {
2026-05-28 22:01:44 +08:00
var targetRow = commandContextTargetRow ( detail );
2026-05-26 02:20:46 +08:00
return runtime . buildSidebarFileTreeContext ( kind , {
selection : sidebarFileTreeSelection ,
currentSourceKind : currentSourceKind ,
workspaceReadonly : function () {
return document . documentElement . getAttribute ( 'data-mnote-workspace-readonly' ) === 'true' ;
},
queryRowById : function ( rowId ) {
return document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( rowId ) + '"]' );
2026-05-28 22:01:44 +08:00
},
targetRow : targetRow ,
openEditorsSnapshot : currentOpenEditorsSnapshotForCommandContext (),
bufferState : detail && detail . bufferState ,
2026-05-26 02:20:46 +08:00
});
} catch ( _ ) {}
}
var s = sidebarFileTreeSelection ;
var rowIds = Array . from ( s . selectedRowIds || []);
var rows = [];
for ( var i = 0 ; i < rowIds . length ; i += 1 ) {
var row = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( rowIds [ i ]) + '"]' );
if ( row instanceof HTMLElement ) rows . push ( row );
}
var resourceKinds = {};
for ( var i = 0 ; i < rows . length ; i += 1 ) {
var rk = rows [ i ]. getAttribute ( 'data-row-kind' ) || 'unknown' ;
resourceKinds [ rk ] = true ;
}
var rkKeys = Object . keys ( resourceKinds );
return {
'workspace.sourceKind' : currentSourceKind () || '' ,
'workspace.readonly' : document . documentElement . getAttribute ( 'data-mnote-workspace-readonly' ) === 'true' ,
'tree.focusKind' : kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind ,
'tree.selectionCount' : rows . length ,
'tree.selectionResourceKind' : rkKeys . length === 1 ? rkKeys [ 0 ] : 'mixed'
};
}
function evaluateSidebarFileTreeWhen ( ctx , expr ) {
var runtime = window . __mnoteFileTreeContextMenuRuntime ;
if ( runtime && typeof runtime . evaluateSidebarFileTreeWhen === 'function' ) {
return runtime . evaluateSidebarFileTreeWhen ( ctx || {}, expr || '' );
}
if ( ! expr ) return true ;
try {
expr = expr . trim ();
// 词法切分:支持 key、!key、==、!=、&&、|| 和括号分组
var tokens = [];
var i = 0 ;
while ( i < expr . length ) {
var ch = expr [ i ];
if ( ch === ' ' || ch === '\t' ) { i ++ ; continue ; }
if ( ch === '(' ) { tokens . push ({ t : '(' }); i ++ ; continue ; }
if ( ch === ')' ) { tokens . push ({ t : ')' }); i ++ ; continue ; }
if ( ch === '&' && expr [ i + 1 ] === '&' ) { tokens . push ({ t : '&&' }); i += 2 ; continue ; }
if ( ch === '|' && expr [ i + 1 ] === '|' ) { tokens . push ({ t : '||' }); i += 2 ; continue ; }
if ( ch === '=' && expr [ i + 1 ] === '=' ) { tokens . push ({ t : '==' }); i += 2 ; continue ; }
if ( ch === '!' && expr [ i + 1 ] === '=' ) { tokens . push ({ t : '!=' }); i += 2 ; continue ; }
if ( ch === '!' ) { tokens . push ({ t : '!' }); i ++ ; continue ; }
if ( ch === '"' || ch === "'" ) {
i ++ ;
var str = '' ;
while ( i < expr . length && expr [ i ] !== ch ) { str += expr [ i ]; i ++ ; }
if ( i < expr . length ) i ++ ;
tokens . push ({ t : 'str' , v : str });
continue ;
}
if (( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) || ch === '_' || ch === '.' || ( ch >= '0' && ch <= '9' )) {
var id = '' ;
while ( i < expr . length && (( expr [ i ] >= 'a' && expr [ i ] <= 'z' ) || ( expr [ i ] >= 'A' && expr [ i ] <= 'Z' ) || ( expr [ i ] >= '0' && expr [ i ] <= '9' ) || expr [ i ] === '_' || expr [ i ] === '.' )) { id += expr [ i ]; i ++ ; }
tokens . push ({ t : 'id' , v : id });
continue ;
}
throw new Error ( 'Unexpected char: ' + ch );
}
var pos = 0 ;
function peek () { return tokens [ pos ]; }
function consume () { return tokens [ pos ++ ]; }
function expect ( t ) {
var tok = consume ();
if ( ! tok || tok . t !== t ) throw new Error ( 'Expected ' + t + ' got ' + ( tok ? tok . t : 'EOF' ));
return tok ;
}
function ctxVal ( key ) {
var v = ctx [ key ];
if ( v === undefined || v === null ) return '' ;
return v ;
}
function parseOr () {
var left = parseAnd ();
while ( peek () && peek (). t === '||' ) { consume (); var right = parseAnd (); left = left || right ; }
return left ;
}
function parseAnd () {
var left = parsePrimary ();
while ( peek () && peek (). t === '&&' ) { consume (); var right = parsePrimary (); left = left && right ; }
return left ;
}
function parsePrimary () {
if ( ! peek ()) throw new Error ( 'Unexpected EOF' );
if ( peek (). t === '(' ) {
consume ();
var val = parseOr ();
expect ( ')' );
return val ;
}
if ( peek (). t === '!' ) {
consume ();
var keyTok = consume ();
if ( ! keyTok || keyTok . t !== 'id' ) throw new Error ( 'Expected key after !' );
return ! ctxVal ( keyTok . v );
}
if ( peek (). t === 'id' ) {
var keyTok = consume ();
if ( peek () && peek (). t === '==' ) {
consume ();
var valTok = consume ();
if ( ! valTok ) throw new Error ( 'Expected value after ==' );
var val = valTok . t === 'str' ? valTok . v : ( valTok . v || '' );
// 与 Rust evaluate_when 保持一致:按 string / bool / number 比较。
var cv = ctxVal ( keyTok . v );
if ( val === 'true' ) return cv === true || cv === 'true' ;
if ( val === 'false' ) return cv === false || cv === '' || cv === 'false' ;
var num = Number ( val );
if ( ! isNaN ( num ) && String ( num ) === val ) return Number ( cv ) === num ;
return String ( cv ) === val ;
}
if ( peek () && peek (). t === '!=' ) {
consume ();
var valTok = consume ();
if ( ! valTok ) throw new Error ( 'Expected value after !=' );
var val = valTok . t === 'str' ? valTok . v : ( valTok . v || '' );
var cv = ctxVal ( keyTok . v );
if ( val === 'true' ) return ! ( cv === true || cv === 'true' );
if ( val === 'false' ) return ! ( cv === false || cv === '' || cv === 'false' );
var num = Number ( val );
if ( ! isNaN ( num ) && String ( num ) === val ) return Number ( cv ) !== num ;
return String ( cv ) !== val ;
}
// 单独 key:按 truthy 规则判断。
var cv = ctxVal ( keyTok . v );
if ( typeof cv === 'boolean' ) return cv ;
if ( typeof cv === 'number' ) return cv !== 0 ;
return cv !== '' && cv !== undefined && cv !== null ;
}
throw new Error ( 'Unexpected token: ' + peek (). t );
}
return parseOr ();
} catch ( e ) {
return true ;
}
}
function openTreeContextMenu ( kind , detail , x , y , trigger ) {
closeTreeContextMenu ();
detail = Object . assign ({}, detail || {}, { contextKind : kind });
var menu = document . createElement ( 'div' );
menu . className = 'mnote-tree-context-menu' ;
menu . setAttribute ( 'role' , 'menu' );
menu . setAttribute ( 'data-testid' , 'mnote-tree-context-menu' );
menu . setAttribute ( 'data-kind' , kind );
var isAttachment = kind === 'attachment' ;
var isAsset = kind === 'filetree' && detail . assetId && detail . rowKind !== 'document' && detail . rowKind !== 'index' ;
var isFileTreeDownload = kind === 'filetree' && detail . downloadable ;
2026-05-28 22:01:44 +08:00
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 ));
2026-06-07 10:35:21 +08:00
var knowledgeRagSupported = supportsKnowledgeRagSource ( detail );
2026-05-26 02:20:46 +08:00
var items = isAttachment ? [
{ action : 'duplicate' , icon : 'file_copy' , label : '拷贝副本' , shortcut : 'Ctrl + D' },
2026-05-28 22:01:44 +08:00
{ action : 'delete-trash' , icon : 'delete' , label : '删除' , shortcut : 'Del' , danger : true , destructive : true , requiresApproval : true , when : '!workspace.readonly' },
2026-05-26 02:20:46 +08:00
{ separator : true },
{ action : 'copy-link' , icon : 'link' , label : '复制链接' },
{ action : 'move-embed' , icon : 'subdirectory_arrow_right' , label : '移动/嵌入到...' , shortcut : 'Alt+Shift+M/G' },
{ action : 'history' , icon : 'history' , label : '块历史...' },
{ separator : true },
{ action : 'popup-preview' , icon : 'preview' , label : '弹窗预览' },
{ action : 'right-preview' , icon : 'right_panel_open' , label : '右侧预览' },
{ action : 'open-edit-mode' , icon : 'edit_note' , label : '弹窗编辑' },
{ action : 'new-window-edit' , icon : 'open_in_new' , label : '新窗口编辑' },
{ action : 'new-window' , icon : 'open_in_new' , label : '在新窗口打开' },
{ action : 'download' , icon : 'download' , label : '下载' },
{ action : 'replace-file' , icon : 'sync' , label : '更换文件' },
{ action : 'rename-attachment' , icon : 'drive_file_rename_outline' , label : '重命名' },
{ action : 'comment' , icon : 'mode_comment' , label : '评论' , shortcut : 'Ctrl+Alt+M' },
{ action : 'caption' , icon : 'notes' , label : '添加说明文字' },
{ separator : true },
{ action : 'color' , icon : 'format_paint' , label : '颜色' }
] : isAsset ? [
{ 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 ) + ' 个项目' : '下载' },
2026-05-28 22:01:44 +08:00
{ action : 'rename' , icon : 'edit' , label : '重命名' , shortcut : 'F2' , when : '!workspace.readonly && !editor.dirty' },
2026-05-26 02:20:46 +08:00
{ action : 'copy-id' , icon : 'tag' , label : '复制资源 ID' },
2026-05-28 22:01:44 +08:00
{ action : 'delete-trash' , icon : 'delete' , label : '删除' , shortcut : 'Del' , danger : true , destructive : true , requiresApproval : true , when : '!workspace.readonly && !editor.dirty' },
2026-05-26 02:20:46 +08:00
{ separator : true },
{ action : 'copy-path' , icon : 'content_copy' , label : 'Copy Path' },
{ action : 'refresh' , icon : 'refresh' , label : 'Refresh' },
{ action : 'reveal' , icon : 'my_location' , label : 'Reveal' }
] : kind === 'filetree' ? [
{ action : 'open-right' , icon : 'right_panel_open' , label : '在右侧边栏打开' , shortcut : 'Alt+' },
{ action : 'download' , icon : 'download' , label : Number ( detail . selectedDownloadCount || 0 ) > 1 ? '下载 ' + Number ( detail . selectedDownloadCount || 0 ) + ' 个项目' : '下载' , disabled : ! isFileTreeDownload , title : isFileTreeDownload ? '下载当前本地文件或文件夹' : '当前项目没有可下载的本地路径' },
{ separator : true },
{ action : 'copy-link' , icon : 'link' , label : '复制访问链接' },
{ action : 'copy-reference-inline' , icon : 'content_copy' , label : '复制页面引用链接' },
{ action : 'copy-id' , icon : 'tag' , label : '复制页面ID' },
{ action : 'copy-path' , icon : 'content_copy' , label : 'Copy Path' },
{ separator : true },
{ action : 'new-file' , icon : 'note_add' , label : 'New File' , when : '!workspace.readonly' },
{ action : 'new-folder' , icon : 'create_new_folder' , label : 'New Folder' , disabled : currentSourceKind () !== 'local_folder' , title : currentSourceKind () === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹' , when : '!workspace.readonly' },
2026-05-27 11:31:12 +08:00
{ action : 'toggle-sidebar-folder-shortcut' , icon : 'star' , label : '加入/取消星标置顶' , disabled : currentSourceKind () !== 'local_folder' || ( detail . rowKind !== 'folder' && detail . rowKind !== 'directory' ), title : currentSourceKind () === 'local_folder' ? '把当前文件夹加入或移出星标置顶' : '仅 local folder 文件夹支持星标置顶' },
2026-07-24 11:36:06 +08:00
{ separator : true },
{ action : 'cut' , icon : 'content_cut' , label : '剪切' , shortcut : 'Ctrl+X' , when : '!workspace.readonly' },
{ action : 'copy' , icon : 'content_copy' , label : '复制' , shortcut : 'Ctrl+C' },
{ action : 'paste-into' , icon : 'content_paste' , label : '粘贴到此处' , shortcut : 'Ctrl+V' , disabled : ! currentSidebarFileTreeClipboard (), title : currentSidebarFileTreeClipboard () ? '粘贴到当前文件树目标' : '剪贴板为空' , when : '!workspace.readonly' },
2026-05-26 02:20:46 +08:00
{ action : 'refresh' , icon : 'refresh' , label : 'Refresh' },
{ action : 'collapse-all' , icon : 'unfold_less' , label : 'Collapse All' },
{ action : 'reveal' , icon : 'my_location' , label : 'Reveal' },
{ separator : true },
2026-05-28 22:01:44 +08:00
{ action : 'rename' , icon : 'edit' , label : '重命名' , shortcut : 'F2' , when : '!workspace.readonly && !editor.dirty' },
2026-05-26 02:20:46 +08:00
{ separator : true },
2026-05-28 22:01:44 +08:00
{ action : 'delete-trash' , icon : 'delete' , label : '删除' , shortcut : 'Del' , danger : true , destructive : true , requiresApproval : true , when : '!workspace.readonly && !editor.dirty' }
2026-05-26 02:20:46 +08:00
] : [
{ action : 'open-right' , icon : 'right_panel_open' , label : '在右侧边栏打开' , shortcut : 'Alt+' },
{ separator : true },
{ action : 'copy-link' , icon : 'link' , label : '复制访问链接' },
{ action : 'copy-reference-inline' , icon : 'content_copy' , label : '复制页面引用链接' },
{ action : 'copy-id' , icon : 'tag' , label : '复制页面ID' },
{ separator : true },
2026-05-28 22:01:44 +08:00
{ action : 'rename' , icon : 'edit' , label : '重命名' , when : '!workspace.readonly && !editor.dirty' },
2026-05-26 02:20:46 +08:00
{ separator : true },
2026-05-28 22:01:44 +08:00
{ action : 'delete-trash' , icon : 'delete' , label : '删除' , shortcut : 'Del' , danger : true , destructive : true , requiresApproval : true , when : '!workspace.readonly && !editor.dirty' }
2026-05-26 02:20:46 +08:00
];
2026-06-07 10:35:21 +08:00
if ( currentSourceKind () === 'local_folder' && detail . rowKind !== 'index' && knowledgeRagSupported ) {
2026-07-03 23:20:16 +08:00
var ragChildren = buildKnowledgeRagSubmenuChildren ( detail , []);
var ragItem = { action : 'knowledge-rag-root' , icon : 'travel_explore' , label : '加入知识库' , children : ragChildren , when : '!workspace.readonly' };
ragItem . loadChildren = function () {
return fetchKnowledgeBaseList ( detail ). then ( function ( bases ) {
return buildKnowledgeRagSubmenuChildren ( detail , bases );
});
};
2026-06-07 01:10:31 +08:00
var insertAt = isAttachment ? 11 : isAsset ? 3 : 3 ;
if ( insertAt >= 0 ) items . splice ( insertAt , 0 , ragItem );
2026-06-01 10:07:42 +08:00
}
2026-05-26 02:20:46 +08:00
items . forEach ( function ( item ) {
if ( item . when !== undefined && ! evaluateSidebarFileTreeWhen ( ctx , item . when )) {
2026-05-28 22:01:44 +08:00
var reason = ctx [ 'workspace.readonly' ] === true
? '当前工作区只读'
: ctx [ 'editor.dirty' ] === true
? '当前目标有未保存修改'
: '当前上下文不支持此操作' ;
item = Object . assign ({}, item , { disabled : true , title : item . title || reason });
2026-05-26 02:20:46 +08:00
}
appendTreeContextMenuButton ( menu , item , detail , trigger );
});
document . body . appendChild ( menu );
var rect = menu . getBoundingClientRect ();
var left = Math . min ( Math . max ( 8 , x || 8 ), Math . max ( 8 , window . innerWidth - rect . width - 8 ));
var top = Math . min ( Math . max ( 8 , y || 8 ), Math . max ( 8 , window . innerHeight - rect . height - 8 ));
menu . style . left = left + 'px' ;
menu . style . top = top + 'px' ;
runtimeState . activeTreeContextMenu = menu ;
}
function openPageTreeContextMenu ( row , x , y , trigger ) {
if ( ! ( row instanceof HTMLElement )) return ;
var documentId = row . getAttribute ( 'data-node-id' ) || '' ;
openTreeContextMenu ( 'page' , {
documentId : documentId ,
rowId : documentId ,
rowKind : 'document' ,
title : rowTitle ( row ),
workspaceId : resolveWorkspaceId ( row )
}, x , y , trigger || row );
}
function openFileTreeContextMenu ( row , x , y , trigger ) {
if ( ! ( row instanceof HTMLElement )) return ;
var documentId = row . getAttribute ( 'data-document-id' ) || row . getAttribute ( 'data-doc-id' ) || '' ;
var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload ( row );
var selectedAssetRows = selectedDownloadRows . filter ( isFileTreeDownloadableAssetRow );
2026-05-28 22:01:44 +08:00
var workspacePath = readWorkspacePathFromRow ( row );
2026-05-26 02:20:46 +08:00
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 ),
2026-05-28 22:01:44 +08:00
workspacePath : workspacePath ,
targetRow : row ,
2026-05-26 02:20:46 +08:00
title : rowTitle ( row ),
workspaceId : resolveWorkspaceId ( row ),
downloadable : isFileTreeDownloadableRow ( row ),
selectedDownloadCount : selectedDownloadRows . length ,
selectedDownloadRowIds : selectedDownloadRows . map ( function ( downloadRow ) { return downloadRow . getAttribute ( 'data-row-id' ) || '' ; }). filter ( Boolean ),
selectedAssetCount : selectedAssetRows . length ,
selectedAssetRowIds : selectedAssetRows . map ( function ( assetRow ) { return assetRow . getAttribute ( 'data-row-id' ) || '' ; }). filter ( Boolean )
}, x , y , trigger || row );
}
function visibleFileTreeRows () {
var runtimeFn = fileTreeSelectionRuntimeFunction ( 'visibleFileTreeRows' );
if ( runtimeFn ) return runtimeFn ( fileTreeSelectionRuntimeDeps ());
return Array . from ( document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]' ))
. filter ( function ( row ) {
if ( ! ( row instanceof HTMLElement )) return false ;
if ( row . closest ( '.tree-children--collapsed' )) return false ;
return row . offsetParent !== null || row . getClientRects (). length > 0 ;
});
}
function syncSidebarFileTreeSelection () {
var runtimeFn = fileTreeSelectionRuntimeFunction ( 'syncSidebarFileTreeSelection' );
if ( runtimeFn ) {
runtimeFn ( sidebarFileTreeSelection , fileTreeSelectionRuntimeDeps ());
return ;
}
var rows = visibleFileTreeRows ();
rows . forEach ( function ( row ) {
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
row . setAttribute ( 'data-selected' , String ( Boolean ( rowId && sidebarFileTreeSelection . selectedRowIds . has ( rowId ))));
row . setAttribute ( 'data-focused' , String ( rowId === sidebarFileTreeSelection . focusedRowId ));
});
window . dispatchEvent ( new CustomEvent ( 'tree.filetree.selection.changed' , {
detail : {
selectedRowIds : Array . from ( sidebarFileTreeSelection . selectedRowIds ),
anchorRowId : sidebarFileTreeSelection . anchorRowId ,
focusedRowId : sidebarFileTreeSelection . focusedRowId
}
}));
}
function selectSidebarFileTreeRow ( row , modifiers ) {
var runtimeFn = fileTreeSelectionRuntimeFunction ( 'selectSidebarFileTreeRow' );
if ( runtimeFn ) return runtimeFn ( row , sidebarFileTreeSelection , modifiers , fileTreeSelectionRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return [];
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
if ( ! rowId ) return [];
var rows = visibleFileTreeRows ();
var visibleRowIds = rows . map ( function ( item ) { return item . getAttribute ( 'data-row-id' ) || '' ; }). filter ( Boolean );
var selected = new Set ( sidebarFileTreeSelection . selectedRowIds );
var shiftKey = Boolean ( modifiers && modifiers . shiftKey );
var ctrlKey = Boolean ( modifiers && ( modifiers . ctrlKey || modifiers . metaKey ));
if ( shiftKey && sidebarFileTreeSelection . anchorRowId ) {
var anchorIndex = visibleRowIds . indexOf ( sidebarFileTreeSelection . anchorRowId );
var targetIndex = visibleRowIds . indexOf ( rowId );
if ( anchorIndex >= 0 && targetIndex >= 0 ) {
selected = new Set ( visibleRowIds . slice ( Math . min ( anchorIndex , targetIndex ), Math . max ( anchorIndex , targetIndex ) + 1 ));
} else {
selected = new Set ([ rowId ]);
sidebarFileTreeSelection . anchorRowId = rowId ;
}
} else if ( ctrlKey ) {
if ( selected . has ( rowId ) && selected . size > 1 ) selected . delete ( rowId );
else selected . add ( rowId );
sidebarFileTreeSelection . anchorRowId = rowId ;
} else {
selected = new Set ([ rowId ]);
sidebarFileTreeSelection . anchorRowId = rowId ;
}
sidebarFileTreeSelection . selectedRowIds = selected ;
sidebarFileTreeSelection . focusedRowId = rowId ;
syncSidebarFileTreeSelection ();
return Array . from ( selected );
}
function selectSidebarFileTreeDocument ( documentId , options ) {
var id = String ( documentId || '' ). trim ();
if ( ! id ) return false ;
2026-05-29 11:13:05 +08:00
var relativePath = options && options . relativePath
? String ( options . relativePath || '' ). trim ()
: id . indexOf ( 'local-md:' ) === 0
? decodeLocalEncodedPath ( id . slice ( 'local-md:' . length ))
: '' ;
var targetRowId = options && options . rowId ? String ( options . rowId || '' ). trim () : '' ;
var row = targetRowId
? document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( targetRowId ) + '"]' )
: null ;
if ( ! ( row instanceof HTMLElement )) {
row = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( 'doc:' + id ) + '"]' )
2026-05-26 02:20:46 +08:00
|| document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape ( id ) + '"]' )
|| document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="' + cssEscape ( id ) + '"]' );
2026-05-29 11:13:05 +08:00
}
if ( row instanceof HTMLElement && relativePath && fileTreeRowLocalRelativePath ( row ) !== relativePath ) {
row = null ;
}
if ( row instanceof HTMLElement && row . closest ( '.tree-children--collapsed' ) && relativePath && typeof revealFileTreeResource === 'function' ) {
void revealFileTreeResource ({
rootUri : options && options . rootUri ,
relativePath : relativePath ,
rowId : options && options . rowId ,
select : true ,
focus : true ,
scroll : options ? options . scrollIntoView !== false : true
});
return true ;
}
2026-05-27 11:31:12 +08:00
if ( ! ( row instanceof HTMLElement )) {
if ( relativePath && typeof revealFileTreeResource === 'function' ) {
void revealFileTreeResource ({
rootUri : options && options . rootUri ,
relativePath : relativePath ,
rowId : options && options . rowId ,
select : true ,
focus : true ,
scroll : options ? options . scrollIntoView !== false : true
2026-05-29 11:13:05 +08:00
}). then ( function ( revealed ) {
if ( revealed ) return ;
var fallbackParentPath = localMarkdownBundleParentPath ( relativePath );
var fallbackBundleRow = fallbackParentPath ? visibleFileTreeRowByRelativePath ( fallbackParentPath ) : null ;
if ( fallbackBundleRow instanceof HTMLElement ) activateSidebarFileTreeRow ( fallbackBundleRow , options );
}). catch ( function () {});
2026-05-27 11:31:12 +08:00
return true ;
}
2026-05-29 11:13:05 +08:00
var bundleParentPath = localMarkdownBundleParentPath ( relativePath );
var bundleRow = bundleParentPath ? visibleFileTreeRowByRelativePath ( bundleParentPath ) : null ;
if ( bundleRow instanceof HTMLElement ) {
return activateSidebarFileTreeRow ( bundleRow , options );
}
2026-05-27 11:31:12 +08:00
return false ;
2026-05-26 02:20:46 +08:00
}
2026-05-27 11:31:12 +08:00
return activateSidebarFileTreeRow ( row , options );
2026-05-26 02:20:46 +08:00
}
function selectSidebarFileTreeRowById ( rowId , options ) {
var id = String ( rowId || '' ). trim ();
if ( ! id ) return false ;
var row = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( id ) + '"]' );
if ( ! ( row instanceof HTMLElement )) return false ;
selectSidebarFileTreeRow ( row , { ctrlKey : false , metaKey : false , shiftKey : false });
document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]' ). forEach ( function ( activeRow ) {
if ( activeRow instanceof HTMLElement ) activeRow . setAttribute ( 'data-active' , 'false' );
});
row . setAttribute ( 'data-active' , 'true' );
if ( options && options . scrollIntoView !== false ) {
try {
row . scrollIntoView ({ block : 'nearest' });
} catch ( _ ) {
row . scrollIntoView ();
}
}
return true ;
}
function pendingLocalFolderRestoreRowId () {
if ( window . __mnotePendingLocalFolderRestoreFiletreeRowId ) {
return String ( window . __mnotePendingLocalFolderRestoreFiletreeRowId || '' ). trim ();
}
if ( ! window . localStorage ) return '' ;
var rowId = '' ;
try {
rowId = String ( window . localStorage . getItem ( 'mnote.pendingLocalFolderRestoreFiletreeRowId' ) || '' ). trim ();
} catch ( _ ) {
rowId = '' ;
}
if ( ! rowId && typeof window . name === 'string' && window . name . indexOf ( 'mnote.pendingLocalFolderRestoreFiletreeRowId=' ) === 0 ) {
rowId = decodeURIComponent ( window . name . slice ( 'mnote.pendingLocalFolderRestoreFiletreeRowId=' . length ));
}
if ( rowId ) window . __mnotePendingLocalFolderRestoreFiletreeRowId = rowId ;
return rowId ;
}
function clearPendingLocalFolderRestoreRowId () {
window . __mnotePendingLocalFolderRestoreFiletreeRowId = '' ;
try {
window . localStorage . removeItem ( 'mnote.pendingLocalFolderRestoreFiletreeRowId' );
} catch ( _ ) {}
if ( typeof window . name === 'string' && window . name . indexOf ( 'mnote.pendingLocalFolderRestoreFiletreeRowId=' ) === 0 ) {
window . name = '' ;
}
}
function applyPendingLocalFolderRestoreFocusOnce () {
var rowId = pendingLocalFolderRestoreRowId ();
if ( ! rowId ) return false ;
document . documentElement . setAttribute ( 'data-mnote-local-folder-restore-pending-row-id' , rowId );
if ( ! selectSidebarFileTreeRowById ( rowId , { scrollIntoView : true })) {
document . documentElement . setAttribute ( 'data-mnote-local-folder-restore-focus-status' , 'row-missing' );
return false ;
}
document . documentElement . setAttribute ( 'data-mnote-local-folder-restore-focused-row-id' , rowId );
document . documentElement . setAttribute ( 'data-mnote-local-folder-restore-focus-status' , 'focused' );
2026-05-29 11:13:05 +08:00
clearPendingLocalFolderRestoreRowId ();
2026-05-26 02:20:46 +08:00
return true ;
}
2026-05-29 11:13:05 +08:00
function schedulePendingLocalFolderRestoreFocus ( options ) {
2026-05-26 02:20:46 +08:00
if ( ! pendingLocalFolderRestoreRowId ()) return false ;
2026-05-29 11:13:05 +08:00
var reason = options && options . reason ? String ( options . reason ) : '' ;
var attempt = Math . max ( 0 , Number ( options && options . attempt || 0 ));
var focused = applyPendingLocalFolderRestoreFocusOnce ();
if ( focused ) return true ;
if ( attempt >= 4 ) {
document . documentElement . setAttribute ( 'data-mnote-local-folder-restore-focus-status' , 'timeout' );
return false ;
}
if ( window . __mnotePendingLocalFolderRestoreFocusFrame ) {
window . cancelAnimationFrame ( window . __mnotePendingLocalFolderRestoreFocusFrame );
window . __mnotePendingLocalFolderRestoreFocusFrame = 0 ;
}
window . __mnotePendingLocalFolderRestoreFocusFrame = window . requestAnimationFrame ( function () {
window . __mnotePendingLocalFolderRestoreFocusFrame = 0 ;
window . setTimeout ( function () {
schedulePendingLocalFolderRestoreFocus ({
reason : reason || 'retry' ,
attempt : attempt + 1 ,
});
}, 60 );
});
2026-05-26 02:20:46 +08:00
return focused ;
}
function selectedSidebarFileTreeRowIdsForDrag ( row ) {
var runtimeFn = fileTreeSelectionRuntimeFunction ( 'selectedSidebarFileTreeRowIdsForDrag' );
if ( runtimeFn ) return runtimeFn ( row , sidebarFileTreeSelection );
var rowId = row instanceof HTMLElement ? row . getAttribute ( 'data-row-id' ) || '' : '' ;
if ( rowId && sidebarFileTreeSelection . selectedRowIds . has ( rowId )) {
return Array . from ( sidebarFileTreeSelection . selectedRowIds );
}
return rowId ? [ rowId ] : [];
}
function selectedSidebarFileTreeRows () {
var runtimeFn = fileTreeSelectionRuntimeFunction ( 'selectedSidebarFileTreeRows' );
if ( runtimeFn ) return runtimeFn ( sidebarFileTreeSelection , fileTreeSelectionRuntimeDeps ());
var selectedIds = sidebarFileTreeSelection . selectedRowIds ;
var rows = visibleFileTreeRows (). filter ( function ( row ) {
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
return rowId && selectedIds . has ( rowId );
});
if ( rows . length > 0 ) return rows ;
if ( sidebarFileTreeSelection . focusedRowId ) {
var focused = document . querySelector (
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( sidebarFileTreeSelection . focusedRowId ) + '"]'
);
if ( focused instanceof HTMLElement ) return [ focused ];
}
return [];
}
function fileTreeRowDocumentId ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowDocumentId' );
if ( runtimeFn ) return runtimeFn ( row );
if ( ! ( row instanceof HTMLElement )) return '' ;
return String ( row . getAttribute ( 'data-document-id' ) || row . getAttribute ( 'data-doc-id' ) || '' ). trim ();
}
function fileTreeRowAssetId ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowAssetId' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return '' ;
var assetId = String ( row . getAttribute ( 'data-asset-id' ) || '' ). trim ();
if ( assetId ) return assetId ;
if ( currentSourceKind () === 'local_folder' && fileTreeRowKind ( row ) === 'asset' ) {
return String ( row . getAttribute ( 'data-row-id' ) || '' ). trim ();
}
return '' ;
}
function decodeLocalEncodedPath ( value ) {
var runtimeFn = fileTreeRuntimeFunction ( 'decodeLocalEncodedPath' );
if ( runtimeFn ) return runtimeFn ( value );
2026-05-27 11:31:12 +08:00
var path = String ( value || '' ). trim (). replace ( /~([0-9A-Fa-f]{2})/g , '%$1' );
2026-05-26 02:20:46 +08:00
if ( ! path ) return '' ;
try {
return decodeURIComponent ( path );
} catch ( _ ) {
return path ;
}
}
2026-05-27 11:31:12 +08:00
function normalizeFileTreeRelativePath ( value ) {
var normalized = String ( value || '' ). trim (). replace ( /^\/+|\/+$/g , '' );
return normalized === '.' ? '' : normalized ;
}
function parentRelativePathForFileTreePath ( relativePath ) {
var normalized = normalizeFileTreeRelativePath ( relativePath );
if ( ! normalized ) return '' ;
var index = normalized . lastIndexOf ( '/' );
return index > 0 ? normalized . slice ( 0 , index ) : '' ;
}
function fileNameStem ( value ) {
var name = String ( value || '' ). trim ();
var dot = name . lastIndexOf ( '.' );
return dot > 0 ? name . slice ( 0 , dot ) : name ;
}
function localMarkdownBundleParentPath ( relativePath ) {
var normalized = normalizeFileTreeRelativePath ( relativePath );
if ( ! /\.md$/i . test ( normalized )) return '' ;
var parent = parentRelativePathForFileTreePath ( normalized );
if ( ! parent ) return '' ;
var fileName = normalized . slice ( normalized . lastIndexOf ( '/' ) + 1 );
var parentName = parent . slice ( parent . lastIndexOf ( '/' ) + 1 );
return fileNameStem ( fileName ) === parentName ? parent : '' ;
}
function visibleFileTreeRowByRelativePath ( relativePath ) {
var normalized = normalizeFileTreeRelativePath ( relativePath );
if ( ! normalized ) return null ;
var row = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="' + cssEscape ( normalized ) + '"]' );
if ( ! ( row instanceof HTMLElement )) return null ;
if ( row . closest ( '.tree-children--collapsed' )) return null ;
return row ;
}
function activateSidebarFileTreeRow ( row , options ) {
if ( ! ( row instanceof HTMLElement )) return false ;
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
if ( ! rowId ) return false ;
selectSidebarFileTreeRow ( row , { ctrlKey : false , metaKey : false , shiftKey : false });
document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]' ). forEach ( function ( activeRow ) {
if ( activeRow instanceof HTMLElement ) activeRow . setAttribute ( 'data-active' , 'false' );
});
row . setAttribute ( 'data-active' , 'true' );
if ( options && options . scrollIntoView !== false ) {
try {
row . scrollIntoView ({ block : 'nearest' });
} catch ( _ ) {
row . scrollIntoView ();
}
}
return true ;
}
2026-05-26 02:20:46 +08:00
function fileTreeRowLocalRelativePath ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowLocalRelativePath' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return '' ;
var direct = String ( row . getAttribute ( 'data-local-relative-path' ) || '' ). trim ();
if ( direct ) return direct ;
var assetPath = localFilePathFromAssetId ( fileTreeRowAssetId ( row ));
if ( assetPath ) return assetPath ;
var rowId = String ( row . getAttribute ( 'data-row-id' ) || '' ). trim ();
var nodeId = String ( row . getAttribute ( 'data-node-id' ) || '' ). trim ();
var documentId = fileTreeRowDocumentId ( row );
var kind = fileTreeRowKind ( row );
if ( rowId . indexOf ( 'local:asset:' ) === 0 ) return rowId . slice ( 'local:asset:' . length );
if ( rowId . indexOf ( 'local:markdown:' ) === 0 ) return rowId . slice ( 'local:markdown:' . length );
if ( rowId . indexOf ( 'local:folder:' ) === 0 ) return rowId . slice ( 'local:folder:' . length );
if ( rowId . indexOf ( 'local:node:' ) === 0 ) return rowId . slice ( 'local:node:' . length );
if ( nodeId . indexOf ( 'local:node:' ) === 0 ) return nodeId . slice ( 'local:node:' . length );
if (( kind === 'document' || kind === 'doc' || kind === 'markdown' ) && documentId . indexOf ( 'local-md:' ) === 0 ) {
return decodeLocalEncodedPath ( documentId . slice ( 'local-md:' . length ));
}
if (( kind === 'folder' || kind === 'directory' || kind === 'index' ) && documentId . indexOf ( 'local-dir:' ) === 0 ) {
return decodeLocalEncodedPath ( documentId . slice ( 'local-dir:' . length ));
}
return '' ;
}
function fileTreeRowLocalUploadTargetRelativePath ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowLocalUploadTargetRelativePath' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return '' ;
var kind = fileTreeRowKind ( row );
var relativePath = fileTreeRowLocalRelativePath ( row );
if ( ! relativePath ) return '' ;
if ( kind === 'folder' || kind === 'directory' ) return relativePath ;
var lastSlash = relativePath . lastIndexOf ( '/' );
return lastSlash >= 0 ? relativePath . slice ( 0 , lastSlash ) : '' ;
}
2026-05-28 22:01:44 +08:00
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'
};
}
2026-05-26 02:20:46 +08:00
function fileTreeRowKind ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowKind' );
if ( runtimeFn ) return runtimeFn ( row );
if ( ! ( row instanceof HTMLElement )) return '' ;
return String ( row . getAttribute ( 'data-row-kind' ) || '' ). trim ();
}
function isFileTreeDownloadableAssetRow ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'isFileTreeDownloadableAssetRow' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return false ;
var kind = fileTreeRowKind ( row );
if ( kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory' ) return false ;
return Boolean ( fileTreeRowAssetId ( row ));
}
function isFileTreeDownloadableRow ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'isFileTreeDownloadableRow' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! ( row instanceof HTMLElement )) return false ;
if ( currentSourceKind () !== 'local_folder' ) return isFileTreeDownloadableAssetRow ( row );
return Boolean ( fileTreeRowLocalRelativePath ( row ) || isFileTreeDownloadableAssetRow ( row ));
}
function fileTreeAssetDownloadDetail ( row ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeAssetDownloadDetail' );
if ( runtimeFn ) return runtimeFn ( row , fileTreeRuntimeDeps ());
if ( ! isFileTreeDownloadableRow ( row )) return null ;
var title = rowTitle ( row );
var relativePath = fileTreeRowLocalRelativePath ( row );
var assetId = fileTreeRowAssetId ( row ) || ( relativePath ? 'local-file:' + relativePath : '' );
return {
contextKind : 'filetree' ,
documentId : fileTreeRowDocumentId ( row ),
rowId : row . getAttribute ( 'data-row-id' ) || '' ,
rowKind : fileTreeRowKind ( row ),
assetId : assetId ,
localRelativePath : relativePath ,
2026-05-28 22:01:44 +08:00
workspacePath : readWorkspacePathFromRow ( row ),
2026-05-26 02:20:46 +08:00
title : title ,
fileName : title ,
workspaceId : resolveWorkspaceId ( row )
};
}
function selectedSidebarFileTreeRowsForDownload ( contextRow ) {
var contextRowId = contextRow instanceof HTMLElement ? contextRow . getAttribute ( 'data-row-id' ) || '' : '' ;
var selectedRows = selectedSidebarFileTreeRows (). filter ( isFileTreeDownloadableRow );
if ( contextRowId && sidebarFileTreeSelection . selectedRowIds . has ( contextRowId ) && selectedRows . length > 0 ) return selectedRows ;
return isFileTreeDownloadableRow ( contextRow ) ? [ contextRow ] : selectedRows ;
}
function selectedSidebarFileTreeAssetRowsForDownload ( contextRow ) {
return selectedSidebarFileTreeRowsForDownload ( contextRow ). filter ( isFileTreeDownloadableAssetRow );
}
function downloadSelectedFileTreeAssetRows ( detail , trigger ) {
var contextRow = trigger && trigger . closest ? trigger . closest ( '.tree-row[data-shell-mode="filetree"]' ) : trigger ;
if ( ! ( contextRow instanceof HTMLElement ) && detail && detail . rowId ) {
contextRow = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( detail . rowId ) + '"]' );
}
var rows = selectedSidebarFileTreeRowsForDownload ( contextRow );
var seen = new Set ();
var downloads = [];
rows . forEach ( function ( row ) {
var item = fileTreeAssetDownloadDetail ( row );
var key = item && ( item . localRelativePath || item . assetId || item . rowId );
if ( ! item || ! key || seen . has ( key )) return ;
seen . add ( key );
downloads . push ( item );
});
if ( downloads . length === 0 && detail && detail . assetId ) {
downloads . push ( Object . assign ({}, detail , { fileName : detail . fileName || detail . title || '附件' }));
} else if ( downloads . length === 0 && detail && detail . localRelativePath ) {
downloads . push ( Object . assign ({}, detail , {
assetId : detail . assetId || 'local-file:' + detail . localRelativePath ,
fileName : detail . fileName || detail . title || '附件'
}));
}
if ( downloads . length === 0 ) {
recordFileTreeActionStatus ( 'blocked' , Object . assign ({}, detail || {}, { reason : 'no-downloadable-assets' }));
return false ;
}
var primary = Object . assign ({}, downloads [ 0 ], {
count : downloads . length ,
assetIds : downloads . map ( function ( item ) { return item . assetId || '' ; }). filter ( Boolean ),
localRelativePaths : downloads . map ( function ( item ) { return item . localRelativePath || '' ; }). filter ( Boolean )
});
recordFileTreeAction ( downloads . length > 1 ? 'bulk-download' : 'download' , primary );
recordFileTreeActionStatus ( 'requested' , primary );
document . documentElement . setAttribute ( 'data-mnote-filetree-download-count' , String ( downloads . length ));
document . documentElement . setAttribute ( 'data-mnote-filetree-download-asset-ids' , downloads . map ( function ( item ) { return item . assetId || '' ; }). filter ( Boolean ). join ( ',' ));
document . documentElement . setAttribute ( 'data-mnote-filetree-download-paths' , downloads . map ( function ( item ) { return item . localRelativePath || '' ; }). filter ( Boolean ). join ( ',' ));
downloads . forEach ( function ( item ) {
void openEditorAttachmentDownload ( item );
});
return true ;
}
function hasSelectedDocumentAncestor ( row , selectedDocRowIds ) {
var node = row instanceof HTMLElement ? row . closest ( '.tree-node' ) : null ;
while ( node && node . parentElement ) {
var parentChildren = node . parentElement . closest ( '.tree-children' );
var parentNode = parentChildren ? parentChildren . closest ( '.tree-node' ) : null ;
var parentRow = parentNode ? parentNode . querySelector ( ':scope > .tree-row[data-shell-mode="filetree"]' ) : null ;
if ( parentRow instanceof HTMLElement ) {
var parentRowId = parentRow . getAttribute ( 'data-row-id' ) || '' ;
if ( selectedDocRowIds . has ( parentRowId )) return true ;
}
node = parentNode ;
}
return false ;
}
function classifySidebarFileTreeAsset ( row ) {
var badge = row instanceof HTMLElement ? row . querySelector ( '.tree-kind-badge' ) : null ;
var iconKind = badge instanceof HTMLElement ? String ( badge . getAttribute ( 'data-kind' ) || '' ). trim () : '' ;
var objectKind = row instanceof HTMLElement ? String ( row . getAttribute ( 'data-object-kind' ) || '' ). trim () : '' ;
var title = rowTitle ( row ). toLowerCase ();
if ( objectKind === 'mindmap' || iconKind === 'mindmap' ) return 'mindmap' ;
2026-06-04 18:51:16 +08:00
if ( objectKind === 'table' || iconKind === 'table' ) return 'table' ;
2026-05-26 02:20:46 +08:00
return 'file' ;
}
function buildSidebarFileTreeDeletePlan ( rows ) {
var docRows = [];
var folderRows = [];
var assetRows = [];
rows . forEach ( function ( row ) {
var kind = fileTreeRowKind ( row );
2026-05-27 11:31:12 +08:00
var documentId = fileTreeRowDocumentId ( row );
if (( kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || documentId . indexOf ( 'local-md:' ) === 0 ) && documentId ) {
2026-05-26 02:20:46 +08:00
docRows . push ( row );
return ;
}
if ( kind === 'folder' && currentSourceKind () === 'local_folder' ) {
folderRows . push ( row );
return ;
}
if ( fileTreeRowAssetId ( row )) assetRows . push ( row );
});
var selectedContainerRowIds = new Set ( docRows . concat ( folderRows ). map ( function ( row ) { return row . getAttribute ( 'data-row-id' ) || '' ; }). filter ( Boolean ));
var topDocRows = [];
var topFolderRows = [];
var seenDocs = new Set ();
var seenFolders = new Set ();
docRows . forEach ( function ( row ) {
var documentId = fileTreeRowDocumentId ( row );
if ( ! documentId || seenDocs . has ( documentId )) return ;
if ( hasSelectedDocumentAncestor ( row , selectedContainerRowIds )) return ;
seenDocs . add ( documentId );
topDocRows . push ( row );
});
folderRows . forEach ( function ( row ) {
var rowId = row . getAttribute ( 'data-row-id' ) || '' ;
if ( ! rowId || seenFolders . has ( rowId )) return ;
if ( hasSelectedDocumentAncestor ( row , selectedContainerRowIds )) return ;
seenFolders . add ( rowId );
topFolderRows . push ( row );
});
var selectedTopContainerRows = new Set ( topDocRows . concat ( topFolderRows ). map ( function ( row ) { return row . getAttribute ( 'data-row-id' ) || '' ; }). filter ( Boolean ));
var fileAssetRows = [];
var mindmapRows = [];
var tableRows = [];
var seenAssets = new Set ();
assetRows . forEach ( function ( row ) {
var assetId = fileTreeRowAssetId ( row );
if ( ! assetId || seenAssets . has ( assetId )) return ;
if ( hasSelectedDocumentAncestor ( row , selectedTopContainerRows )) return ;
seenAssets . add ( assetId );
var assetKind = classifySidebarFileTreeAsset ( row );
if ( assetKind === 'mindmap' ) mindmapRows . push ( row );
else if ( assetKind === 'table' ) tableRows . push ( row );
else fileAssetRows . push ( row );
});
return {
docRows : topDocRows ,
folderRows : topFolderRows ,
fileAssetRows : fileAssetRows ,
mindmapRows : mindmapRows ,
tableRows : tableRows
};
}
function sidebarFileTreeDeleteConfirmText ( plan ) {
var docCount = plan . docRows . length ;
var folderCount = plan . folderRows . length ;
var fileCount = plan . fileAssetRows . length ;
var mindmapCount = plan . mindmapRows . length ;
var tableCount = plan . tableRows . length ;
var parts = [];
if ( docCount > 0 ) parts . push ( docCount + ' 个页面(删除到垃圾桶)' );
if ( folderCount > 0 ) parts . push ( folderCount + ' 个文件夹(删除到垃圾桶)' );
if ( fileCount > 0 ) parts . push ( fileCount + ' 个附件(删除,10 分钟内可撤销)' );
if ( mindmapCount > 0 ) parts . push ( mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)' );
if ( tableCount > 0 ) parts . push ( tableCount + ( currentSourceKind () === 'local_folder' ? ' 个在线表格(移入垃圾桶,10 分钟内可恢复)' : ' 个在线表格(删除)' ));
return '确认删除选中的 ' + parts . join ( ' + ' ) + ' 吗?' ;
}
async function postSidebarFileTreeJson ( url , body ) {
var response = await fetch ( url , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' },
body : JSON . stringify ( body || {})
});
var payload = await response . json (). catch ( function () { return null ; });
if ( ! response . ok ) {
throw new Error (( payload && ( payload . error || payload . message )) || ( url + ' failed: ' + response . status ));
}
return payload ;
}
function fileTreeRowsByRowIds ( rowIds ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeRowsByRowIds' );
if ( runtimeFn ) return runtimeFn ( rowIds , fileTreeRuntimeDeps ());
return ( rowIds || []). map ( function ( rowId ) {
return document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( rowId ) + '"]' );
}). filter ( function ( row ) { return row instanceof HTMLElement ; });
}
function fileTreeChildCount ( documentId ) {
var runtimeFn = fileTreeRuntimeFunction ( 'fileTreeChildCount' );
if ( runtimeFn ) return runtimeFn ( documentId , fileTreeRuntimeDeps ());
if ( ! documentId ) return 0 ;
2026-07-24 11:36:06 +08:00
var row = document . querySelector (
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( 'doc:' + documentId ) + '"],'
+ '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( documentId ) + '"],'
+ '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape ( documentId ) + '"],'
+ '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-node-id="' + cssEscape ( documentId ) + '"]'
);
2026-05-26 02:20:46 +08:00
var node = row ? row . closest ( '.tree-node' ) : null ;
2026-07-24 11:36:06 +08:00
var children = node
? node . querySelectorAll ( ':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"]' )
: [];
2026-05-26 02:20:46 +08:00
return children . length ;
}
2026-05-27 11:31:12 +08:00
function fileTreeMoveTargetParentId ( targetRow ) {
if ( ! ( targetRow instanceof HTMLElement )) return null ;
return fileTreeMenuTargetParentId ({
rowKind : fileTreeRowKind ( targetRow ),
rowId : targetRow . getAttribute ( 'data-row-id' ) || '' ,
documentId : fileTreeRowDocumentId ( targetRow )
}, targetRow ) || null ;
}
2026-07-24 11:36:06 +08:00
function fileTreeParentIdFromAttr ( parentAttr ) {
var raw = String ( parentAttr || '' ). trim ();
if ( ! raw ) return null ;
if ( raw . indexOf ( 'doc:' ) === 0 ) return raw . slice ( 4 );
return raw ;
}
/**
* Wolai-style move target:
* - inside → nest under target (folder/dir/doc)
* - before/after → same parent as target, sortOrder = sibling index (+1 for after)
*/
function resolveFileTreeMoveTarget ( targetRow , position ) {
if ( ! ( targetRow instanceof HTMLElement )) {
return { parentId : null , sortOrder : 0 };
}
var pos = position || 'inside' ;
if ( pos === 'inside' ) {
var nestParentId = fileTreeMoveTargetParentId ( targetRow );
return {
parentId : nestParentId ,
sortOrder : nestParentId ? fileTreeChildCount ( nestParentId ) : 0 ,
};
}
var parentAttr = targetRow . getAttribute ( 'data-parent-id' ) || '' ;
var parentId = fileTreeParentIdFromAttr ( parentAttr );
var siblings = Array . from (
document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]' )
). filter ( function ( row ) {
return ( row . getAttribute ( 'data-parent-id' ) || '' ) === parentAttr ;
});
var index = siblings . indexOf ( targetRow );
if ( index < 0 ) index = 0 ;
return {
parentId : parentId ,
sortOrder : Math . max ( 0 , index + ( pos === 'after' ? 1 : 0 )),
};
}
2026-05-27 11:31:12 +08:00
function fileTreeMoveSourceId ( row ) {
if ( ! ( row instanceof HTMLElement )) return '' ;
return fileTreeRowDocumentId ( row )
|| ( currentSourceKind () === 'local_folder'
? ( fileTreeRowAssetId ( row ) || row . getAttribute ( 'data-row-id' ) || '' )
: fileTreeRowAssetId ( row ));
}
async function moveSidebarFileTreeRows ( rowIds , targetRow , options ) {
var rows = fileTreeRowsByRowIds ( rowIds || []);
if ( rows . length === 0 ) return false ;
var copy = Boolean ( options && options . copy );
2026-07-24 11:36:06 +08:00
var dropPosition = ( options && options . dropPosition ) || 'inside' ;
var resolved = resolveFileTreeMoveTarget ( targetRow , dropPosition );
var targetParentId = resolved . parentId ;
var baseSortOrder = typeof resolved . sortOrder === 'number' ? resolved . sortOrder : 0 ;
2026-05-27 11:31:12 +08:00
var workspaceId = resolveWorkspaceId ( targetRow || document . body );
var writable = await ensureFileTreeWritableTarget ( 'move' , targetRow , rowIds || [], copy );
if ( ! writable ) return false ;
var batchId = nextFileTreeOperationBatchId ( copy ? 'copy' : 'move' );
var plan = buildSidebarFileTreeDeletePlan ( rows );
var failures = [];
if ( copy ) {
dispatchSidebarEvent ( 'tree.filetree.copy-requested' , { rowIds : rowIds || [], targetDocumentId : targetParentId });
recordFileTreeActionStatus ( 'copy-requested' , { documentId : targetParentId , batchId : batchId });
return true ;
}
var movableRows = plan . docRows . concat ( plan . folderRows );
if ( currentSourceKind () === 'local_folder' ) {
movableRows = movableRows . concat ( plan . fileAssetRows , plan . mindmapRows , plan . tableRows );
}
for ( var i = 0 ; i < movableRows . length ; i += 1 ) {
var sourceRow = movableRows [ i ];
var sourceId = fileTreeMoveSourceId ( sourceRow );
if ( ! sourceId || sourceId === targetParentId ) continue ;
try {
await dispatchTreeCommand ( targetRow || sourceRow , {
action : 'move' ,
batchId : batchId ,
workspaceId : workspaceId ,
documentId : sourceId ,
parentId : targetParentId ,
2026-07-24 11:36:06 +08:00
sortOrder : baseSortOrder + i
2026-05-27 11:31:12 +08:00
});
} catch ( error ) {
failures . push ( sourceId + ': ' + ( error && error . message ? error . message : '移动失败' ));
}
}
if ( currentSourceKind () !== 'local_folder' ) {
var assetIds = plan . fileAssetRows . map ( fileTreeRowAssetId ). filter ( Boolean );
if ( assetIds . length > 0 ) {
try {
await postSidebarFileTreeJson ( '/api/media/batch' , {
action : 'move' ,
assetIds : assetIds ,
targetDocumentId : targetParentId
});
} catch ( error ) {
failures . push ( assetIds . join ( ',' ) + ': ' + ( error && error . message ? error . message : '移动附件失败' ));
}
}
}
window . dispatchEvent ( new CustomEvent ( 'tree:local-command-batch-complete' , { detail : { batchId : batchId , action : 'move' , failed : failures . length , count : movableRows . length } }));
if ( failures . length > 0 ) {
recordFileTreeActionStatus ( 'failed' , { documentId : targetParentId , fallback : 'alert' , batchId : batchId });
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( '部分对象移动失败:' + failures . join ( '; ' ));
2026-05-27 11:31:12 +08:00
return false ;
}
recordFileTreeActionStatus ( 'applied' , { documentId : targetParentId , batchId : batchId });
return true ;
}
2026-05-26 02:20:46 +08:00
async function pasteSidebarFileTreeClipboard ( trigger ) {
2026-07-24 11:36:06 +08:00
var clipboard = currentSidebarFileTreeClipboard ();
if ( ! clipboard ) return false ;
2026-05-26 02:20:46 +08:00
var targetRow = trigger instanceof HTMLElement ? trigger : null ;
if ( ! targetRow && sidebarFileTreeSelection . focusedRowId ) {
targetRow = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape ( sidebarFileTreeSelection . focusedRowId ) + '"]' );
}
2026-05-27 11:31:12 +08:00
var targetDocumentId = fileTreeMoveTargetParentId ( targetRow ) || currentDocumentId ();
2026-07-24 11:36:06 +08:00
var action = clipboard . action === 'cut' ? 'move' : 'copy' ;
2026-05-26 02:20:46 +08:00
recordFileTreeAction ( 'paste' , {
rowId : targetRow ? targetRow . getAttribute ( 'data-row-id' ) || '' : '' ,
documentId : targetDocumentId ,
2026-07-24 11:36:06 +08:00
sourceRowIds : clipboard . rowIds ,
clipboardAction : clipboard . action
2026-05-26 02:20:46 +08:00
});
2026-07-24 11:36:06 +08:00
var ok = await moveSidebarFileTreeRows ( clipboard . rowIds , targetRow , { copy : action === 'copy' });
if ( ok && action === 'move' ) {
setSidebarFileTreeClipboard ( null , []);
}
2026-05-27 11:31:12 +08:00
return ok ;
2026-05-26 02:20:46 +08:00
}
async function deleteSelectedSidebarFileTreeRows ( trigger ) {
var rows = selectedSidebarFileTreeRows ();
var plan = buildSidebarFileTreeDeletePlan ( rows );
var total = plan . docRows . length + plan . folderRows . length + plan . fileAssetRows . length + plan . mindmapRows . length + plan . tableRows . length ;
if ( total === 0 ) return false ;
2026-07-28 17:04:27 +08:00
if ( ! ( await window . mnote . confirm ( sidebarFileTreeDeleteConfirmText ( plan )))) return false ;
2026-05-27 11:31:12 +08:00
var batchId = nextFileTreeOperationBatchId ( 'bulk-delete' );
document . documentElement . setAttribute ( 'data-mnote-filetree-bulk-delete-batch-id' , batchId );
recordFileTreeAction ( 'bulk-delete' , { rowId : trigger instanceof HTMLElement ? trigger . getAttribute ( 'data-row-id' ) || '' : '' , count : total , batchId : batchId });
recordFileTreeActionStatus ( 'pending' , { count : total , batchId : batchId });
2026-05-26 02:20:46 +08:00
var failures = [];
for ( var i = 0 ; i < plan . docRows . length ; i += 1 ) {
var docRow = plan . docRows [ i ];
var documentId = fileTreeRowDocumentId ( docRow );
try {
await dispatchTreeCommand ( trigger || docRow , {
action : 'archive' ,
2026-05-27 11:31:12 +08:00
batchId : batchId ,
2026-05-26 02:20:46 +08:00
workspaceId : resolveWorkspaceId ( docRow ),
documentId : documentId
});
applyRemoveDocumentDelta ({ documentId : documentId });
} catch ( error ) {
failures . push ( documentId );
}
}
for ( var fd = 0 ; fd < plan . folderRows . length ; fd += 1 ) {
var folderRow = plan . folderRows [ fd ];
var folderId = folderRow . getAttribute ( 'data-row-id' ) || folderRow . getAttribute ( 'data-node-id' ) || '' ;
try {
await dispatchTreeCommand ( trigger || folderRow , {
action : 'archive' ,
2026-05-27 11:31:12 +08:00
batchId : batchId ,
2026-05-26 02:20:46 +08:00
workspaceId : resolveWorkspaceId ( folderRow ),
documentId : folderId
});
} catch ( error ) {
failures . push ( folderId );
}
}
if ( plan . fileAssetRows . length > 0 ) {
var fileAssetIds = plan . fileAssetRows . map ( fileTreeRowAssetId ). filter ( Boolean );
try {
if ( currentSourceKind () === 'local_folder' ) {
for ( var lf = 0 ; lf < fileAssetIds . length ; lf += 1 ) {
await dispatchTreeCommand ( trigger || plan . fileAssetRows [ lf ], {
action : 'archive' ,
2026-05-27 11:31:12 +08:00
batchId : batchId ,
2026-05-26 02:20:46 +08:00
workspaceId : resolveWorkspaceId ( plan . fileAssetRows [ lf ]),
documentId : fileAssetIds [ lf ]
});
}
} else {
await postSidebarFileTreeJson ( '/api/media/batch' , { action : 'delete' , assetIds : fileAssetIds });
}
fileAssetIds . forEach ( removeFileTreeAssetRow );
} catch ( error ) {
2026-05-27 11:31:12 +08:00
failures = failures . concat ( fileAssetIds . map ( function ( assetId ) {
return assetId + ': ' + ( error && error . message ? error . message : '删除附件失败' );
}));
2026-05-26 02:20:46 +08:00
}
}
for ( var m = 0 ; m < plan . mindmapRows . length ; m += 1 ) {
var mindmapRow = plan . mindmapRows [ m ];
var mindmapId = fileTreeRowAssetId ( mindmapRow );
try {
if ( currentSourceKind () === 'local_folder' ) {
await dispatchTreeCommand ( trigger || mindmapRow , {
action : 'archive' ,
2026-05-27 11:31:12 +08:00
batchId : batchId ,
2026-05-26 02:20:46 +08:00
workspaceId : resolveWorkspaceId ( mindmapRow ),
documentId : mindmapId
});
} else {
var mindmapDocId = fileTreeRowDocumentId ( mindmapRow );
var mindmapResponse = await fetch ( '/api/mindmap/' + encodeURIComponent ( mindmapDocId ) + '/' + encodeURIComponent ( mindmapId ), { method : 'DELETE' });
if ( ! mindmapResponse . ok ) throw new Error ( 'mindmap_delete_failed_' + mindmapResponse . status );
}
removeFileTreeAssetRow ( mindmapId );
} catch ( error ) {
failures . push ( mindmapId );
}
}
for ( var t = 0 ; t < plan . tableRows . length ; t += 1 ) {
var tableRow = plan . tableRows [ t ];
var tableId = fileTreeRowAssetId ( tableRow );
try {
if ( currentSourceKind () === 'local_folder' ) {
await dispatchTreeCommand ( trigger || tableRow , {
action : 'archive' ,
2026-05-27 11:31:12 +08:00
batchId : batchId ,
2026-05-26 02:20:46 +08:00
workspaceId : resolveWorkspaceId ( tableRow ),
documentId : tableId
});
} else {
var tableResponse = await fetch ( '/api/tables/' + encodeURIComponent ( tableId ), { method : 'DELETE' });
if ( ! tableResponse . ok ) throw new Error ( 'table_delete_failed_' + tableResponse . status );
window . dispatchEvent ( new CustomEvent ( 'online-table-deleted' , { detail : { tableId : tableId } }));
}
removeFileTreeAssetRow ( tableId );
} catch ( error ) {
failures . push ( tableId );
}
}
sidebarFileTreeSelection . selectedRowIds = new Set ();
sidebarFileTreeSelection . focusedRowId = null ;
syncSidebarFileTreeSelection ();
if ( failures . length > 0 ) {
2026-05-27 11:31:12 +08:00
window . dispatchEvent ( new CustomEvent ( 'tree:local-command-batch-complete' , { detail : { batchId : batchId , action : 'bulk-delete' , failed : failures . length , count : total } }));
recordFileTreeActionStatus ( 'failed' , { count : total , failures : failures . slice ( 0 , 20 ), fallback : 'alert' , batchId : batchId });
2026-07-28 17:04:27 +08:00
await window . mnote . alert ( '部分对象删除失败:' + failures . slice ( 0 , 5 ). join ( ', ' ) + ( failures . length > 5 ? '…' : '' ));
2026-05-26 02:20:46 +08:00
return false ;
}
document . documentElement . setAttribute ( 'data-mnote-filetree-bulk-delete-applied' , 'true' );
2026-05-27 11:31:12 +08:00
window . dispatchEvent ( new CustomEvent ( 'tree:local-command-batch-complete' , { detail : { batchId : batchId , action : 'bulk-delete' , failed : 0 , count : total } }));
recordFileTreeActionStatus ( 'archived' , { count : total , undo : 'trash-modal' , batchId : batchId });
2026-06-04 18:51:16 +08:00
if ( currentSourceKind () === 'local_folder' ) void refreshLocalFolderSidebarSnapshot ();
2026-05-26 02:20:46 +08:00
return true ;
}
return {
rowTitle ,
rowCenter ,
renameFileTreeAsset ,
beginFileTreeInlineRename ,
closeTreeContextMenu ,
copyTreeContextValue ,
triggerBrowserDownload ,
recordFileTreeAction ,
recordFileTreeActionStatus ,
documentHref ,
convertToPreviousSiblingChild ,
fileTreeCopyPath ,
fileTreeMenuTargetParentId ,
withOfficeEditModeGuard ,
handleTreeContextMenuAction ,
appendTreeContextMenuButton ,
buildSidebarFileTreeContext ,
evaluateSidebarFileTreeWhen ,
openTreeContextMenu ,
openPageTreeContextMenu ,
openFileTreeContextMenu ,
visibleFileTreeRows ,
syncSidebarFileTreeSelection ,
selectSidebarFileTreeRow ,
selectSidebarFileTreeDocument ,
selectSidebarFileTreeRowById ,
pendingLocalFolderRestoreRowId ,
clearPendingLocalFolderRestoreRowId ,
applyPendingLocalFolderRestoreFocusOnce ,
schedulePendingLocalFolderRestoreFocus ,
selectedSidebarFileTreeRowIdsForDrag ,
selectedSidebarFileTreeRows ,
fileTreeRowDocumentId ,
fileTreeRowAssetId ,
decodeLocalEncodedPath ,
fileTreeRowLocalRelativePath ,
fileTreeRowLocalUploadTargetRelativePath ,
2026-05-28 22:01:44 +08:00
readWorkspacePathFromRow ,
2026-05-26 02:20:46 +08:00
fileTreeRowKind ,
isFileTreeDownloadableAssetRow ,
isFileTreeDownloadableRow ,
fileTreeAssetDownloadDetail ,
selectedSidebarFileTreeRowsForDownload ,
selectedSidebarFileTreeAssetRowsForDownload ,
downloadSelectedFileTreeAssetRows ,
hasSelectedDocumentAncestor ,
classifySidebarFileTreeAsset ,
buildSidebarFileTreeDeletePlan ,
sidebarFileTreeDeleteConfirmText ,
postSidebarFileTreeJson ,
fileTreeRowsByRowIds ,
fileTreeChildCount ,
2026-07-24 11:36:06 +08:00
resolveFileTreeMoveTarget ,
2026-05-27 11:31:12 +08:00
moveSidebarFileTreeRows ,
2026-05-26 02:20:46 +08:00
pasteSidebarFileTreeClipboard ,
deleteSelectedSidebarFileTreeRows ,
};
};