feat(tree): checkpoint resource lifecycle work
提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。 不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
This commit is contained in:
+4
@@ -2,6 +2,10 @@
|
||||
|
||||
> 更新时间:2026-05-13
|
||||
>
|
||||
> 2026-05-15 口径更新:
|
||||
> - 本文件完成时的 `index.md` 可见 UI 模型已被 `/mnt/Data1T/mnote/design/04-tree-domain/process/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖。
|
||||
> - 后续 Convex File Tree 默认页面正文行显示为 `{title}.md`,rowId 为 `doc:<documentId>`;本文中的 `index.md` 仅表示历史正文 object identity / 兼容语义,不再作为默认可见子行继续扩展。
|
||||
>
|
||||
> 上游依据:
|
||||
> - `/mnt/Data1T/mnote/design/10-review/05-tree.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 4-25 Trash Empty 隔离 Workspace 验收设计
|
||||
|
||||
> 状态:done
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 3
|
||||
> 范围:页面、附件、mindmap、table 删除进垃圾箱后,执行 workspace 级 empty trash,并确认 Convex 行与关联数据被永久清理。
|
||||
|
||||
## 背景
|
||||
|
||||
`documents.emptyTrashByWorkspace`、`mediaAssets.emptyTrashByWorkspace`、`mindmaps.emptyTrashByWorkspace`、`tables.emptyTrashByWorkspace` 都是 workspace 级破坏性操作。默认测试账号 `mnote.e2e@example.com` 已有长期工作区与历史垃圾箱数据,不能直接在该 workspace 上点击“清空垃圾箱”做 smoke,否则可能误删非本轮测试数据。
|
||||
|
||||
阶段 3 的验收必须先建立隔离边界,再执行真实清空。
|
||||
|
||||
## 目标
|
||||
|
||||
- 使用本轮独立身份或独立 workspace 创建测试页面、子页面、附件、mindmap、table。
|
||||
- 对五类对象执行默认删除,确认进入对应垃圾箱。
|
||||
- 执行页面垃圾箱清空与资源垃圾箱清空。
|
||||
- 查询 Convex,确认本轮 document / media_asset / mindmap / document_table / document_table_rows 不再存在。
|
||||
- 记录关联清理边界:Storage、OCR、LightRAG、page references、recent pages、favorites。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
优先使用一次性 Convex Auth 用户:
|
||||
|
||||
1. 通过 Rust `/api/auth` 走 password `signUp`,邮箱使用 `mnote.trash.<timestamp>@example.com`。
|
||||
2. 使用同一 browser/request context 打开 `/`,触发 `workspaces.ensureDefaultWorkspace` 创建独立 personal workspace。
|
||||
3. 在该 workspace 内创建:
|
||||
- 根页面 `TEST-10REVIEW-07-P3-root-*`
|
||||
- 子页面 `TEST-10REVIEW-07-P3-child-*`
|
||||
- 附件 `TEST-10REVIEW-07-P3-file.txt`
|
||||
- mindmap `TEST-10REVIEW-07-P3-mind`
|
||||
- table `TEST-10REVIEW-07-P3-table`
|
||||
4. 通过 Rust 3000 主壳 API 执行软删除:
|
||||
- 页面:`POST /api/tree/commands`,`action: "archive"`
|
||||
- 附件:`POST /api/media/batch`,`action: "delete"`
|
||||
- mindmap:`DELETE /api/mindmap/{docId}/{mindmapId}`
|
||||
- table:`DELETE /api/tables/{tableId}`
|
||||
5. 打开 `/trash?workspaceId=<isolated>`,确认页面区与资源区出现本轮数据。
|
||||
6. 调用:
|
||||
- `POST /api/documents/empty-trash`
|
||||
- `POST /api/media/empty-trash`
|
||||
- `POST /api/mindmap-trash/empty`
|
||||
- `POST /api/tables/empty-trash`
|
||||
7. 使用 Convex query 或 Rust 只读 API 查询,确认本轮对象不存在。
|
||||
|
||||
## 禁止方案
|
||||
|
||||
- 不得在默认测试账号既有 workspace 上执行 workspace 级清空。
|
||||
- 不得只用 UI 列表消失作为永久删除证据。
|
||||
- 不得把 route fixture 单测等同于真实 Convex 清空验收。
|
||||
|
||||
## 执行结果
|
||||
|
||||
- 已新增 `scripts/task427-trash-empty-isolated-workspace-smoke.js`,覆盖“一次性用户注册 -> 独立 workspace -> seed 五类对象 -> 删除进垃圾箱 -> 清空 -> Convex 不存在断言”。
|
||||
- 2026-05-15 执行通过:一次性 workspace `ws_req_1778793827876_33` 内,页面、子页面、附件、mindmap、table 均完成删除进垃圾箱与 empty trash 后不可查询断言。
|
||||
- Rust 3000 已补资源软删除兼容入口;正式 `tree.resource.*` 命令仍由 10-review 阶段 5 收口。
|
||||
- LightRAG 目前可见 ingest 任务入口,未见 purge 反向删除索引路径;阶段 3 验收只能记录该边界,不能标为已清理。
|
||||
|
||||
## 验收产物
|
||||
|
||||
- 新增或复用一个 smoke 脚本,默认只操作一次性用户 / 一次性 workspace。
|
||||
- 输出本轮 `workspaceId`、`documentId`、`childDocumentId`、`assetId`、`mindmapId`、`tableId`。
|
||||
- 输出清空前后的 Convex 查询摘要。
|
||||
- 将结果回填到 `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 3。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 4-26 FileTree 批量删除与选择矩阵 Smoke
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 4
|
||||
|
||||
## 1. 目标
|
||||
|
||||
补齐 Convex filetree 下的真实浏览器验收,避免只用 reducer、local-folder smoke 或 API seed 证明“文件树多选删除已完成”。
|
||||
|
||||
本设计只覆盖阶段 4 的最小闭环:
|
||||
|
||||
- 隔离用户 / 隔离 workspace seed 3 个页面、2 个普通附件、1 个 mindmap、1 个 table。
|
||||
- 在 3000 主入口打开 filetree UI。
|
||||
- 验证 Ctrl/Cmd 多选、Shift 范围、右键已选项保持 selection、右键未选项切换 action target。
|
||||
- 验证 Delete / Backspace 触发混合 doc + asset 批量删除。
|
||||
- 验证确认文案按页面、附件、mindmap、table 计数。
|
||||
- 验证删除后无需浏览器刷新,filetree 中目标行立即消失,并能在 trash dataset 或 `/trash` 中看到。
|
||||
|
||||
## 2. 安全边界
|
||||
|
||||
- 不在默认测试账号既有 workspace 上执行批量删除。
|
||||
- 不对默认 workspace 执行 `emptyTrashByWorkspace`。
|
||||
- 使用一次性 Convex Auth 用户注册后自动获得的默认 workspace。
|
||||
- 清理仅针对本轮创建对象;如清理失败,只记录对象 ID,不扩大删除范围。
|
||||
|
||||
## 3. 建议脚本
|
||||
|
||||
新增:
|
||||
|
||||
- `scripts/task428-filetree-bulk-delete-selection-smoke.js`
|
||||
|
||||
脚本输入:
|
||||
|
||||
- `MNOTE_UI_BASE_URL`:默认 `http://127.0.0.1:3001`,便于复用 `FRONTEND_PORT=3001 npm run desktop:hot`。
|
||||
- `NEXT_PUBLIC_CONVEX_URL` / `CONVEX_SELF_HOSTED_URL`:默认 `http://127.0.0.1:3210`。
|
||||
|
||||
脚本输出:
|
||||
|
||||
- `tmp/task428-filetree-bulk-delete-selection-smoke/result.json`
|
||||
- 包含本轮 email、workspaceId、docIds、assetIds、captured confirm 文案、tree command / resource request 摘要、trash 验证结果。
|
||||
|
||||
## 4. 验收矩阵
|
||||
|
||||
### 4.1 Seed
|
||||
|
||||
- 创建父页面 `root`。
|
||||
- 创建子页面 `child`。
|
||||
- 创建兄弟页面 `sibling`。
|
||||
- 创建资源页面 `resources`,用于挂载:
|
||||
- 普通附件 2 个。
|
||||
- mindmap 1 个。
|
||||
- table 1 个。
|
||||
|
||||
### 4.2 Selection
|
||||
|
||||
- Ctrl/Cmd 点击:选中 `root` 与一个外部普通附件。
|
||||
- Shift 点击:在可见行上形成范围选区,至少包含 2 行。
|
||||
- 右键已选项:selection 数量保持不变。
|
||||
- 右键未选项:selection 切换为该项。
|
||||
|
||||
### 4.3 Delete / Backspace
|
||||
|
||||
第一轮 Delete:
|
||||
|
||||
- 选择 `root`、`child`、外部普通附件、mindmap、table。
|
||||
- 预期 preflight plan 去重:`root` 保留,`child` 被父页面覆盖,外部资源保留。
|
||||
- 确认文案应包含:
|
||||
- `1 个页面`
|
||||
- `1 个附件`
|
||||
- `1 个思维导图`
|
||||
- `1 个在线表格`
|
||||
- 确认后:
|
||||
- `root`、`child`、外部普通附件、mindmap、table 对应行无需刷新消失。
|
||||
- Convex trash / sidebar dataset 可查询到对应 deleted / archived 状态。
|
||||
|
||||
第二轮 Backspace:
|
||||
|
||||
- 选择 `sibling` 与第二个普通附件。
|
||||
- 触发 Backspace,确认后无需刷新消失。
|
||||
- 作为 Delete 入口的键盘等价验证。
|
||||
|
||||
## 5. 执行结果
|
||||
|
||||
`scripts/task428-filetree-bulk-delete-selection-smoke.js` 已落地并通过真实浏览器验证:
|
||||
|
||||
- 创建一次性用户和隔离 workspace。
|
||||
- seed 页面、附件、mindmap、table。
|
||||
- 验证 Ctrl/Cmd 多选、Shift 范围、右键已选保持、右键未选切换。
|
||||
- 验证 Delete 混合删除页面 + 附件 + mindmap + table。
|
||||
- 验证 Backspace 混合删除页面 + 附件。
|
||||
- 验证确认文案计数、filetree 行无需刷新消失、Convex 垃圾箱状态。
|
||||
- 通过 route interception 强制 `/api/media/batch` 失败,验证页面成功进垃圾箱、失败附件仍保留,并出现“部分对象删除失败”摘要。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 4-29 VSCode Explorer Cut Paste Move v1
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7
|
||||
|
||||
## 1. 目标
|
||||
|
||||
先收口阶段 7 的最小可执行项:文件树 `Ctrl/Cmd+X` 后 `Ctrl/Cmd+V` 必须执行移动,而不是复制或 no-op。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
- `wolai-frontend` 主 Sidebar 文件树剪贴板 action 从 `copy` 扩展为 `copy | cut`。
|
||||
- `Ctrl/Cmd+C` 写入 `copy` payload,`Ctrl/Cmd+X` 写入 `cut` payload。
|
||||
- `Ctrl/Cmd+V` 读取 `cut` payload 时:
|
||||
- Rust family renderer 复用 `tree.filetree.drop.preflight` 的 move plan。
|
||||
- 页面移动走 `moveDocumentCommand`。
|
||||
- 普通附件移动走 `moveFileTreeResourceAssets`。
|
||||
- 成功后清空剪贴板并刷新/广播相关树变化。
|
||||
- `copy` 路径保持原行为。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
- F2 inline rename、右键菜单补齐、DnD readonly / 冲突确认不在本小阶段内实现,继续留在阶段 7 后续子项。
|
||||
- 不扩展 `tree.filetree.paste.preflight` 的 Rust plan;cut 粘贴复用 drop preflight,是当前最小改动。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- `decodeFileTreeClipboardPayload` 能识别 `copy | cut`,拒绝其它 action。
|
||||
- Sidebar 快捷键监听同时处理 `Ctrl/Cmd+C`、`Ctrl/Cmd+X`、`Ctrl/Cmd+V`。
|
||||
- cut paste 分支不调用 `copyTreeCommand` / `copyFileTreeResourceAssets`,而调用 `moveDocumentCommand` / `moveFileTreeResourceAssets`。
|
||||
- 成功 cut paste 后清空剪贴板,避免二次粘贴重复移动。
|
||||
|
||||
## 5. 已落地
|
||||
|
||||
- `src/lib/file-tree/clipboard.ts`:
|
||||
- `FileTreeClipboardAction` 扩展为 `copy | cut`。
|
||||
- decoder 允许 `cut`,继续拒绝其它 action。
|
||||
- 新增 `clearFileTreeClipboardPayload`。
|
||||
- `src/components/sidebar/sidebar.tsx`:
|
||||
- 全局文件树快捷键支持 `Ctrl/Cmd+X`。
|
||||
- Rust family renderer 的 cut paste 复用 `preflightFileTreeInternalDrop(... copy: false)`。
|
||||
- cut paste 执行页面移动与附件移动,成功后清空剪贴板。
|
||||
- `src/components/sidebar/tree-pane-bindings.ts` 透出清空剪贴板 helper。
|
||||
|
||||
## 6. 验证命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/lib/file-tree/clipboard.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts
|
||||
```
|
||||
|
||||
结果:2 个测试文件通过,7 项测试通过。
|
||||
|
||||
局部 TypeScript 筛查:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit --pretty false 2>&1 | rg "(src/lib/file-tree/clipboard.ts|src/components/sidebar/tree-pane-bindings.ts|src/components/sidebar/sidebar.tsx)"
|
||||
```
|
||||
|
||||
结果:无输出,本轮修改文件无新增 TypeScript 报错。全量 `tsc` 仍受仓库既有错误影响。
|
||||
|
||||
## 7. 待补
|
||||
|
||||
- 2026-05-15 已补 3000 主入口真实浏览器 smoke:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js`。
|
||||
- `task430` 使用一次性用户 / workspace,验证页面行 `Ctrl/Cmd+X` 后聚焦目标页面并 `Ctrl/Cmd+V`,捕获 `/api/tree/commands action=move`,并通过 Convex 查询确认子页面 `parent_id` 变为目标父页面。
|
||||
- 普通附件 cut-paste move 的执行路径已有代码与 preflight 设计,真实附件 move 全矩阵仍归入后续资源树 / DnD 综合 smoke,不阻塞本小阶段“页面 cut-paste move”验收。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 4-30 VSCode Explorer F2 Inline Rename v1
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7
|
||||
|
||||
## 1. 目标
|
||||
|
||||
补齐 `wolai-frontend` 文件树 DOM host 的 F2 行内重命名入口,避免键盘路径只触发 reducer 事件但 UI 仍无 inline editor。
|
||||
|
||||
## 2. 已落地
|
||||
|
||||
- `tree-shell-dom-host.tsx` 为 filetree row 增加行内 `tree-rename-input`。
|
||||
- F2 不再只转发 `beginRenameFocused`,而是直接进入当前 row 的 inline rename 状态。
|
||||
- Enter / blur 提交,Escape 取消。
|
||||
- 页面 / index row 提交到 `/api/tree/commands` 的 `rename`,并通过 `onTreeMutation` 回写 `tree.node.renamed`。
|
||||
- 普通 asset row 提交到 `/api/media/batch` 的 `rename`,并广播 `wolai:assets-changed`。
|
||||
- F2 输入框已补行内 validation:空名、非法路径字符、同级重名会在 input 下方显示错误并阻断提交,不再依赖 alert。
|
||||
- 普通附件重命名时,如果用户输入不带扩展名,会保留原附件扩展名后再提交,例如 `附件.pdf` 输入 `附件新名` 会提交 `附件新名.pdf`。
|
||||
- jsdom / 旧浏览器环境缺少 `CSS.escape` 时使用本地 fallback,避免 F2 聚焦逻辑抛错。
|
||||
|
||||
## 3. 验证命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/components/sidebar/tree-shell-host.test.tsx
|
||||
```
|
||||
|
||||
结果:1 个测试文件通过,10 项测试通过;新增测试覆盖 F2 进入 inline input、提交 `tree.node.rename`、空名 / 非法字符 / 同级重名行内阻断,以及附件扩展名保留。
|
||||
|
||||
## 4. 待补
|
||||
|
||||
- 2026-05-15 已补 3000 主入口真实浏览器 smoke:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js`。
|
||||
- `task430` 覆盖页面 filetree row F2 inline rename:请求 `/api/tree/commands action=rename`,Convex 标题更新,无需刷新可见。
|
||||
- `task430` 覆盖普通附件 row F2 inline rename:请求 `/api/media/batch action=rename`,Convex `media_assets.file_name` 更新,无需刷新可见。
|
||||
- 3000 Rust SSR 主壳已补 F2 inline rename;页面树 fallback 中仍保留 prompt 式重命名,按当前口径不属于 filetree F2 小阶段完成条件。
|
||||
- 2026-05-15 二次补强:本次只补 `wolai-frontend` DOM host 的行内 validation;Rust SSR srcdoc 路径如继续作为 3000 主入口,需要在后续 smoke 中补同等级 validation parity。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 4-31 VSCode Explorer Context Menu Minimum v1
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7
|
||||
|
||||
## 1. 目标
|
||||
|
||||
先让主 Sidebar 右键菜单显式覆盖 VSCode Explorer 常见项,避免缺项被误读为“无能力”。已有 executor 的项接真实动作;暂无 executor 的项以禁用态展示原因。
|
||||
|
||||
## 2. 已落地
|
||||
|
||||
- `New File`:复用当前“新建子页面”能力。
|
||||
- `New Folder`:禁用态,说明页面树暂不区分文件夹,待 Resource Tree folder 合同收口。
|
||||
- `Paste Into`:主 Sidebar 节点右键已接真实 paste target,目标为当前菜单节点;`Ctrl/Cmd+V` 继续使用 focused target。
|
||||
- `Refresh`:调用 `refreshTree()`,不使用 `window.location.reload()`。
|
||||
- `Collapse All`:清空展开集合。
|
||||
- `Copy Path`:复制当前页面标题路径。
|
||||
- `Copy Relative Path`:复制不带根斜杠的页面标题路径,按当前 workspace tree 计算。
|
||||
- `Reveal`:展开父链并滚动到当前节点。
|
||||
|
||||
## 3. 验证命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/components/sidebar/sidebar-context-menu-source.test.ts
|
||||
pnpm test src/components/sidebar/sidebar-paste-preflight-source.test.ts -- --runInBand
|
||||
```
|
||||
|
||||
结果:`sidebar-context-menu-source.test.ts` 1 项通过;`sidebar-paste-preflight-source.test.ts` 3 项通过。覆盖菜单项存在、`Copy Relative Path` 接入、`Paste Into` 右键 action target、仍保留 `New Folder` 禁用态、未引入 `window.location.reload`。
|
||||
|
||||
## 4. 待补
|
||||
|
||||
- 2026-05-15 已补 3000 主入口真实浏览器 smoke:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js`。
|
||||
- `task430` 验证 filetree 右键菜单可见 `New File / New Folder / Paste Into / Refresh / Collapse All / Copy Path / Reveal`;当时 `New Folder` 与 `Paste Into` 为禁用态,且 title 中说明原因。
|
||||
- 2026-05-15 二次补强:`Paste Into` 已从禁用态升级为真实 paste target;主 Sidebar 抽出 `executeFileTreePaste(targetDocumentIdOverride)`,右键传当前菜单节点 `node.id`,键盘 `Ctrl/Cmd+V` 传 `null` 保留 focused target。
|
||||
- `New Folder` 需等待 folder/resource tree 合同,而不是在页面树里临时伪造文件夹真相。
|
||||
- 2026-05-15 二次补强:已补 `Copy Relative Path`;空白区右键菜单当前仍没有独立入口,后续若新增必须复用同一 paste helper 和 target 规则。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 4-32 VSCode Explorer DnD Modifier Guard v1
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7
|
||||
|
||||
## 1. 目标
|
||||
|
||||
补齐文件树 DnD copy modifier 的 VSCode 基线:Alt、Ctrl、Meta 任一修饰键都应进入 copy drop,而不是只有 Alt 生效。
|
||||
|
||||
## 2. 已落地
|
||||
|
||||
- `tree-shell-dom-host.tsx` 的 filetree dragover 改为 `Alt / Ctrl / Meta -> copy`。
|
||||
- `mnote-web` SSR 主壳 `layout.rs` 的 filetree dragover / drop 也同步改为 `Alt / Ctrl / Meta -> copy`。
|
||||
|
||||
## 3. 已有防线
|
||||
|
||||
- 父拖子 / 拖到自身禁止由 Rust `tree.filetree.drop.preflight` 校验,错误语义为不能把页面移动到自身或后代下面。
|
||||
- 主 Sidebar 继续依赖 drop preflight,不在 UI 层复制第二套树祖先判断。
|
||||
|
||||
## 4. 验证命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/components/sidebar/sidebar-dnd-source.test.ts
|
||||
```
|
||||
|
||||
结果:1 个测试文件通过,1 项测试通过。
|
||||
|
||||
## 5. 待补
|
||||
|
||||
- 2026-05-15 已补 3000 主入口 `POST /api/tree/filetree/drop-preflight`,通过 Rust bridge `tree.filetree.drop.preflight` 返回真实 preflight plan。
|
||||
- 2026-05-15 已补 3000 主入口真实浏览器 smoke:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task430-vscode-explorer-stage7-smoke.js`。
|
||||
- `task430` 验证自拖自身与父拖子均返回 400;copy modifier preflight 返回 `copy: true` 与 `documentTransferPlan.action=copy`。
|
||||
- 后续 readonly 与命名冲突确认已由 `4-33-vscode-explorer-dnd-readonly-conflict-v1.md` 收口。
|
||||
@@ -0,0 +1,152 @@
|
||||
# 4-33 VSCode Explorer DnD Readonly Conflict v1
|
||||
|
||||
> 状态:done
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7 DnD 剩余项
|
||||
|
||||
## 1. 目标
|
||||
|
||||
补齐阶段 7 中尚未闭合的 DnD 剩余项:主 Sidebar Convex filetree 的 readonly 禁止与命名冲突确认。
|
||||
|
||||
当前已完成的 DnD 证据是:
|
||||
|
||||
- 自拖自身拒绝。
|
||||
- 父拖到子节点 / 后代拒绝。
|
||||
- Alt / Ctrl / Meta copy modifier 被识别为 copy。
|
||||
- 3000 主入口 `/api/tree/filetree/drop-preflight` 已返回 Rust bridge `tree.filetree.drop.preflight` plan。
|
||||
|
||||
尚未完成的是:
|
||||
|
||||
- 主 Sidebar Convex filetree 下的 readonly 目标 / readonly source 禁止。
|
||||
- 主 Sidebar Convex filetree 下的重名目标冲突确认,而不是直接 move/copy。
|
||||
|
||||
## 2. 当前代码事实
|
||||
|
||||
- `bridge-runtime` 的 `FileTreeDropPreflightCommandPayload` 当前只有:
|
||||
- `copy`
|
||||
- `targetDocumentId / targetRowId / focusedRowId / activeDocumentId`
|
||||
- `rowIds`
|
||||
- `rows`
|
||||
- `documentParents`
|
||||
- `FileTreeDropPreflightRow` 当前只有:
|
||||
- `rowId / rowKind / documentId / assetId / assetDocumentId / assetType / storagePath`
|
||||
- 现有 `build_filetree_drop_plan` 能判断:
|
||||
- 目标页面。
|
||||
- doc / asset transfer plan。
|
||||
- 父拖子 / 自拖非法 move。
|
||||
- copy / move action。
|
||||
- 现有 payload 不能判断:
|
||||
- 目标是否 readonly。
|
||||
- source 是否 readonly。
|
||||
- target 下是否已有同名页面 / 附件。
|
||||
- 冲突应 rename、replace、skip 还是 cancel。
|
||||
|
||||
## 3. 设计增量
|
||||
|
||||
### 3.1 Drop preflight payload 增量
|
||||
|
||||
为 `tree.filetree.drop.preflight` payload 增加以下可选字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceCapabilities": ["read", "write", "move", "copy"],
|
||||
"targetCapabilities": ["read", "write", "drop"],
|
||||
"rows": [
|
||||
{
|
||||
"rowId": "doc:...",
|
||||
"title": "页面标题",
|
||||
"operationProfile": "convex_workspace"
|
||||
}
|
||||
],
|
||||
"targetChildren": [
|
||||
{
|
||||
"rowKind": "doc",
|
||||
"documentId": "...",
|
||||
"assetId": null,
|
||||
"title": "页面标题"
|
||||
}
|
||||
],
|
||||
"conflictPolicy": "prompt"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `sourceCapabilities` / `targetCapabilities` 用于表达当前 workspace source 与目标 row 的写权限,不在 UI 层猜测。
|
||||
- `operationProfile` 允许 local readonly、remote readonly、shared readonly 等来源进入同一 preflight,而不是散落在前端。
|
||||
- `title` 与 `targetChildren` 用于做同级重名检测。
|
||||
- `conflictPolicy` 默认 `prompt`,后续可扩展 `rename` / `skip` / `replace`,但本阶段只要求“发现冲突并要求确认”。
|
||||
|
||||
### 3.2 Drop preflight plan 增量
|
||||
|
||||
为 `FileTreeDropPlan` 增加:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowed": true,
|
||||
"blockedReason": null,
|
||||
"requiresConfirmation": false,
|
||||
"conflicts": []
|
||||
}
|
||||
```
|
||||
|
||||
readonly 场景:
|
||||
|
||||
- `allowed=false`
|
||||
- `blockedReason="readonly_target"` 或 `readonly_source"`
|
||||
- 不返回可执行 transfer plan,避免 UI 误执行。
|
||||
|
||||
命名冲突场景:
|
||||
|
||||
- `allowed=true`
|
||||
- `requiresConfirmation=true`
|
||||
- `conflicts` 列出冲突对象、目标父级、建议策略。
|
||||
- UI 必须展示确认,不允许静默执行。
|
||||
|
||||
### 3.3 3000 主壳行为
|
||||
|
||||
- 内部 drop 触发前继续调用 `/api/tree/filetree/drop-preflight`。
|
||||
- 若 `allowed=false`,显示阻塞原因,不发 `/api/tree/commands` 或 `/api/media/batch`。
|
||||
- 若 `requiresConfirmation=true`,显示冲突确认;用户取消时不执行。
|
||||
- 用户确认后,按 plan 执行 move/copy。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- readonly target:
|
||||
- 真实或构造的 readonly target drop preflight 返回 400 或 `allowed=false`。
|
||||
- 浏览器不发执行请求。
|
||||
- UI 有“只读”类可解释反馈。
|
||||
- conflict:
|
||||
- 目标父页面已有同名页面或同名附件时,preflight 返回 `requiresConfirmation=true` 与 `conflicts`。
|
||||
- 用户取消确认后不执行 move/copy。
|
||||
- 用户确认后执行 move/copy,并在结果中记录冲突处理策略。
|
||||
- no false positive:
|
||||
- 非冲突移动不弹冲突确认。
|
||||
- copy modifier 仍保留 `copy=true`。
|
||||
- 父拖子 / 自拖仍被拒绝。
|
||||
|
||||
## 5. 建议执行顺序
|
||||
|
||||
1. 扩展 `bridge-runtime` payload / plan 类型,补 readonly 与 conflict 单测。
|
||||
2. 扩展 3000 `/api/tree/filetree/drop-preflight` route 透传新增字段。
|
||||
3. 扩展 3000 Rust SSR 主壳内部 drop 逻辑,生成 row title / target children / capability payload。
|
||||
4. 新增 `scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js`:
|
||||
- 一次性用户 / workspace。
|
||||
- 构造同名目标,验证 conflict confirmation cancel / confirm。
|
||||
- 构造 readonly payload 或 readonly source fixture,验证 blocked。
|
||||
5. 更新 `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md`,仅在 `task431` 通过后勾选阶段 7 DnD 总项。
|
||||
|
||||
## 6. 当前状态
|
||||
|
||||
截至 2026-05-15,本文件已完成阶段 7 DnD readonly / conflict 的执行与验证:
|
||||
|
||||
- `bridge-runtime` 的 `tree.filetree.drop.preflight` 已支持 source / target capability、target children、conflict policy、`requiresConfirmation` 与 `conflicts`。
|
||||
- readonly target / source 在 preflight 阶段被拒绝;同名目标返回确认计划;copy modifier 不被 readonly move 规则误拒绝。
|
||||
- 主 Sidebar 内部 drop 与 cut-paste move 路径在 `requiresConfirmation` 时确认,取消后不执行后续 move/copy。
|
||||
- 验证命令:
|
||||
- `cargo test -p bridge-runtime tree_filetree_drop_preflight -- --nocapture`
|
||||
- `pnpm test src/lib/file-tree/shell.test.ts src/lib/file-tree/resource-command-client.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-dnd-source.test.ts`
|
||||
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js`
|
||||
|
||||
注意:验证时 3000 常驻进程仍是旧 `mnote-web` 二进制,因此 `task431` 对 3000 初次执行复现旧 readonly 放行行为;当前代码在 3001 新编译进程验证通过,3000 需重启后生效。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 4-37 [done] 垃圾箱改为居中弹窗工作台 v1
|
||||
|
||||
> 更新时间:2026-05-15
|
||||
>
|
||||
> 背景:
|
||||
> - 用户建议:垃圾箱应做成弹窗,而不是单一页面。
|
||||
> - 当前 `/trash` 是完整页面入口;在本地文件夹 / 云空间切换异常时,用户会通过垃圾箱“找回”原空间页面,这进一步暴露垃圾箱作为导航页面会改变当前工作区上下文。
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
把垃圾箱从“离开当前工作区的独立页面”逐步改为“当前工作区内的居中弹窗工作台”。
|
||||
|
||||
弹窗形态以 Wolai 居中对话框为准:遮罩覆盖页面,工作台面板在视口中心显示,左右和上下都保留留白;不要做成靠右侧贴边的 drawer / 抽屉。
|
||||
|
||||
目标体验:
|
||||
|
||||
- 用户点击左下角“垃圾箱”时,不离开当前页面和当前文件树上下文。
|
||||
- 垃圾箱以居中 modal 打开,不贴右侧边缘,不改变当前 URL。
|
||||
- 弹窗内展示页面垃圾箱与资源垃圾箱。
|
||||
- 恢复、彻底删除、清空操作完成后,当前 Sidebar / File Tree / Page Tree 实时同步。
|
||||
- 关闭弹窗后,用户回到原页面、原选中 row、原滚动位置。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 本稿不改变删除、恢复、彻底删除的底层命令语义。
|
||||
- 本稿不一次性废弃 `/trash`;`/trash` 可作为兼容、直接链接和测试入口保留。
|
||||
- 本稿不扩大到全局搜索、模板中心等其它 footer 入口。
|
||||
|
||||
## 3. 最小实现切片
|
||||
|
||||
P0:入口形态
|
||||
|
||||
- 左下角“垃圾箱”从普通导航改为打开弹窗。
|
||||
- 弹窗复用现有 `mnote-trash-workbench` DOM 与 API。
|
||||
- 打开弹窗不改变当前 URL;如需要可用 `?trash=1` 或 history state,但默认不跳整页。
|
||||
|
||||
P1:上下文保持
|
||||
|
||||
- 弹窗打开前记录当前 `sourceKind`、`workspaceId`、`rootUri`、active row、scrollTop。
|
||||
- 关闭弹窗后恢复原 File Tree / Page Tree 视图状态。
|
||||
- 本地文件夹场景下,垃圾箱只展示本地 `.mnote/trash` 能力;Convex 云空间展示 Convex trash。
|
||||
|
||||
P2:实时同步
|
||||
|
||||
- 页面 restore / purge / empty 后,当前 Sidebar 通过 tree event 或本地 apply 更新。
|
||||
- 资源 restore / purge / empty 后,File Tree 对应资源 row 同步恢复或消失。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- 点击“垃圾箱”不会离开当前文档 URL。
|
||||
- 垃圾箱面板在视口中心显示,左右居中偏差不超过 8px,并保留可见的上下留白。
|
||||
- 弹窗内能恢复页面、彻底删除页面、清空页面垃圾箱。
|
||||
- 弹窗内能恢复/彻底删除/清空附件、mindmap、table 等资源。
|
||||
- 关闭弹窗后,原页面仍打开,File Tree 滚动位置和选中 row 不被无关重置。
|
||||
- `/trash` 兼容入口仍可访问并通过现有 smoke。
|
||||
|
||||
## 5. 当前状态
|
||||
|
||||
当前状态:`done`
|
||||
|
||||
2026-05-15 已完成最小可用实现:
|
||||
|
||||
- 左下角“垃圾箱”入口保留 `href="/trash"` 作为兼容 fallback,但主壳点击会拦截为 `data-mnote-action="open-trash-modal"`。
|
||||
- `PageLayout` 新增垃圾箱 modal:打开后 fetch `/trash?workspaceId=...`,解析并挂载现有 `mnote-trash-workbench`,不改变当前 URL。
|
||||
- modal 关闭时会关闭 workbench EventSource,并恢复 File Tree `scrollTop`;不改变原页面、active row 或当前文档 URL。
|
||||
- `/trash` 独立页面继续保留,作为直接链接、兼容入口和现有 smoke 的验证对象。
|
||||
- 本地文件夹上下文下,modal 保持在本地 source,不跳回 Convex;当前先展示 `.mnote/trash` / `trash-index.json` 的本地垃圾箱说明,后续如需完整 local trash list 可继续扩展。
|
||||
- 2026-05-15 根据用户参考图修正 modal 形态:从右侧 drawer 改为居中对话框,并在 smoke 中增加居中与上下留白断言,避免后续重复退回侧栏抽屉形态。
|
||||
|
||||
代码落点:
|
||||
|
||||
- `rust/crates/mnote-web/src/workspace_shell.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- `scripts/task442-trash-modal-workbench-smoke.js`
|
||||
|
||||
已通过验证:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web workspace_shell -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_entry -- --nocapture
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task442-trash-modal-workbench-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js
|
||||
```
|
||||
|
||||
结果文件:
|
||||
|
||||
- `tmp/task442-trash-modal-workbench-smoke/result.json`
|
||||
- `tmp/task432-filetree-trash-page-dual-browser-no-refresh-smoke/result.json`
|
||||
@@ -0,0 +1,161 @@
|
||||
# 4-27 Resource Lifecycle Command Cutover v1
|
||||
|
||||
> 状态:process
|
||||
> 更新时间:2026-05-15
|
||||
> 范围:`tree.resource.archive / restore / purge / rename` 正式命令面,以及 `/api/media/*`、mindmap、table 资源垃圾箱兼容入口的收口边界。
|
||||
|
||||
## 1. 本阶段目标
|
||||
|
||||
- 普通附件 file asset 的删除、恢复、永久删除、重命名必须具备正式 `tree.resource.*` 命令。
|
||||
- `/api/media/batch` 与 `/api/media/purge` 可以继续作为历史 URL,但只能作为兼容 alias,内部必须生成正式 command plan 并记录 command artifact。
|
||||
- mindmap / table 旧 URL 可以保留为兼容 route,但 delete / restore / purge 必须迁移到同一 `tree.resource.*` 命令 payload,不再扩写分散 route 语义。
|
||||
|
||||
## 2. 已落地范围
|
||||
|
||||
- `bridge-runtime` 新增 file asset 生命周期命令计划:
|
||||
- `tree.resource.archive -> mediaAssets:patchById`
|
||||
- `tree.resource.restore -> mediaAssets:patchById`
|
||||
- `tree.resource.purge -> mediaAssets:purgeById`
|
||||
- `tree.resource.rename -> mediaAssets:patchById`
|
||||
- `storage-convex-bridge` 为上述命令提供默认 Convex 映射。
|
||||
- `wolai-frontend` 的 `/api/media/batch`:
|
||||
- `delete` 改为 `tree.resource.archive`
|
||||
- `restore` 改为 `tree.resource.restore`
|
||||
- `rename` 改为 `tree.resource.rename`
|
||||
- `wolai-frontend` 的 `/api/media/purge` 改为 `tree.resource.purge`。
|
||||
- `mnote-web` 的 Rust 3000 `/api/media/batch` 与 `/api/media/purge` 改为通过 runtime command 执行,旧 URL 仅作为兼容 alias。
|
||||
- `mnote-web` 的 Rust 3000 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 改为通过 `tree.resource.archive / restore / purge` 执行,旧 URL 仅作为兼容 alias。
|
||||
- `mnote-web` 的 Rust 3000 `/api/tables/{tableId}` DELETE、`/api/tables/restore`、`/api/tables/purge` 改为通过 `tree.resource.archive / restore / purge` 执行,旧 URL 仅作为兼容 alias。
|
||||
- tree shell command event schema 已补资源生命周期事件。
|
||||
|
||||
## 3. 未完成边界
|
||||
|
||||
- mindmap 与 table 的 DELETE / restore / purge 已进入 `tree.resource.*` runtime command;但旧 URL 仍保留为兼容入口,前端 Sidebar 仍按资源类型分流到这些旧 URL。
|
||||
- `tree.resource.rename` 当前只覆盖普通附件 file asset;mindmap rename 暂不支持,table rename 需要结合 `tables:update` 与 UI 标题语义另行收口。
|
||||
- `empty-trash` 仍是 workspace 级批量兼容合同:`mediaAssets.emptyTrashByWorkspace`、`mindmaps.emptyTrashByWorkspace`、`tables.emptyTrashByWorkspace`,暂不展开为逐资源 `tree.resource.purge`。
|
||||
- 双浏览器 no-refresh、垃圾箱恢复后的 reveal/focus、跨资源 projection delta 细粒度更新仍由后续阶段覆盖。
|
||||
|
||||
## 4. 验收
|
||||
|
||||
- 普通附件通过 `/api/media/batch delete/restore/rename` 或 `/api/media/purge` 操作时,测试中应观察到正式命令名 `tree.resource.archive/restore/rename/purge`。
|
||||
- Mindmap 通过 `/api/mindmap/{docId}/{mindmapId}` DELETE / PATCH restore / PATCH purge 操作时,响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与 `resourceKind=mindmap`。
|
||||
- Table 通过 `/api/tables/{tableId}` DELETE、`/api/tables/restore`、`/api/tables/purge` 操作时,响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与 `resourceKind=table`。
|
||||
- Rust runtime plan 必须包含 `domainEventPlan` 与 `streamDeltaHint`。
|
||||
- Rust 3000 兼容 route 不再直接写 `mediaAssets:patchById / purgeById`,而是先进入 runtime command。
|
||||
- 现有垃圾箱 route 回归保持通过。
|
||||
|
||||
## 5. 验证命令
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_file_asset_commands
|
||||
cargo test -p bridge-runtime tree_resource_lifecycle_plans_cover_mindmap_and_table_commands
|
||||
cargo test -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping
|
||||
cargo test -p mnote-web trash
|
||||
cargo test -p mnote-web runtime_command_event_schema_covers_tree_and_resource_command_channels
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/app/api/media/purge/route.test.ts src/app/api/media/batch/route.test.ts src/lib/documents/rust-runtime.test.ts
|
||||
```
|
||||
|
||||
## 6. 执行记录
|
||||
|
||||
- 2026-05-15:先补 `mnote-web` 路由测试断言 mindmap / table DELETE、restore、purge 响应必须携带 `canonicalCommand=tree.resource.archive/restore/purge` 与对应 `resourceKind`,测试红灯确认旧路由仍只返回 legacy mutation 结果。
|
||||
- 2026-05-15:`mnote-web` mindmap / table 兼容 route 已改为构造 `tree.resource.archive/restore/purge` runtime command,并通过 `execute_runtime_command_via_convex_with_artifacts` 执行;旧 URL 保留。
|
||||
- 2026-05-15:新增 `bridge-runtime` 合同测试 `tree_resource_lifecycle_plans_cover_mindmap_and_table_commands`,断言 mindmap 映射到 `mindmaps:softDelete/restore/purge`,table 映射到 `tables:remove/restore/purge`,并携带 `resourceLifecyclePlan`、`domainEventHint` 与 stream delta hint。
|
||||
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime tree_resource_lifecycle_plans_cover_mindmap_and_table_commands -- --nocapture` 通过,1 项通过。
|
||||
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 通过,3 项通过。
|
||||
- 2026-05-15 验证:`cargo test --manifest-path rust/Cargo.toml -p storage-convex-bridge tree_resource_lifecycle_commands_have_convex_mapping -- --nocapture` 通过,1 项通过。
|
||||
|
||||
## 7. 剩余边界
|
||||
|
||||
- `tree.resource.rename` 仍只覆盖普通附件 file asset;mindmap rename 继续明确不支持,table rename 待结合 `/api/tables/{tableId}` PATCH 与表格标题语义单独收口。
|
||||
- `empty-trash` 仍是 workspace 级批量合同,不把它伪装成逐资源 `tree.resource.purge`。
|
||||
- 前端 Sidebar 仍按类型调用旧 URL;这是兼容 alias,不是新增长期业务语义。后续如要统一客户端命令面,应在 `resource-command-client` 引入 mindmap/table payload,并保留旧 URL 适配层。
|
||||
|
||||
## 8. 子阶段:local_folder file_path resource lifecycle
|
||||
|
||||
### 8.1 目标与范围
|
||||
|
||||
本子阶段只覆盖 `local_folder` workspace 中由本地文件系统提供的非 md 资源文件,典型对象包括图片、PDF、音视频、Office 文件、压缩包和其他附件型二进制文件。Markdown 页面本体、目录节点、远端 media asset、mindmap、table 不在本子阶段内。
|
||||
|
||||
最小目标是把本地非 md 资源文件的 delete / restore / purge 纳入 `tree.resource.*` 正式命令面,同时保留本地文件路径作为用户可理解的 identity 输入。实现不要求立即接入云端文件删除,也不要求新增复杂版本库;只要求命令可审计、垃圾箱可索引、冲突可判定、恢复行为可预测。
|
||||
|
||||
### 8.2 最小设计
|
||||
|
||||
- 资源归属:`resourceKind=local_file`,`resourceScope=local_folder`。
|
||||
- 资源定位:命令 payload 必须携带 `workspaceId`、`rootId`、`filePath`、`resourceKind`;`filePath` 使用相对 `local_folder` 根目录的规范化 UTF-8 路径,不允许绝对路径、空路径、`..` 逃逸或平台分隔符混用。
|
||||
- md 排除:扩展名判定为 Markdown 的文件继续走页面/树节点生命周期,不进入 `local_file` 资源生命周期。
|
||||
- delete:`tree.resource.archive` 不直接物理删除文件,先把源文件移动到本工作区受控 trash 存储区,并写入 trash index。
|
||||
- restore:`tree.resource.restore` 只从 trash index 中恢复已归档资源,默认恢复到原 `filePath`;若原路径冲突,按冲突策略处理。
|
||||
- purge:`tree.resource.purge` 只允许删除 trash index 中已归档资源对应的 trash 文件和索引记录;不允许对当前活跃文件路径直接 purge。
|
||||
- 事件:三类命令都产出 `domainEventPlan` 与 `streamDeltaHint`,至少能通知资源列表、附件引用状态、垃圾箱视图刷新。
|
||||
|
||||
### 8.3 命令 identity
|
||||
|
||||
`local_folder` 文件的稳定命令 identity 分两层:
|
||||
|
||||
- 用户输入 identity:`workspaceId + rootId + normalizedFilePath`。这是兼容 UI、外部变更监听和命令 payload 的最小输入。
|
||||
- 归档后 identity:`trashEntryId`。第一次 archive 成功后生成并写入 trash index,restore / purge 必须优先使用 `trashEntryId`,同时校验其记录的原始 `filePath`、`workspaceId`、`rootId` 与命令 payload 一致。
|
||||
|
||||
`resourceId` 不直接等同于裸 `filePath`。对活跃文件可派生为 `local_file:{workspaceId}:{rootId}:{pathHash}`,但该值只作为事件和 projection 的稳定 key;实际文件操作仍以规范化路径和 trash index 记录为准,避免路径重命名、大小写差异或 Unicode 归一化导致误删。
|
||||
|
||||
### 8.4 Trash index
|
||||
|
||||
每个 `local_folder` root 维护一个受控 trash index,最小字段如下:
|
||||
|
||||
- `trashEntryId`
|
||||
- `workspaceId`
|
||||
- `rootId`
|
||||
- `resourceKind=local_file`
|
||||
- `originalFilePath`
|
||||
- `trashedFilePath`
|
||||
- `fileSize`
|
||||
- `contentHash`,可延后异步补齐,但 purge 前若存在则用于防误删校验
|
||||
- `archivedAt`
|
||||
- `archivedByCommandId`
|
||||
- `restoreStatus`
|
||||
- `purgedAt`
|
||||
- `purgedByCommandId`
|
||||
|
||||
trash index 是 restore / purge 的唯一可信入口。目录扫描只能用于发现活跃文件或 orphan trash 文件,不能绕过 index 直接恢复或永久删除。
|
||||
|
||||
### 8.5 冲突策略
|
||||
|
||||
- archive 时源文件不存在:命令返回 `not_found`,不生成新的 trash entry;若 index 已存在同一路径的活跃归档记录,返回该 `trashEntryId` 并标记为幂等归档结果。
|
||||
- archive 时目标 trash 路径已存在:生成新的 `trashEntryId` 与唯一 `trashedFilePath`,不覆盖旧 trash 文件。
|
||||
- restore 时原路径不存在:直接恢复到 `originalFilePath`,并把 index 状态改为 restored。
|
||||
- restore 时原路径已存在且 contentHash 相同:视为幂等恢复,index 状态改为 restored,不覆盖活跃文件。
|
||||
- restore 时原路径已存在且 contentHash 不同或无法判定:不得覆盖活跃文件;返回 `conflict=file_path_occupied`,UI 可选择恢复为带后缀副本或让用户改名,但默认命令不自动改名。
|
||||
- purge 时 trash 文件不存在但 index 仍为 archived:允许把 index 标记为 purged,并记录 `missing_trash_file`,用于容忍外部手动清理。
|
||||
- purge 时 index 状态不是 archived:返回幂等结果,不删除任何活跃文件。
|
||||
- 任一命令发现 `filePath` 逃逸 root、大小写归一化后指向不同文件、或 symlink 指向 root 外部:返回 `invalid_path`,不移动、不删除。
|
||||
|
||||
### 8.6 验收标准
|
||||
|
||||
- 本地非 md 资源文件 delete 只能通过 `tree.resource.archive` 进入 trash,不允许直接 unlink 活跃文件。
|
||||
- restore / purge 必须优先基于 `trashEntryId` 与 trash index 执行;仅传 `filePath` 时只能定位唯一 archived entry,不能模糊匹配多条记录。
|
||||
- trash index 中能追踪原路径、trash 路径、归档命令、恢复/永久删除命令和当前状态。
|
||||
- restore 冲突时默认不覆盖用户已有文件,并返回可被 UI 展示的结构化冲突原因。
|
||||
- purge 只能作用于 archived trash entry,不能对 active `filePath` 执行永久删除。
|
||||
- Markdown 文件不进入本子阶段;测试中应验证 `.md` / `.markdown` 路径被拒绝或转交页面生命周期。
|
||||
- 命令事件必须包含 `resourceKind=local_file`、`resourceScope=local_folder`、`filePath`、`trashEntryId`(archive 成功后)和 `canonicalCommand=tree.resource.archive/restore/purge`。
|
||||
- 外部变更监听与本命令并发时,以文件系统当前状态和 trash index 状态共同判定;不得因 stale projection 覆盖真实文件系统变化。
|
||||
|
||||
### 8.7 执行记录
|
||||
|
||||
- 2026-05-15:先补 `tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index` 红灯,确认 `local:asset:docs/photo.png` delete 原本掉到 Markdown 删除路径。
|
||||
- 2026-05-15:`mnote-web` local folder executor 已把 `delete / restore / purge` 分流到 entry 级处理;`local:asset:*` / `local:node:*` raw file 通过 `tree.resource.archive/restore/purge` 进入 `.mnote/trash` 与 `trash-index.json`,Markdown 页面继续走原页面生命周期。
|
||||
- 2026-05-15:Rust 3000 主壳 filetree delete plan 已对 `sourceKind=local_folder && rowKind=asset` 使用 rowId 作为本地资源 identity,Delete / Backspace 走 `/api/tree/commands`,不走 `/api/media/batch`。
|
||||
- 2026-05-15:新增 `scripts/task437-local-folder-asset-trash-lifecycle-smoke.js`,真实浏览器覆盖 local asset Delete 进入本地回收站、restore 回原路径、purge 清理 trash 文件与索引;结果文件 `tmp/task437-local-folder-asset-trash-lifecycle-smoke/result.json` 记录 `navigationEvents=[]`。
|
||||
|
||||
### 8.8 验证命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection -- --nocapture
|
||||
MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3427 node scripts/task437-local-folder-asset-trash-lifecycle-smoke.js
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# 4-28 Trash Restore Location Reveal Focus v1
|
||||
|
||||
> 状态:process
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 6
|
||||
|
||||
## 1. 目标
|
||||
|
||||
页面进入垃圾箱前必须记录可恢复的位置快照;恢复时优先回到删除前父节点与排序位置,并在 UI 中 reveal / focus 恢复对象。若原父节点已不存在或仍在垃圾箱,应显式 fallback 到根目录并提示用户。
|
||||
|
||||
## 2. 已落地范围
|
||||
|
||||
- `documents` schema 新增 `restore_parent_id` 与 `restore_sort_order`。
|
||||
- `documents.softDelete` 对页面子树写入删除前 `parent_id / sort_order` 快照。
|
||||
- `documents.restore`:
|
||||
- 读取同 workspace 当前文档集合。
|
||||
- 父节点仍存在或属于本次级联恢复子树时,恢复到原父节点。
|
||||
- 父节点已不存在或仍在垃圾箱时,恢复到根目录。
|
||||
- 恢复时回写 `parent_id / sort_order`,并对目标父级兄弟节点重新编号,避免恢复到原排序位时产生重复 `sort_order`。
|
||||
- 清空恢复位置快照。
|
||||
- 返回 `restore_location.parent_id / sort_order / fallback_reason`。
|
||||
- `/api/tree/commands restore` 读取页面 meta 时允许包含已删除页面,避免垃圾箱页面在进入 restore mutation 前被 `getMeta` 过滤成 404。
|
||||
- `restoreDocumentCommand` 透传恢复位置结果。
|
||||
- Sidebar 垃圾箱恢复后刷新数据、展开恢复父链、滚动并 focus 恢复行;发生 fallback 时提示“原父页面已不存在,已恢复到根目录”。
|
||||
|
||||
## 3. 设计口径
|
||||
|
||||
- 当前记录的是稳定数据位置:`parent_id / sort_order`。
|
||||
`view path / expanded path` 不持久化到 Convex,恢复后由 Sidebar 根据当前父链展开得到。
|
||||
- 子树恢复只恢复与父节点同一 `deleted_at` 批次的子节点,避免把更早独立删除的子页面误恢复。
|
||||
- fallback 策略先固定为“恢复到根目录 + 明示提示”,不在本阶段引入位置选择弹窗。
|
||||
- `sort_order` 会按当前兄弟数量 clamp;如果原位置越界,则放到可解释的最近位置。
|
||||
|
||||
## 4. 已验证
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts
|
||||
```
|
||||
|
||||
结果:4 个测试文件通过,20 项测试通过。
|
||||
|
||||
补充 route 与排序回归:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm test src/lib/documents/tree-command-client.test.ts src/lib/documents/restore-location.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/sidebar-sync.test.ts src/app/api/tree/commands/route.test.ts
|
||||
```
|
||||
|
||||
结果:5 个测试文件通过,32 项测试通过。
|
||||
|
||||
真实浏览器 smoke:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping
|
||||
|
||||
cd /mnt/Data1T/mnote
|
||||
node scripts/task429-trash-restore-location-reveal-smoke.js
|
||||
```
|
||||
|
||||
结果:通过。脚本使用一次性用户 / workspace,在 3000 主入口验证:
|
||||
|
||||
- 删除子页面后通过 `tree.node.restore` 恢复,返回 `restore_location.parent_id` 为原父页面、`sort_order` 为原排序位。
|
||||
- 原排序位已被兄弟页面占用时,兄弟页面被后移,Convex 中不产生重复 `sort_order`。
|
||||
- 当前浏览器页面不刷新,恢复后的页面行重新出现在侧边栏树中。
|
||||
- 构造 `restore_parent_id` 指向缺失父节点的历史数据,恢复后返回 `fallback_reason: parent_missing_or_deleted`,并落到根目录。
|
||||
|
||||
局部 TypeScript 筛查:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/wolai-frontend
|
||||
pnpm exec tsc --noEmit --pretty false 2>&1 | rg "convex/documents.ts\\((160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175)"
|
||||
```
|
||||
|
||||
结果:阶段 6 修改附近无新增 TypeScript 报错。全量 `tsc` 仍受仓库既有错误影响,未作为本阶段完成条件。
|
||||
|
||||
## 5. 待补
|
||||
|
||||
- React Sidebar Drawer 的恢复后 DOM focus 仍需在 React 入口可稳定渲染页面树时补浏览器证据;当前 3000 主壳 smoke 已覆盖“恢复后无需刷新可见 / reveal”,fallback 提示仍由 React Drawer 代码路径负责。
|
||||
- 双浏览器 no-refresh:A 恢复页面后,B 不刷新即可看到位置变化;该项归入阶段 8 的实时刷新矩阵。
|
||||
@@ -0,0 +1,122 @@
|
||||
# 4-34 Filetree Trash Dual Browser No Refresh v1
|
||||
|
||||
> 状态:process
|
||||
> 创建时间:2026-05-15
|
||||
> 所属主线:04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
|
||||
> 来源:`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 8
|
||||
|
||||
## 1. 目标
|
||||
|
||||
补齐文件树与垃圾箱的双浏览器 no-refresh 回归:A 浏览器执行页面、附件、mindmap、table 的 create / archive / restore / purge / empty-trash,B 浏览器不刷新即可在 File Tree 与 Trash 工作台看到变化。
|
||||
|
||||
阶段 8 不以“单浏览器本地 optimistic update 成功”作为完成标准;`refreshTree()` 只能作为 A 浏览器本地兜底,不能作为 B 浏览器同步证据。
|
||||
|
||||
## 2. 验收矩阵
|
||||
|
||||
| 对象 | create | archive | restore | purge | empty-trash | 证据要求 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 页面 document | A 新建后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空页面垃圾箱后 B Trash 清空 | 记录 tree delta/resync 或 Convex live 来源 |
|
||||
| 普通附件 file asset | A 创建/上传或 seed 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 `tree.resource.*` / compat alias 与实时来源 |
|
||||
| mindmap | A 新建 mindmap 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 mindmap route 仍为 compat alias |
|
||||
| table | A 新建/seed table 后 B File Tree 可见 | A 删除后 B File Tree 消失、Trash 可见 | A 恢复后 B File Tree 可见、Trash 消失 | A 彻底删除后 B Trash 消失 | A 清空资源垃圾箱后 B Trash 清空 | 记录 table route 仍为 compat alias |
|
||||
| local folder Markdown | 外部创建 `.md` 后 page/file tree 可见 | 本地删除进入 `.mnote/trash` | restore 回原路径 | purge 后原文件与 trash 均不存在 | 可选本地 trash 清空 | 必须记录是否发生 reload,不能把 reload 型刷新标为 no-refresh |
|
||||
| local folder 非 md 资源 | 外部创建 `.txt/.png/.pdf/.xlsx` 后 filetree 可见 | 删除语义待定 | restore 待定 | purge 待定 | 待定 | 当前不走 Markdown watcher SSE;需记录 polling / reload / delta 来源 |
|
||||
|
||||
## 3. 分批执行
|
||||
|
||||
### 3.1 task432:页面生命周期双浏览器
|
||||
|
||||
先覆盖页面 document,因为它是 `tree.node.*` 主链:
|
||||
|
||||
- 使用测试账号或一次性用户登录两个 browser context。
|
||||
- 使用隔离 workspace / 本轮测试前缀创建父页面与子页面。
|
||||
- B 打开 3000 主入口文档页与 `/trash?workspaceId=...` 两个 tab,安装 tree event recorder。
|
||||
- A 执行 create / archive / restore / purge / empty-trash。
|
||||
- B 每一步不刷新页面,等待 File Tree / Trash DOM 更新,并记录是 `tree:delta`、`tree:resync`、EventSource snapshot 还是 Convex live 更新。
|
||||
|
||||
通过后才勾选阶段 8 的页面子项。
|
||||
|
||||
执行记录:
|
||||
|
||||
- 2026-05-15:`task432-filetree-trash-page-dual-browser-no-refresh-smoke` 已通过。A 浏览器对页面执行 create / archive / restore / purge / empty-trash;B 浏览器文件树与 `/trash` 均无需刷新可见更新,且全程无 `framenavigated`。事件来源记录为:create -> `tree:resync`,archive -> `tree:delta remove_document`,restore -> `tree:delta upsert_document`,purge -> `tree:resync`,empty-trash -> `tree:resync`。结果文件:`tmp/task432-filetree-trash-page-dual-browser-no-refresh-smoke/result.json`。
|
||||
- 2026-05-15:清空页面垃圾箱的初次 smoke 暴露两层根因并已修正:Rust `documents.emptyTrashByWorkspace` 之前未写 `tree.trash.documents.emptied` domain event,Convex `documents.emptyTrashByWorkspace` validator 也未放行 `streamDeltaHint/domainEventHint/domainEventPlan`,导致 `/trash` 必须刷新才清空。两处已补齐并重新部署本地 Convex。
|
||||
|
||||
### 3.2 task433:普通附件生命周期双浏览器
|
||||
|
||||
覆盖普通 file asset:
|
||||
|
||||
- 优先复用 `task428` / `task427` 的 API seed 方式,避免依赖上传文件选择器。
|
||||
- archive / restore / purge 走 `/api/media/batch` 与 `/api/media/purge` 兼容 URL,但必须能在 artifact / response 中证明进入 `tree.resource.archive/restore/purge`。
|
||||
- B 侧分别观察 File Tree 与 Trash 工作台。
|
||||
|
||||
执行记录:
|
||||
|
||||
- 2026-05-15:`task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke` 已通过。A 浏览器对普通附件执行 upload / archive / restore / archive+purge / upload+archive+empty-trash;B 浏览器文件树与 `/trash` 均无需刷新可见更新,执行阶段无 `framenavigated`。事件来源记录为:upload -> `tree:delta upsert_assets`,archive -> `tree:resync`,restore -> `tree:resync`,purge -> `tree:resync`,empty-trash -> `tree:resync`。结果文件:`tmp/task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke/result.json`。
|
||||
- 2026-05-15:初次 smoke 暴露 `tree.resource.archive/restore/purge` legacy Convex 参数转换覆盖真实 Convex userId,导致 `mediaAssets.patchById` membership 检查失败;已修正 `mnote-web` transport,使资源 lifecycle 转换优先保留 `args_json.userId`。随后暴露 `/api/media/empty-trash` 不写 tree event,导致 B `/trash` 清空必须刷新;已补 `tree.trash.media.emptied` domain event 与 `resync_required` stream delta。
|
||||
- 2026-05-15:验证 `cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_resource_lifecycle_args_keep_effective_user_id -- --nocapture`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web media_trash_routes_delete_restore_purge_and_empty -- --nocapture` 均通过。
|
||||
|
||||
### 3.3 task434:mindmap / table 生命周期双浏览器
|
||||
|
||||
覆盖仍处 compat alias 的 mindmap / table:
|
||||
|
||||
- mindmap 可复用现有 mindmap block / asset seed 方式。
|
||||
- table 可复用 `task427` 的 table seed 与 purge query 方式。
|
||||
- 本阶段不把 mindmap / table 宣称为正式 `tree.resource.*` cutover,只验证双浏览器可见性并保留 compat 边界。
|
||||
|
||||
执行记录:
|
||||
|
||||
- 2026-05-15:`task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke` 已通过。A 浏览器对 mindmap 执行 create / archive / restore / archive+purge,对 table 执行 create / archive / restore / archive+purge,并对 mindmap + table 执行 create / archive / empty-trash;B 浏览器文件树与 `/trash` 均无需刷新可见更新,执行阶段无 `framenavigated`。结果文件:`tmp/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke/result.json`。
|
||||
- 2026-05-15:为避免把 table 预置数据误标为 create 实时证据,Rust `mnote-web` 已新增 `/api/tables/create` 兼容入口,创建成功后记录 `tree.resource.table.created` domain event 与 `resync_required` stream delta。mindmap / table 的 delete / restore / purge / empty-trash compat route 也补齐 tree resync artifacts。
|
||||
- 2026-05-15:事件来源记录为:mindmap create / archive / restore 主要触发 `tree:delta resync_required`,mindmap purge 触发 `tree:resync`;table create / archive / restore 主要触发 `tree:delta resync_required`,table purge / empty-trash 触发 `tree:resync`。
|
||||
- 2026-05-15:验证 `cargo test --manifest-path rust/Cargo.toml -p mnote-web trash_routes -- --nocapture --test-threads=1` 与 `node --check scripts/task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` 均通过。
|
||||
|
||||
### 3.4 task435:empty-trash 双浏览器汇总
|
||||
|
||||
在一次性 workspace 中执行页面垃圾箱与资源垃圾箱清空:
|
||||
|
||||
- A 清空页面垃圾箱后,B `/trash` 页面不刷新变为空。
|
||||
- A 清空资源垃圾箱后,B `/trash` 页面资源区不刷新变为空。
|
||||
- Convex query 断言对应对象不可再查询。
|
||||
|
||||
### 3.5 task436:local folder watcher 与本地资源生命周期
|
||||
|
||||
本阶段不混入 Convex 双浏览器矩阵,单独验证 local folder source:
|
||||
|
||||
- 打开 local folder page tree 与 filetree,记录初始 `navigationEvents`。
|
||||
- 外部创建 `.md`,断言 page tree / filetree 可见,并记录是否由 `/api/local-folder/events`、`/api/tree/local-folder-watch`、reload 或手动刷新驱动。
|
||||
- 外部重命名 `.md`,断言旧 row 消失、新 row 出现;当前打开文档若受影响,应出现刷新或冲突提示。
|
||||
- 外部删除 `.md`,断言 page tree / filetree 中 row 消失;若当前打开该文档,应有明确状态提示。
|
||||
- 外部创建 / 重命名 / 删除至少两类非 md 文件,例如 `.txt` 与 `.png`,断言 filetree 与真实文件系统一致,并记录是否发生 `window.location.reload()`。
|
||||
- 对本地非 md 资源执行删除 / restore / purge;若 UI 仍禁用,记录为未完成能力,不得用 Markdown trash 代替。
|
||||
|
||||
当前只读审计结论:
|
||||
|
||||
- `LocalFolderWatcherRegistry` 使用 `notify` 递归监听 root,但事件发送前经过 `is_markdown_path`,只允许 `.md/.markdown`。
|
||||
- `/api/local-folder/events` 订阅该 watcher,主要服务打开的本地 Markdown document session。
|
||||
- 主 tree 的 local folder 模式不接 `/api/tree/events`,而是轮询 `/api/tree/local-folder-watch`;revision 扫描可见条目,非 md 文件变化会改变 revision,但当前变化后触发 `window.location.reload()`。
|
||||
- `task163-local-folder-unified-tree-browser-smoke.js` 已覆盖外部新增 Markdown、外部新增 / 删除 `watcher-asset.txt` 的可见性;尚未覆盖外部 Markdown 重命名 / 删除、非 md rename、非 md trash / restore / purge。
|
||||
|
||||
通过后才允许在 10-review 中把 local folder 阶段 8 子项勾选;如果仍发生 reload,只能标为“reload 型刷新可见”,不能标为 no-refresh。
|
||||
|
||||
## 4. 实时来源记录
|
||||
|
||||
每个 smoke 结果必须记录:
|
||||
|
||||
- B 浏览器是否接到 `/api/tree/events`。
|
||||
- B 浏览器是否接到 `tree:snapshot`、`tree:delta`、`tree:resync`。
|
||||
- B 侧 DOM 更新是否发生在无 reload / 无 framenavigated 的前提下。
|
||||
- 是否依赖了 A 浏览器本地 `refreshTree()`;如果依赖,只能作为 A 本地补偿,不计入 B 实时证据。
|
||||
|
||||
## 5. 当前边界
|
||||
|
||||
- 3000 常驻进程可能是旧 `mnote-web` 二进制;修改 Rust 后做 smoke 时必须重启 3000,或用新编译的临时 3001 并在结果中明确记录。
|
||||
- 资源生命周期中,普通附件 file asset 已进入正式 `tree.resource.*`;mindmap / table 仍是 compat route,阶段 8 只验证 no-refresh,不改变 cutover 口径。
|
||||
- local folder Markdown watcher 只有部分历史覆盖,且主 tree filetree 当前是 revision polling + reload;非 md 本地资源文件 watcher / trash / restore / purge 仍单列为待补,不应混入 Convex 双浏览器矩阵。
|
||||
- `resync_required` delta 不是数据替换本身,必须伴随后续 snapshot / resync 或客户端主动拉取 snapshot;后续新增资源 route 时必须补事件合同回归。
|
||||
|
||||
## 6. 完成标准
|
||||
|
||||
- `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 8 的 Convex 页面、普通附件、mindmap、table checklist 全部有对应 smoke 证据。
|
||||
- local folder 子项必须有 task436 或等价 smoke 证据;若仍发生 reload,保持未完成 no-refresh 状态。
|
||||
- 每个 smoke 结果写入 `tmp/task43*-*/result.json`,并在 10-review 中记录命令、对象、实时来源和剩余边界。
|
||||
- 不再用单浏览器 no-refresh、API purge 成功或 Convex query 成功替代双浏览器 UI no-refresh。
|
||||
@@ -0,0 +1,235 @@
|
||||
# 4-35 [process] Convex File Tree 标题即 Markdown 文件名体系切换 v1
|
||||
|
||||
> 更新时间:2026-05-15
|
||||
>
|
||||
> 背景:
|
||||
> - 用户反馈本地文件夹中新建页面会先显示 `/mnt/Data1T/mnote/tmp/image copy 98.png`,再转回 `新页面.md`。
|
||||
> - 用户判断这是在线文件夹与本地文件夹的冲突,后续希望“统一而不是继续局部修改”。
|
||||
> - 目标倾向:不再采用 Convex `page -> index.md` 体系,而是采用直接 `新页面.md` 体系,文件名与标题相同;在线文件夹尽量向本地文件夹靠拢。
|
||||
|
||||
## 1. 决策问题
|
||||
|
||||
当前 `convex_workspace` 的 File Tree 长期以复合资源树表达页面:
|
||||
|
||||
```text
|
||||
page/document
|
||||
index.md
|
||||
mindmap asset
|
||||
attachment asset
|
||||
```
|
||||
|
||||
`local_folder` 则天然以真实文件系统表达:
|
||||
|
||||
```text
|
||||
新页面.md
|
||||
图片.png
|
||||
子目录/
|
||||
```
|
||||
|
||||
这两种形态同时存在时,UI、active row、rename、新建、外部 watcher、projection refresh 容易出现不一致。用户观察到的“先显示 tmp 图片路径,再转回新页面.md”就是这类冲突的症状之一:同一套 File Tree UI 里混入了本地路径资源、Convex 页面容器、`index.md` 伪文件和异步投影校准。
|
||||
|
||||
本设计稿用于冻结新的方向:后续 Convex File Tree 应逐步向本地文件夹模型靠拢,把页面在文件树中的默认表现改为直接的 Markdown 文件行,例如 `新页面.md`,而不是页面容器下的 `index.md`。
|
||||
|
||||
## 2. 建议结论
|
||||
|
||||
建议新建一个设计文档,而不是只建 bug 修补。
|
||||
|
||||
原因:
|
||||
|
||||
1. 这会改变 `file_tree` projection 合同,不只是修一个显示名称。
|
||||
2. 这会影响 `ObjectIdentity`、active row、rename/title sync、resource parent、trash、restore、search、open intent 和 smoke 期望。
|
||||
3. 现有 `4-24` / `5-12` 已把 `index.md` 作为 Page Aggregate body 的 object identity 收口到 done;如果要废弃或弱化该 UI 体系,必须有新的迁移设计覆盖旧口径,不能在代码里零散改。
|
||||
4. Convex 存储层仍然是 document record,不等于必须在 File Tree UI 中展示 `index.md` 伪文件。可以先改 projection / UI 语义,再决定是否迁移底层存储。
|
||||
|
||||
## 3. 范围
|
||||
|
||||
本设计只讨论 `convex_workspace` 在 File Tree 中的页面呈现与命令语义,不改变 `local_folder` 已有真实文件系统规则。
|
||||
|
||||
纳入范围:
|
||||
|
||||
- Convex 页面在 File Tree 中显示为 `{title}.md`。
|
||||
- 新建页面时直接出现 `新页面.md`,不先出现 `page/index.md` 或临时资源路径。
|
||||
- 重命名页面时,标题与文件名同源:`新标题` 对应 `新标题.md`。
|
||||
- 点击 `{title}.md` 打开 Page Aggregate body。
|
||||
- mindmap、附件、OnlyOffice、代码附件继续作为同一页面下的资源对象,但不能再依赖可见的 `index.md` 子行作为锚点。
|
||||
- Active / reveal 以 `{title}.md` row 或明确 asset row 为准。
|
||||
|
||||
暂不纳入范围:
|
||||
|
||||
- 立即迁移 Convex 底层 documents 表结构。
|
||||
- 自动双向同步 local folder 与 Convex。
|
||||
- Markdown 文件名与标题的所有非法字符、大小写、重名冲突最终规则;本稿先冻结方向,具体规则后续 checklist 补齐。
|
||||
|
||||
## 4. 新旧模型对比
|
||||
|
||||
旧模型:
|
||||
|
||||
```text
|
||||
新页面
|
||||
index.md
|
||||
mindmap-mindmap_xxx.json
|
||||
image.png
|
||||
```
|
||||
|
||||
新目标模型:
|
||||
|
||||
```text
|
||||
新页面.md
|
||||
mindmap-mindmap_xxx.json
|
||||
image.png
|
||||
```
|
||||
|
||||
解释:
|
||||
|
||||
- `新页面.md` 是页面正文对象的 File Tree row。
|
||||
- 该 row 的 object identity 可继续内部映射到 Page Aggregate body,但 UI 不再展示 `index.md`。
|
||||
- 资源行仍然挂在页面正文对象下面,表达“属于该页面/正文 block 关联资源”。
|
||||
- Page Tree 仍可显示 `新页面`,不带 `.md`,作为页面导航 projection。
|
||||
|
||||
## 5. 关键合同变更
|
||||
|
||||
### 5.1 File Tree row
|
||||
|
||||
Convex 页面正文 row:
|
||||
|
||||
- `rowKind=markdown_page` 或沿用 `index` 但对 UI 隐藏 `index.md` 语义,需要后续冻结。
|
||||
- `title="{pageTitle}.md"`。
|
||||
- `objectIdentity="page:markdown:{documentId}"` 或兼容映射到既有 `page:index:{documentId}`,但对外不再暴露为 `index.md`。
|
||||
- `documentId` 仍是 Convex document id。
|
||||
- `resourceMeta.sourceKind="convex_workspace"`。
|
||||
|
||||
本地 `.md` row:
|
||||
|
||||
- 保持真实文件名。
|
||||
- `objectIdentity` 与本地 page identity 绑定,不以裸路径作为长期唯一 id。
|
||||
|
||||
### 5.2 Rename / title sync
|
||||
|
||||
Convex workspace 中:
|
||||
|
||||
- 文件树重命名 `新标题.md` 应写入页面标题 `新标题`。
|
||||
- 页面标题编辑应能同步更新 File Tree 行显示为 `新标题.md`。
|
||||
- `.md` 扩展名是 projection/UI 层文件语义,不应直接写进页面标题。
|
||||
- 重名冲突必须返回结构化 preflight,不允许静默覆盖或临时跳成其它资源路径。
|
||||
|
||||
Local folder 中:
|
||||
|
||||
- 继续以真实文件名为主,标题优先级仍按现有本地 Markdown 规则处理。
|
||||
|
||||
### 5.3 Create
|
||||
|
||||
Convex workspace 新建页面:
|
||||
|
||||
- 命令结果中应直接携带最终 File Tree row identity 与 `title="新页面.md"`。
|
||||
- UI 乐观插入时不得先显示本地 tmp 路径、上传文件名或 `index.md` fallback。
|
||||
- 如果后端 title 去重为 `新页面 2`,前端最终只校准成 `新页面 2.md`,中间不出现无关资源名。
|
||||
|
||||
### 5.4 Resource parent
|
||||
|
||||
mindmap / attachment 等资源仍挂在页面正文对象下,但 parent row 从旧 `index:{documentId}` 或 page container 迁移到新的 markdown page row。
|
||||
|
||||
需要特别验证:
|
||||
|
||||
- 点击 mindmap asset 后 active row 不跳回 `{title}.md`,除非确实打开的是页面正文。
|
||||
- 删除 `{title}.md` 进入页面垃圾箱;删除资源进入资源垃圾箱。
|
||||
- purge 页面时 Convex document 与其资源关系按正式命令同步清理或标记孤儿处理。
|
||||
|
||||
## 6. 与既有设计的覆盖关系
|
||||
|
||||
本稿不否定 `4-24` / `5-12` 的单一真源目标,只调整 File Tree 的可见模型:
|
||||
|
||||
- 仍然保留 Page Aggregate body 是页面正文真源。
|
||||
- 仍然保留 mindmap / attachment / OnlyOffice 是独立 resource object。
|
||||
- 仍然要求 object identity 隔离,避免 mindmap 污染页面正文。
|
||||
- 变化是:Convex File Tree 不再必须通过可见 `index.md` 行表达页面正文对象。
|
||||
|
||||
后续如执行本设计,必须同步更新以下旧口径:
|
||||
|
||||
- `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` 中“页面节点下固定派生 index.md row”的可见 UI 表达。
|
||||
- `design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md` 中 `index.md tab` 的命名口径,可迁移为 `markdown page tab` 或 `{title}.md tab`。
|
||||
- `design/10-review/done/05-tree.md` 作为历史记录不直接改写,但 README 或新设计需说明其 `index.md` 口径已被本稿覆盖。
|
||||
|
||||
## 7. 建议实施阶段
|
||||
|
||||
### P0:只改 Convex File Tree projection 的页面正文显示
|
||||
|
||||
- 新建页面后 File Tree 直接出现 `{title}.md`。
|
||||
- 点击 `{title}.md` 打开 Page Aggregate body。
|
||||
- Page Tree 仍显示无扩展名标题。
|
||||
- 保持底层 object identity 兼容,减少一次性改动。
|
||||
|
||||
### P1:Active / reveal / object identity 迁移
|
||||
|
||||
- active page body 映射到 `{title}.md` row。
|
||||
- mindmap asset 打开后保持 asset active,不降级为页面正文 row。
|
||||
- 长列表 reveal 稳定。
|
||||
|
||||
### P2:Rename / create / conflict preflight
|
||||
|
||||
- 文件树 rename `{title}.md` 与页面标题同步。
|
||||
- 新建同名页面明确生成 `新页面 2.md` 或返回冲突选择。
|
||||
- 不再出现临时 tmp 路径或资源文件名作为页面 row title。
|
||||
|
||||
### P3:旧 index.md UI 退场
|
||||
|
||||
- 默认 Convex File Tree 不显示 `index.md`。
|
||||
- 如仍需 debug,可通过显式 debug/projection inspector 展示旧 identity。
|
||||
- 更新 smoke 和设计口径,避免后续继续围绕 `index.md` 修补 UI。
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
- 在 Convex workspace 新建页面后,File Tree 首次可见 row 就是 `新页面.md` 或去重后的 `{title}.md`,不得短暂显示 `/tmp/...`、图片文件名、`index.md` 或其它 fallback。
|
||||
- 点击 `{title}.md` 打开页面正文;点击 mindmap asset 打开 mindmap object editor,二者 active row 不互相覆盖。
|
||||
- 页面标题编辑与 File Tree rename 最终保持一致:标题 `ABC` 对应 File Tree `ABC.md`。
|
||||
- 同名冲突、非法字符、扩展名输入等情况有结构化 preflight 和可预测结果。
|
||||
- Local folder 与 Convex workspace 共用同一 File Tree UI 组件、selection/focus/reveal/context menu/keyboard 行为;差异只来自 source capability 与 executor。
|
||||
- 真实浏览器 smoke 覆盖:新建页面、重命名、点击页面正文、点击 mindmap、滚动下半部分点击、删除进垃圾箱、restore 后 reveal。
|
||||
|
||||
## 9. 当前状态
|
||||
|
||||
当前状态:`process`
|
||||
|
||||
2026-05-15 已完成 P0/P1/P2 的最小切片:
|
||||
|
||||
- `bridge-runtime` 的 Convex `file_tree` projection 不再为页面正文派生可见 `index.md` row。
|
||||
- Convex 页面正文 row 继续使用 `rowKind=document` / `rowId=doc:<documentId>`,但标题改为 `{pageTitle}.md`。
|
||||
- mindmap / attachment / table 等资源继续挂在 `doc:<documentId>` 下。
|
||||
- 3000 主文档壳本地 create apply 同步改为只插入 `doc:<documentId>`,标题显示为 `{title}.md`,不再插入 `index:<documentId>`。
|
||||
- File Tree 当前页面 selected / focused 默认改为 `doc:<documentId>`。
|
||||
- `task169` 的“回到页面正文”验证从点击 `index:<documentId>` 改为点击 `doc:<documentId>`,object identity 仍为 `page`。
|
||||
- File Tree inline rename 对页面正文 row 输入 `{title}.md` 时,提交给 `tree.node.rename` 前会剥离 `.md`,页面标题保持 `{title}`,File Tree 显示保持 `{title}.md`。
|
||||
- File Tree inline rename 已加入主壳内校验:空名、非法文件名字符、同级同名页面会停留在输入框内并显示结构化提示,不提交 rename command。
|
||||
|
||||
已通过验证:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime file_tree_projection -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web file_tree_projection -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell_filetree_renderer_outputs_initial_nested_html_contract -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell -- --nocapture
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task426-mnote-web-main-no-reload-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task438-filetree-title-md-active-reveal-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task439-filetree-title-md-rename-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task440-page-title-filetree-md-sync-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task169-mindmap-realtime-smoke.js
|
||||
```
|
||||
|
||||
真实浏览器结果文件:
|
||||
|
||||
- `tmp/task426-mnote-web-main-no-reload-smoke/result.json`
|
||||
- `tmp/task438-filetree-title-md-active-reveal-smoke/result.json`
|
||||
- `tmp/task439-filetree-title-md-rename-smoke/result.json`
|
||||
- `tmp/task440-page-title-filetree-md-sync-smoke/result.json`
|
||||
- `tmp/task169-mindmap-realtime-smoke/result.json`
|
||||
|
||||
本轮 P3 收口:
|
||||
|
||||
- 页面标题编辑反向同步到 File Tree `{title}.md` 已由真实浏览器 smoke `scripts/task440-page-title-filetree-md-sync-smoke.js` 覆盖:标题栏输入 `{title}` 后,File Tree 显示 `{title}.md`,Page Tree 与标题输入框保持 `{title}`,且不出现 `index:<documentId>` 行。
|
||||
- `tree_shell/filetree_renderer.rs` 的默认 nested fixture 已从可见 `index.md` 子行改为页面 markdown row 下挂 mindmap asset;`rowKind=index` 仍只作为历史 projection / 命令兼容解析保留,不再作为默认 Convex File Tree UI 口径。
|
||||
- 旧设计中的“可见 `index.md`”表述由本稿覆盖:后续 UI / smoke / review 应以 `doc:<documentId>` + `{title}.md` 作为 Convex 页面正文 row;如果文档讨论历史 `index.md`,必须明确它是兼容身份或历史记录,不是默认可见行。
|
||||
|
||||
仍保留的后续增强:
|
||||
|
||||
- 删除进垃圾箱、restore 后 reveal、双浏览器 resync、搜索过滤下 reveal 等更宽场景继续由 `4-28`、`4-34` 与后续 tree domain checklist 覆盖。
|
||||
Reference in New Issue
Block a user