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:
lix-2026
2026-05-16 07:38:45 +08:00
parent 274779c0d8
commit 384da4e44c
85 changed files with 12570 additions and 394 deletions
@@ -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 plancut 粘贴复用 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 的行内 validationRust 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` 验证自拖自身与父拖子均返回 400copy 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`