refactor: externalize filetree runtime helpers
This commit is contained in:
@@ -537,6 +537,60 @@ Reasonix read-only audit:
|
|||||||
- `type="module"` 自带作用域,`tree-shell-runtime.js` 已移除外层 IIFE,并只 hoist P3 关键函数;其他 DOM runtime 仍按原局部闭包组织,避免扩大改动。
|
- `type="module"` 自带作用域,`tree-shell-runtime.js` 已移除外层 IIFE,并只 hoist P3 关键函数;其他 DOM runtime 仍按原局部闭包组织,避免扩大改动。
|
||||||
- debug tree shell runtime 与主壳 `SIDEBAR_TREE_JS` 仍是两套 runtime,后续 P4/P5 继续拆主壳 filetree runtime 和 document pane host adapter。
|
- debug tree shell runtime 与主壳 `SIDEBAR_TREE_JS` 仍是两套 runtime,后续 P4/P5 继续拆主壳 filetree runtime 和 document pane host adapter。
|
||||||
|
|
||||||
|
### 9.7 Batch 6 执行记录(2026-05-25)
|
||||||
|
|
||||||
|
Reasonix read-only audit:
|
||||||
|
|
||||||
|
- run id: `reasonix-2026-05-24T16-11-49-842Z-94562749`
|
||||||
|
- worktree: `/mnt/Data1T/mnote-worktrees/p4-filetree-audit`
|
||||||
|
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-11-49-842Z-94562749/process-handoff.md`
|
||||||
|
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md`、工作区 diff 与验证结果。
|
||||||
|
- 采纳结论:主壳 filetree 第一批应优先迁纯 DOM/字符串读取 helper、path 解析和下载详情构造;DND、paste、菜单 DOM、上传执行链继续留在后续批次。
|
||||||
|
|
||||||
|
采纳内容:
|
||||||
|
|
||||||
|
- 新增 `rust/crates/mnote-web/browser/filetree-runtime.js`。
|
||||||
|
- 新增固定 asset route:`GET /api/mnote-browser-runtime/filetree-runtime.js`。
|
||||||
|
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
|
||||||
|
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理,保留原实现 fallback:
|
||||||
|
- `fileTreeRowDocumentId`
|
||||||
|
- `fileTreeRowAssetId`
|
||||||
|
- `decodeLocalEncodedPath`
|
||||||
|
- `fileTreeRowLocalRelativePath`
|
||||||
|
- `fileTreeRowLocalUploadTargetRelativePath`
|
||||||
|
- `fileTreeRowKind`
|
||||||
|
- `isFileTreeDownloadableAssetRow`
|
||||||
|
- `isFileTreeDownloadableRow`
|
||||||
|
- `fileTreeAssetDownloadDetail`
|
||||||
|
- `fileTreeRowsByRowIds`
|
||||||
|
- `fileTreeChildCount`
|
||||||
|
- 新增 `rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`,先承接 `buildSidebarFileTreeContext` 与 `evaluateSidebarFileTreeWhen` 的 CommandContext / when 表达式求值;菜单 DOM 构建仍留在 `SIDEBAR_TREE_JS`。
|
||||||
|
|
||||||
|
采纳边界:
|
||||||
|
|
||||||
|
- 本批次不迁移 `selectFileTreeRow`、`openFileTreeContextMenu`、`pasteSidebarFileTreeClipboard`、`downloadSelectedFileTreeAssetRows`、内部/外部 DND、上传执行链。
|
||||||
|
- `fileTreeChildCount` 虽与 paste sortOrder 有关,但本批仅做同构 runtime 代理并保留 inline fallback,未改变 paste 命令语义。
|
||||||
|
- filetree context menu 本批只迁 CommandContext/when evaluator,不迁 menu item DOM、事件绑定或右键定位逻辑。
|
||||||
|
|
||||||
|
已通过验证:
|
||||||
|
|
||||||
|
- `node --check rust/crates/mnote-web/browser/filetree-runtime.js`
|
||||||
|
- `node --check rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`
|
||||||
|
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
|
||||||
|
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
|
||||||
|
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime -- --test-threads=1`
|
||||||
|
- 结果:9 个相关测试通过。
|
||||||
|
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu -- --test-threads=1`
|
||||||
|
- 结果:2 个相关测试通过。
|
||||||
|
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
|
||||||
|
- 结果:runtime asset route 测试通过。
|
||||||
|
- 真实浏览器轻量验收:
|
||||||
|
- dev server: `npm run dev:hot`
|
||||||
|
- 登录后访问 `http://127.0.0.1:3000/`
|
||||||
|
- `document.scripts` 包含 `/api/mnote-browser-runtime/filetree-runtime.js` 与 `/api/mnote-browser-runtime/filetree-context-menu-runtime.js`
|
||||||
|
- `window.__mnoteFileTreeRuntime.fileTreeRowLocalRelativePath` 和 `window.__mnoteFileTreeContextMenuRuntime.evaluateSidebarFileTreeWhen` 均为函数。
|
||||||
|
- 两个 runtime asset 均返回 `200`,`content-type=application/javascript; charset=utf-8`。
|
||||||
|
|
||||||
## 10. 分阶段执行清单
|
## 10. 分阶段执行清单
|
||||||
|
|
||||||
### P0:定位辅助与基线保护
|
### P0:定位辅助与基线保护
|
||||||
@@ -600,7 +654,9 @@ Reasonix read-only audit:
|
|||||||
### P4:抽出主壳 filetree runtime
|
### P4:抽出主壳 filetree runtime
|
||||||
|
|
||||||
- [ ] 从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
|
- [ ] 从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
|
||||||
- [ ] 先拆纯函数和 reducer bridge,再拆 DOM 绑定。
|
- [x] 先拆纯函数和 reducer bridge,再拆 DOM 绑定。
|
||||||
|
- 2026-05-25 / Batch 6 已迁:filetree 行 accessor、local relative path、download detail、row lookup、child count wrapper,以及 context menu CommandContext / when evaluator。
|
||||||
|
- 暂不迁:selection、keyboard、clipboard、DND、菜单 DOM、上传执行链。
|
||||||
- [ ] 主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护,但启动函数迁到外置模块。
|
- [ ] 主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护,但启动函数迁到外置模块。
|
||||||
- [ ] 与 Rust `FileTreeSelectionState` 合同对齐,不在前端新增第二套选择真相。
|
- [ ] 与 Rust `FileTreeSelectionState` 合同对齐,不在前端新增第二套选择真相。
|
||||||
- [ ] 覆盖:
|
- [ ] 覆盖:
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// MNote 文件树上下文菜单运行时外置模块。
|
||||||
|
// 当前先承接 CommandContext 构造与 when 表达式求值,树语义仍由 Rust/kernel 主导。
|
||||||
|
|
||||||
|
function buildSidebarFileTreeContext(kind, deps) {
|
||||||
|
var selection = deps && deps.selection ? deps.selection : {};
|
||||||
|
var rowIds = Array.isArray(selection.selectedRowIds)
|
||||||
|
? selection.selectedRowIds
|
||||||
|
: Array.from(selection.selectedRowIds || []);
|
||||||
|
var rows = [];
|
||||||
|
var queryRowById = deps && typeof deps.queryRowById === 'function'
|
||||||
|
? deps.queryRowById
|
||||||
|
: function() { return null; };
|
||||||
|
for (var i = 0; i < rowIds.length; i += 1) {
|
||||||
|
var row = queryRowById(rowIds[i]);
|
||||||
|
if (row instanceof HTMLElement) rows.push(row);
|
||||||
|
}
|
||||||
|
var resourceKinds = {};
|
||||||
|
for (var j = 0; j < rows.length; j += 1) {
|
||||||
|
var rk = rows[j].getAttribute('data-row-kind') || 'unknown';
|
||||||
|
resourceKinds[rk] = true;
|
||||||
|
}
|
||||||
|
var rkKeys = Object.keys(resourceKinds);
|
||||||
|
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function'
|
||||||
|
? deps.currentSourceKind
|
||||||
|
: function() { return ''; };
|
||||||
|
var readonly = deps && typeof deps.workspaceReadonly === 'function'
|
||||||
|
? deps.workspaceReadonly()
|
||||||
|
: false;
|
||||||
|
return {
|
||||||
|
'workspace.sourceKind': currentSourceKind() || '',
|
||||||
|
'workspace.readonly': 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) {
|
||||||
|
if (!expr) return true;
|
||||||
|
try {
|
||||||
|
expr = String(expr || '').trim();
|
||||||
|
var tokens = tokenizeWhenExpression(expr);
|
||||||
|
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 compareValue(cv, value, equal) {
|
||||||
|
if (value === 'true') {
|
||||||
|
var truth = cv === true || cv === 'true';
|
||||||
|
return equal ? truth : !truth;
|
||||||
|
}
|
||||||
|
if (value === 'false') {
|
||||||
|
var falsy = cv === false || cv === '' || cv === 'false';
|
||||||
|
return equal ? falsy : !falsy;
|
||||||
|
}
|
||||||
|
var num = Number(value);
|
||||||
|
if (!isNaN(num) && String(num) === value) {
|
||||||
|
return equal ? Number(cv) === num : Number(cv) !== num;
|
||||||
|
}
|
||||||
|
return equal ? String(cv) === value : String(cv) !== value;
|
||||||
|
}
|
||||||
|
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 grouped = parseOr();
|
||||||
|
expect(')');
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
if (peek().t === '!') {
|
||||||
|
consume();
|
||||||
|
var notTok = consume();
|
||||||
|
if (!notTok || notTok.t !== 'id') throw new Error('Expected key after !');
|
||||||
|
return !ctxVal(notTok.v);
|
||||||
|
}
|
||||||
|
if (peek().t === 'id') {
|
||||||
|
var keyTok = consume();
|
||||||
|
if (peek() && peek().t === '==') {
|
||||||
|
consume();
|
||||||
|
var eqTok = consume();
|
||||||
|
if (!eqTok) throw new Error('Expected value after ==');
|
||||||
|
return compareValue(ctxVal(keyTok.v), eqTok.t === 'str' ? eqTok.v : (eqTok.v || ''), true);
|
||||||
|
}
|
||||||
|
if (peek() && peek().t === '!=') {
|
||||||
|
consume();
|
||||||
|
var neTok = consume();
|
||||||
|
if (!neTok) throw new Error('Expected value after !=');
|
||||||
|
return compareValue(ctxVal(keyTok.v), neTok.t === 'str' ? neTok.v : (neTok.v || ''), false);
|
||||||
|
}
|
||||||
|
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 (_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeWhenExpression(expr) {
|
||||||
|
var tokens = [];
|
||||||
|
var i = 0;
|
||||||
|
while (i < expr.length) {
|
||||||
|
var ch = expr[i];
|
||||||
|
if (ch === ' ' || ch === '\t') { i += 1; continue; }
|
||||||
|
if (ch === '(') { tokens.push({ t: '(' }); i += 1; continue; }
|
||||||
|
if (ch === ')') { tokens.push({ t: ')' }); i += 1; 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 += 1; continue; }
|
||||||
|
if (ch === '"' || ch === "'") {
|
||||||
|
i += 1;
|
||||||
|
var str = '';
|
||||||
|
while (i < expr.length && expr[i] !== ch) {
|
||||||
|
str += expr[i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if (i < expr.length) i += 1;
|
||||||
|
tokens.push({ t: 'str', v: str });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(ch >= 'a' && ch <= 'z') ||
|
||||||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
|
(ch >= '0' && ch <= '9') ||
|
||||||
|
ch === '_' ||
|
||||||
|
ch === '.'
|
||||||
|
) {
|
||||||
|
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 += 1;
|
||||||
|
}
|
||||||
|
tokens.push({ t: 'id', v: id });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error('Unexpected char: ' + ch);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__mnoteFileTreeContextMenuRuntime = {
|
||||||
|
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
|
||||||
|
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
|
||||||
|
tokenizeWhenExpression: tokenizeWhenExpression
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// MNote 主壳文件树运行时外置模块。
|
||||||
|
// 当前先承接 SIDEBAR_TREE_JS 中低风险的行数据解析 helper;
|
||||||
|
// inline script 保留 fallback,避免模块加载顺序影响首屏行为。
|
||||||
|
|
||||||
|
function fileTreeRowKind(row) {
|
||||||
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
|
return String(row.getAttribute('data-row-kind') || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeRowDocumentId(row) {
|
||||||
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
|
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeRowAssetId(row, deps) {
|
||||||
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
|
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
|
||||||
|
if (assetId) return assetId;
|
||||||
|
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
|
||||||
|
if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') {
|
||||||
|
return String(row.getAttribute('data-row-id') || '').trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeLocalEncodedPath(value) {
|
||||||
|
var path = String(value || '').trim().replace(/~2F/g, '/');
|
||||||
|
if (!path) return '';
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(path);
|
||||||
|
} catch (_) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeRowLocalRelativePath(row, deps) {
|
||||||
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
|
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
||||||
|
if (direct) return direct;
|
||||||
|
var localFilePathFromAssetId = deps && typeof deps.localFilePathFromAssetId === 'function'
|
||||||
|
? deps.localFilePathFromAssetId
|
||||||
|
: function() { return ''; };
|
||||||
|
var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row, deps));
|
||||||
|
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, deps) {
|
||||||
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
|
var kind = fileTreeRowKind(row);
|
||||||
|
var relativePath = fileTreeRowLocalRelativePath(row, deps);
|
||||||
|
if (!relativePath) return '';
|
||||||
|
if (kind === 'folder' || kind === 'directory') return relativePath;
|
||||||
|
var lastSlash = relativePath.lastIndexOf('/');
|
||||||
|
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileTreeDownloadableAssetRow(row, deps) {
|
||||||
|
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, deps));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileTreeDownloadableRow(row, deps) {
|
||||||
|
if (!(row instanceof HTMLElement)) return false;
|
||||||
|
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
|
||||||
|
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row, deps);
|
||||||
|
return Boolean(fileTreeRowLocalRelativePath(row, deps) || isFileTreeDownloadableAssetRow(row, deps));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeAssetDownloadDetail(row, deps) {
|
||||||
|
if (!isFileTreeDownloadableRow(row, deps)) return null;
|
||||||
|
var rowTitle = deps && typeof deps.rowTitle === 'function' ? deps.rowTitle : function() { return ''; };
|
||||||
|
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
||||||
|
var title = rowTitle(row);
|
||||||
|
var relativePath = fileTreeRowLocalRelativePath(row, deps);
|
||||||
|
var assetId = fileTreeRowAssetId(row, deps) || (relativePath ? 'local-file:' + relativePath : '');
|
||||||
|
return {
|
||||||
|
contextKind: 'filetree',
|
||||||
|
documentId: fileTreeRowDocumentId(row),
|
||||||
|
rowId: row.getAttribute('data-row-id') || '',
|
||||||
|
rowKind: fileTreeRowKind(row),
|
||||||
|
assetId: assetId,
|
||||||
|
localRelativePath: relativePath,
|
||||||
|
title: title,
|
||||||
|
fileName: title,
|
||||||
|
workspaceId: resolveWorkspaceId(row)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeRowsByRowIds(rowIds, deps) {
|
||||||
|
var cssEscape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
|
||||||
|
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, deps) {
|
||||||
|
if (!documentId) return 0;
|
||||||
|
var cssEscape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
|
||||||
|
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
|
||||||
|
var node = row ? row.closest('.tree-node') : null;
|
||||||
|
var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : [];
|
||||||
|
return children.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__mnoteFileTreeRuntime = {
|
||||||
|
fileTreeRowKind: fileTreeRowKind,
|
||||||
|
fileTreeRowDocumentId: fileTreeRowDocumentId,
|
||||||
|
fileTreeRowAssetId: fileTreeRowAssetId,
|
||||||
|
decodeLocalEncodedPath: decodeLocalEncodedPath,
|
||||||
|
fileTreeRowLocalRelativePath: fileTreeRowLocalRelativePath,
|
||||||
|
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
|
||||||
|
isFileTreeDownloadableAssetRow: isFileTreeDownloadableAssetRow,
|
||||||
|
isFileTreeDownloadableRow: isFileTreeDownloadableRow,
|
||||||
|
fileTreeAssetDownloadDetail: fileTreeAssetDownloadDetail,
|
||||||
|
fileTreeRowsByRowIds: fileTreeRowsByRowIds,
|
||||||
|
fileTreeChildCount: fileTreeChildCount
|
||||||
|
};
|
||||||
@@ -102,6 +102,14 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/local-upload-runtime.js",
|
"/api/mnote-browser-runtime/local-upload-runtime.js",
|
||||||
get(web_shell::local_upload_runtime_asset),
|
get(web_shell::local_upload_runtime_asset),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||||
|
get(web_shell::filetree_runtime_asset),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||||
|
get(web_shell::filetree_context_menu_runtime_asset),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
get(web_shell::tree_live_controller_runtime_asset),
|
get(web_shell::tree_live_controller_runtime_asset),
|
||||||
@@ -508,6 +516,42 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mnote_browser_runtime_assets_are_explicitly_mounted() {
|
||||||
|
for path in [
|
||||||
|
"/api/mnote-browser-runtime/resource-open-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/local-upload-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
|
] {
|
||||||
|
let response = app(false)
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(path)
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("request"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("response");
|
||||||
|
assert_eq!(
|
||||||
|
response.status(),
|
||||||
|
StatusCode::OK,
|
||||||
|
"{path} 应返回 runtime asset"
|
||||||
|
);
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(axum::http::header::CONTENT_TYPE)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
assert!(
|
||||||
|
content_type.starts_with("application/javascript"),
|
||||||
|
"{path} 应声明 JavaScript content-type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dev_hot_reload_endpoint_is_mounted() {
|
async fn dev_hot_reload_endpoint_is_mounted() {
|
||||||
let response = app(false)
|
let response = app(false)
|
||||||
|
|||||||
@@ -4395,6 +4395,34 @@ pub async fn local_upload_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn filetree_context_menu_runtime_asset() -> Response {
|
||||||
|
const JS: &str = include_str!("../../browser/filetree-context-menu-runtime.js");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
"application/javascript; charset=utf-8",
|
||||||
|
)
|
||||||
|
.header(header::CACHE_CONTROL, "no-store")
|
||||||
|
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||||
|
.body(Body::from(JS))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn filetree_runtime_asset() -> Response {
|
||||||
|
const JS: &str = include_str!("../../browser/filetree-runtime.js");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
"application/javascript; charset=utf-8",
|
||||||
|
)
|
||||||
|
.header(header::CACHE_CONTROL, "no-store")
|
||||||
|
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||||
|
.body(Body::from(JS))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn tree_live_controller_runtime_asset() -> Response {
|
pub async fn tree_live_controller_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/tree-live-controller.js");
|
const JS: &str = include_str!("../../browser/tree-live-controller.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|||||||
@@ -3146,6 +3146,22 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
return typeof fn === 'function' ? fn : null;
|
return typeof fn === 'function' ? fn : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileTreeRuntimeFunction(name) {
|
||||||
|
var runtime = window.__mnoteFileTreeRuntime;
|
||||||
|
var fn = runtime && runtime[name];
|
||||||
|
return typeof fn === 'function' ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTreeRuntimeDeps() {
|
||||||
|
return {
|
||||||
|
currentSourceKind: currentSourceKind,
|
||||||
|
localFilePathFromAssetId: localFilePathFromAssetId,
|
||||||
|
rowTitle: rowTitle,
|
||||||
|
resolveWorkspaceId: resolveWorkspaceId,
|
||||||
|
cssEscape: cssEscape
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function uploadedAssetTitle(asset) {
|
function uploadedAssetTitle(asset) {
|
||||||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
|
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
|
||||||
if (runtimeFn) return runtimeFn(asset);
|
if (runtimeFn) return runtimeFn(asset);
|
||||||
@@ -4656,6 +4672,21 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
|
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
|
||||||
|
|
||||||
function buildSidebarFileTreeContext(kind) {
|
function buildSidebarFileTreeContext(kind) {
|
||||||
|
var runtime = window.__mnoteFileTreeContextMenuRuntime;
|
||||||
|
if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') {
|
||||||
|
try {
|
||||||
|
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) + '"]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
var s = sidebarFileTreeSelection;
|
var s = sidebarFileTreeSelection;
|
||||||
var rowIds = Array.from(s.selectedRowIds || []);
|
var rowIds = Array.from(s.selectedRowIds || []);
|
||||||
var rows = [];
|
var rows = [];
|
||||||
@@ -4679,6 +4710,10 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function evaluateSidebarFileTreeWhen(ctx, expr) {
|
function evaluateSidebarFileTreeWhen(ctx, expr) {
|
||||||
|
var runtime = window.__mnoteFileTreeContextMenuRuntime;
|
||||||
|
if (runtime && typeof runtime.evaluateSidebarFileTreeWhen === 'function') {
|
||||||
|
return runtime.evaluateSidebarFileTreeWhen(ctx || {}, expr || '');
|
||||||
|
}
|
||||||
if (!expr) return true;
|
if (!expr) return true;
|
||||||
try {
|
try {
|
||||||
expr = expr.trim();
|
expr = expr.trim();
|
||||||
@@ -5099,11 +5134,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowDocumentId(row) {
|
function fileTreeRowDocumentId(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowDocumentId');
|
||||||
|
if (runtimeFn) return runtimeFn(row);
|
||||||
if (!(row instanceof HTMLElement)) return '';
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowAssetId(row) {
|
function fileTreeRowAssetId(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowAssetId');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!(row instanceof HTMLElement)) return '';
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
|
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
|
||||||
if (assetId) return assetId;
|
if (assetId) return assetId;
|
||||||
@@ -5114,6 +5153,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function decodeLocalEncodedPath(value) {
|
function decodeLocalEncodedPath(value) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('decodeLocalEncodedPath');
|
||||||
|
if (runtimeFn) return runtimeFn(value);
|
||||||
var path = String(value || '').trim().replace(/~2F/g, '/');
|
var path = String(value || '').trim().replace(/~2F/g, '/');
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
try {
|
try {
|
||||||
@@ -5124,6 +5165,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowLocalRelativePath(row) {
|
function fileTreeRowLocalRelativePath(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!(row instanceof HTMLElement)) return '';
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
||||||
if (direct) return direct;
|
if (direct) return direct;
|
||||||
@@ -5148,6 +5191,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowLocalUploadTargetRelativePath(row) {
|
function fileTreeRowLocalUploadTargetRelativePath(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalUploadTargetRelativePath');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!(row instanceof HTMLElement)) return '';
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
var kind = fileTreeRowKind(row);
|
var kind = fileTreeRowKind(row);
|
||||||
var relativePath = fileTreeRowLocalRelativePath(row);
|
var relativePath = fileTreeRowLocalRelativePath(row);
|
||||||
@@ -5158,11 +5203,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowKind(row) {
|
function fileTreeRowKind(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind');
|
||||||
|
if (runtimeFn) return runtimeFn(row);
|
||||||
if (!(row instanceof HTMLElement)) return '';
|
if (!(row instanceof HTMLElement)) return '';
|
||||||
return String(row.getAttribute('data-row-kind') || '').trim();
|
return String(row.getAttribute('data-row-kind') || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFileTreeDownloadableAssetRow(row) {
|
function isFileTreeDownloadableAssetRow(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableAssetRow');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!(row instanceof HTMLElement)) return false;
|
if (!(row instanceof HTMLElement)) return false;
|
||||||
var kind = fileTreeRowKind(row);
|
var kind = fileTreeRowKind(row);
|
||||||
if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false;
|
if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false;
|
||||||
@@ -5170,12 +5219,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isFileTreeDownloadableRow(row) {
|
function isFileTreeDownloadableRow(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableRow');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!(row instanceof HTMLElement)) return false;
|
if (!(row instanceof HTMLElement)) return false;
|
||||||
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row);
|
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row);
|
||||||
return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row));
|
return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeAssetDownloadDetail(row) {
|
function fileTreeAssetDownloadDetail(row) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeAssetDownloadDetail');
|
||||||
|
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||||||
if (!isFileTreeDownloadableRow(row)) return null;
|
if (!isFileTreeDownloadableRow(row)) return null;
|
||||||
var title = rowTitle(row);
|
var title = rowTitle(row);
|
||||||
var relativePath = fileTreeRowLocalRelativePath(row);
|
var relativePath = fileTreeRowLocalRelativePath(row);
|
||||||
@@ -5360,12 +5413,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeRowsByRowIds(rowIds) {
|
function fileTreeRowsByRowIds(rowIds) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsByRowIds');
|
||||||
|
if (runtimeFn) return runtimeFn(rowIds, fileTreeRuntimeDeps());
|
||||||
return (rowIds || []).map(function(rowId) {
|
return (rowIds || []).map(function(rowId) {
|
||||||
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(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; });
|
}).filter(function(row) { return row instanceof HTMLElement; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileTreeChildCount(documentId) {
|
function fileTreeChildCount(documentId) {
|
||||||
|
var runtimeFn = fileTreeRuntimeFunction('fileTreeChildCount');
|
||||||
|
if (runtimeFn) return runtimeFn(documentId, fileTreeRuntimeDeps());
|
||||||
if (!documentId) return 0;
|
if (!documentId) return 0;
|
||||||
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
|
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
|
||||||
var node = row ? row.closest('.tree-node') : null;
|
var node = row ? row.closest('.tree-node') : null;
|
||||||
@@ -10480,6 +10537,8 @@ pub fn PageLayout(
|
|||||||
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
|
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
|
||||||
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
|
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
|
||||||
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
|
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
|
||||||
|
<script type="module" src="/api/mnote-browser-runtime/filetree-runtime.js"></script>
|
||||||
|
<script type="module" src="/api/mnote-browser-runtime/filetree-context-menu-runtime.js"></script>
|
||||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||||
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
|
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -10520,6 +10579,9 @@ pub fn PageLayout(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::SIDEBAR_TREE_JS;
|
use super::SIDEBAR_TREE_JS;
|
||||||
|
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
||||||
|
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
||||||
|
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
|
||||||
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
||||||
|
|
||||||
fn js_function_body(source: &str, name: &str) -> String {
|
fn js_function_body(source: &str, name: &str) -> String {
|
||||||
@@ -10844,6 +10906,37 @@ mod tests {
|
|||||||
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback() {
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("function fileTreeRuntimeFunction"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("window.__mnoteFileTreeRuntime"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeRowAssetId')"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeRowLocalRelativePath')"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("fileTreeRuntimeFunction('fileTreeChildCount')"));
|
||||||
|
let document_id_body = js_function_body(SIDEBAR_TREE_JS, "fileTreeRowDocumentId");
|
||||||
|
assert!(
|
||||||
|
document_id_body.contains("data-document-id")
|
||||||
|
&& document_id_body.contains("data-doc-id"),
|
||||||
|
"inline fallback 必须保留旧 data attribute 解析"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filetree_runtime_contains_row_accessor_helpers() {
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowDocumentId"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowAssetId"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function decodeLocalEncodedPath"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalRelativePath"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalUploadTargetRelativePath"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeAssetDownloadDetail"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsByRowIds"));
|
||||||
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
||||||
assert!(SIDEBAR_TREE_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
assert!(SIDEBAR_TREE_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
||||||
@@ -11044,6 +11137,7 @@ mod tests {
|
|||||||
fn sidebar_filetree_command_context_helper_functions_exist() {
|
fn sidebar_filetree_command_context_helper_functions_exist() {
|
||||||
assert!(SIDEBAR_TREE_JS.contains("function buildSidebarFileTreeContext"));
|
assert!(SIDEBAR_TREE_JS.contains("function buildSidebarFileTreeContext"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("function evaluateSidebarFileTreeWhen"));
|
assert!(SIDEBAR_TREE_JS.contains("function evaluateSidebarFileTreeWhen"));
|
||||||
|
assert!(SIDEBAR_TREE_JS.contains("__mnoteFileTreeContextMenuRuntime"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("'workspace.sourceKind'"));
|
assert!(SIDEBAR_TREE_JS.contains("'workspace.sourceKind'"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("'workspace.readonly'"));
|
assert!(SIDEBAR_TREE_JS.contains("'workspace.readonly'"));
|
||||||
assert!(SIDEBAR_TREE_JS.contains("'tree.focusKind'"));
|
assert!(SIDEBAR_TREE_JS.contains("'tree.focusKind'"));
|
||||||
@@ -11052,6 +11146,17 @@ mod tests {
|
|||||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-workspace-readonly"));
|
assert!(SIDEBAR_TREE_JS.contains("data-mnote-workspace-readonly"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filetree_context_menu_runtime_contains_command_context_helpers() {
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function buildSidebarFileTreeContext"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function tokenizeWhenExpression"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.sourceKind'"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.readonly'"));
|
||||||
|
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'tree.selectionCount'"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sidebar_filetree_context_menu_items_have_when_for_readonly() {
|
fn sidebar_filetree_context_menu_items_have_when_for_readonly() {
|
||||||
// FileTree 分支的写操作必须带 when: '!workspace.readonly'。
|
// FileTree 分支的写操作必须带 when: '!workspace.readonly'。
|
||||||
|
|||||||
Reference in New Issue
Block a user