fix: 修复本地文件夹垃圾箱恢复聚焦

This commit is contained in:
lix-2026
2026-05-21 14:39:38 +08:00
parent b0fa1e53d8
commit 83ad32dd67
17 changed files with 680 additions and 39 deletions
@@ -0,0 +1,111 @@
# 4-28 Trash Restore Location Reveal Focus v1
> 状态:done
> 创建时间: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 时提示“原父页面已不存在,已恢复到根目录”。
- local folder 非 md 资源 restore 通过 `mnote.pendingLocalFolderRestoreFiletreeRowId` 记录恢复目标,filetree 在加载与轮询刷新时消费该 pending row-id 并聚焦恢复行。
## 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. 2026-05-21 local folder restore focus 收口
### 5.1 Convex 页面(已有部分证据)
- React Sidebar Drawer 的恢复后 DOM focus 仍需在 React 入口可稳定渲染页面树时补浏览器证据;当前 3000 主壳 smoke(`task429`)已覆盖"恢复后无需刷新可见 / reveal"fallback 提示仍由 React Drawer 代码路径负责。
- 双浏览器 no-refresh:A 恢复页面后,B 不刷新即可看到位置变化;该项归入阶段 8 的实时刷新矩阵。
- 已知源码问题:`selectSidebarFileTreeDocument` 存在且工作(create / navigate 路径调用),restore 路径仍未直接调用,但 local folder 已通过 pending row-id 消费实现恢复行 focus。
### 5.2 Local folder 非 md 资源 restore focus(已完成,Worker C 2026-05-21/22 复核)
当前状态:**已完成**。实现通过 pending row-id 传递与 filetree 轮询消费完成,审计证据及源码对照如下:
| 检查项 | 状态 | 证据来源 |
|--------|------|----------|
| `selectSidebarFileTreeDocument` 是否从 restore 路径直接调用 | ❌ 否 | 采用 `pendingLocalFolderRestoreFiletreeRowId` + filetree 轮询消费,而不是 restore handler 直调 |
| restore 后 filetree 行是否有 `data-active` / `data-selected` | ✅ 有 | `task473` 通过结果:`data-active="true"` |
| restore 后是否 `scrollIntoView``focus()` | ✅ 有 | `applyPendingLocalFolderRestoreFocusOnce` 选中并滚动恢复行 |
| trash workbench 的 restore 后 filetree 同步机制 | 轮询 1200ms | `layout.rs:1991-2004` (`startLocalFolderSidebarWatch`) |
| trash workbench 的 restore 后是否调用 `refreshLocalFolderSidebarSnapshot` | ❌ 否,只调 `refresh()`(自身 workbench | `gateway.rs:1064-1065`,但 filetree 轮询可消费 pending focus |
| `selectSidebarFileTreeDocument` 是否支持 `local-file:` row-id | ❌ 否 | 目前 focus 由 `selectSidebarFileTreeRowById``local:asset:` row-id 完成 |
### 5.3 已采用方案(供后续复核)
1. **短路径**trash workbench inline script 的 restore `.then()` 中写入 `mnote.pendingLocalFolderRestoreFiletreeRowId`
2. **filetree 消费**`refreshLocalFolderSidebarSnapshot()` 与初始 render 共享 `schedulePendingLocalFolderRestoreFocus()`,在 filetree 加载/轮询后聚焦恢复行。
3. **smoke 回归**`task473` 已从已知缺口回归改为可检测正向 smoke,最终应观察到 `data-active="true"` 或其他 focus 证据出现。
### 5.4 2026-05-21/22 浏览器复核
- Codex 独立复跑 `node --check scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3012 node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js``ok:true``restoreNavFree.delta=0``fileReappearsInFiletree=true``focusAfterRestore.attrs.data-active="true"`
- Reasonix Browser Worker 输出目录:`tmp/reasonix-batch-a-browser-task473-2026-05-21/`,初始结论为 `PASS_WITH_KNOWN_GAP`;本轮修复后,Codex 本地 smoke 已验证 gap 关闭。
- 复核结论:local folder 非 md 资源 restore 后 no-refresh、行恢复与 focus/reveal 均已完成,可归档到 `done/`
@@ -0,0 +1,58 @@
# 4-44 FileTree Action Layer Paste / Drop / Delete Checklist v1
> 状态:done
>
> 日期:2026-05-20
>
> OwnerFile Tree actions / preflight / smoke semantics
## 1. 目标
补齐 P1delete、paste、right-click paste、external drop 的动作层仍分散,缺少 dirty / readonly / undo / fallback 的统一动作合同。目标是先把现有行为收口成可测试的轻量 action layer,不一次性照搬 VSCode `IBulkEditService`
## 2. 允许修改范围
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- `rust/crates/mnote-web/src/routes/tree.rs`
- `rust/crates/mnote-web/src/tree_shell/*` 中仅限 preflight / action contract
- `scripts/task430-vscode-explorer-stage7-smoke.js`
- `scripts/task471-local-folder-bulk-resource-trash-smoke.js`
- 可新增聚焦 smoke`scripts/task47*-filetree-action-layer-smoke.js`
## 3. 禁止事项
- 不删除旧 media / mindmap / table fallback route。
- 不把默认删除改成永久删除。
- 不绕过 readonly preflight。
- 不破坏多选 `data-selected` 语义。
## 4. Checklist
- [x] 提炼前端 File Tree action helper,统一记录 action、target、preflight、fallback、失败项。
- [x] delete 对 local folder 资源统一走 archive tree command,并暴露可观测 undo/fallback 占位状态。
- [x] right-click paste 使用当前菜单目标,而不是只能依赖 focused row。
- [x] paste / drop 对 readonly 目标有一致阻断提示和 DOM 标记。
- [x] external drop 二进制文件继续进入上传链路,失败时不清空 selection。
- [x] smoke 增加至少一个语义断言:目标 row、action 名、fallback/readonly/undo 状态之一。
## 5. 验收
- [x] `cargo test -p mnote-web layout -- --test-threads=1`
- [x] `cargo test -p mnote-web tree_command_local_folder -- --test-threads=1`
- [x] `node scripts/task430-vscode-explorer-stage7-smoke.js` — 2026-05-21 实跑通过,结论 `PASS_WITH_KNOWN_GAPS`workspace bootstrap、F2 rename、cut/paste move、DnD preflight guard 通过;FileTree `data-active`、Open Editors、`Paste Into` 菜单与 readonly/重名冲突矩阵仍作为后续缺口记录。
- [x] `node scripts/task471-local-folder-bulk-resource-trash-smoke.js` — 2026-05-21 实跑通过,全部断言成功:多选两个资源行、确认文案含附件数、双 archive command、trash index 含两项、DOM 记录 `bulk-delete`/`archived`/触发 row。
- [ ] 如新增 smoke`node scripts/task47*-filetree-action-layer-smoke.js`
## 5.1 本轮执行记录
- 已合入:`recordFileTreeAction` / `recordFileTreeActionStatus`,为 delete、paste、open-right、new-window 等现有路径暴露稳定 DOM 状态。
- 已合入:右键 `paste-into` 使用菜单触发 row,不再只能靠 focused row。
- 本轮追加:`layout.rs` 为 filetree projection row 暴露 `data-capabilities`,新增 `ensureFileTreeWritableTarget` / `blockReadonlyFileTreeAction`paste 与内部 drop 在执行前复用 `/api/tree/filetree/drop-preflight`readonly 目标统一写入 `data-mnote-filetree-last-action-status=blocked``data-mnote-filetree-readonly-blocked``data-mnote-filetree-readonly-message` 与目标 row 标记,并沿用 `alert` fallback 提示。
- 本轮追加:`task471-local-folder-bulk-resource-trash-smoke.js` 增加语义断言,覆盖 bulk-delete 的 action 名、触发 row 与 `archived` undo 状态;未修改 `task430` 的隔离 workspaceId 前置条件,避免把环境准备失败误判为业务回归。
- 2026-05-21 Reasonix Worker B 实跑:`node scripts/task471-local-folder-bulk-resource-trash-smoke.js` 通过,所有断言成功,无业务语义改动;更新验收 checklist 标记为 `[x]`
- 2026-05-21 Codex 与 Reasonix Browser Worker 复核:`task430` / `task471` 均可在当前 3000 环境实跑。`task430` 的 Open Editors 与 active row 只记录现状,不作为本阶段业务失败;`task471` 的多选资源删除链路有截图与 JSON 证据。
## 6. 非目标
- 不实现完整 undo stack。
- 不实现 cloud object storage drop executor。
@@ -0,0 +1,59 @@
# 4-45 FileTree / Open Editors Lifecycle Link Checklist v1
> 状态:done
>
> 日期:2026-05-21
>
> Ownerfiletree action smoke / resource lifecycle observability
## 1. 目标
参考 Sidex Explorer + Open Editors 联动,补 MNote filetree action 与 open editor state 的可观测测试。当前优先修 smoke 缺口和文档状态,不直接改核心业务。
## 2. 允许修改范围
- `scripts/task430-vscode-explorer-stage7-smoke.js`
- `scripts/task471-local-folder-bulk-resource-trash-smoke.js`
- `design/04-tree-domain/process/4-44-filetree-action-layer-paste-drop-delete-v1.md`
- 如发现 checklist 已全部完成,可建议移动到 `done/`,但不要直接移动未验证文件。
## 3. 禁止事项
- 不修改 `layout.rs` / `web_shell.rs` / `local_folder_source.rs`
- 不改变 delete 默认到垃圾箱的语义。
- 不把环境准备失败伪装成通过。
## 4. Checklist
- [x] 跑或修复 `task430` / `task471` 的可复现前置条件。
- [x] 明确哪些 smoke 已过、哪些因环境阻塞。
- [x] 补充 open editor / filetree active row / lifecycle 状态断言建议。
- [x] 更新 4-44 checklist 执行记录。
## 5. 验收
- `node --check scripts/task430-vscode-explorer-stage7-smoke.js`
- `node --check scripts/task471-local-folder-bulk-resource-trash-smoke.js`
- `node scripts/task430-vscode-explorer-stage7-smoke.js`
- `node scripts/task471-local-folder-bulk-resource-trash-smoke.js`
## 6. 非目标
- 不实现完整 undo stack。
- 不改 cloud storage drop executor。
## 7. 本轮执行记录
- 2026-05-21Reasonix Worker D 识别到 `task430` 的前置问题:新注册用户在 `workspaces:fetchWorkspaceSummaries` 前可能还没有 workspace,需要先通过 `/api/tree/commands` create action 触发 workspace bootstrap。
- 2026-05-21Worker D 还建议将 `task471` 的验证记录同步到 `4-44`,并补 open editor / lifecycle 状态断言。
- 2026-05-21Worker D 进程停留在计划提交阶段,没有生成 `final.md`,也没有实际修改 smoke 或 checklist;本轮不把 4-45 checklist 标记为完成。
- **2026-05-22Worker A 实际修改:**
-`task430` 的 workspace bootstrap 添加 fallback:当 `workspaces:fetchWorkspaceSummaries` 无返回时,通过 `/api/tree/commands` create(不带 workspaceId)触发 Rust 端 `workspaces:ensureDefaultWorkspace` 自举 workspace。
- 添加 `result.phases` 阶段追踪(auth-signin → auth-navigate → workspace-bootstrap),让失败可区分环境阻塞 vs 业务回归。
- 添加 `filetree-active-row` 断言:检查当前打开页对应的 FileTree row 是否有 `[data-active="true"]`
- 添加 `filetree-selected-row` 记录:记录选中行 ID(不做强制断言,因选中语义随点击变化)。
- 添加 `open-editors-section` 记录:确认 MNote 当前无 Open Editors DOM sectionVS Code 对比的已知缺口),只记录不报错。
- 所有新增断言均使用 `runOptional` 包装,环境缺口标记为 skipped 而不阻塞整体结果。
- 2026-05-21Codex 独立复跑 `node --check scripts/task430-vscode-explorer-stage7-smoke.js` / `node scripts/task430-vscode-explorer-stage7-smoke.js`,结果 `ok:true``result.phases``auth-signin``auth-navigate``workspace-bootstrap` 均通过;`f2-inline-rename-doc``f2-inline-rename-asset``cut-paste-move``dnd-preflight-guard` 均通过。
- 2026-05-21Reasonix Browser Worker 复跑 task430,输出目录 `tmp/reasonix-batch-a-browser-task430-2026-05-21/`,结论 `PASS_WITH_KNOWN_GAPS`。已知缺口保留为:FileTree row 未同步 `data-active="true"`、Open Editors section 未实现、右键菜单缺 `Paste Into`、DnD readonly/重名冲突仍需后续端到端矩阵。
- 2026-05-21Codex 独立复跑 `node --check scripts/task471-local-folder-bulk-resource-trash-smoke.js` / `node scripts/task471-local-folder-bulk-resource-trash-smoke.js`,结果 `ok:true`。Reasonix Browser Worker 输出目录 `tmp/reasonix-batch-a-browser-task471-2026-05-21/`,确认两个 local folder 资源行可 Ctrl 多选、确认文案含 `2 个附件`、DOM 状态写入 `bulk-delete` / `archived``.mnote/trash-index.json` 包含两项。