Files
mnote/design/05-editor-mainline/done/5-30-sidebar-starred-shortcuts-sqlite-scoped-explorer-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

244 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 5-30 Sidebar 星标置顶快捷入口与 Scoped Explorer 设计 v1
> 状态:done
> Owner05-editor-mainline / 03-rust-web / control-plane
> 背景:星标置顶需要对齐 Wolai 的 Sidebar 顶部快速访问体验,但 MNote 还需要支持把高频本地文件夹固定成 Explorer scoped root,避免每次进入大 workspace 时加载完整根目录。
## 目标
星标置顶不是文件或文件夹自身的属性,而是用户在 Sidebar 固定的一组快捷入口。
- 当前页面 `.md` 可以加入星标置顶,点击后直接打开该页面。
- FileTree 文件夹右键可以加入星标置顶,点击后切到 Explorer,并把该文件夹作为临时 scoped root 显示。
- 星标置顶按登录用户持久化到 SQLite control-plane;同一用户在不同设备登录时应看到同一组快捷入口。
- 顶栏“全网公开”不能默认误导显示;没有真实公开分享状态时隐藏。
- scoped Explorer 必须避免扫描完整大 workspace,例如 `/mnt/Data1T/mnote` 下只打开 `design/` 时只加载 `design/` 子树。
## 非目标
- 不把星标写入 Markdown frontmatter。
- 不写入 `.mnote/starred-pins.json` 作为长期真相。
- 不把文件夹变成 Resource Tree 的业务属性。
- 不在前端 `localStorage` 中持久化最终星标真相;前端最多做加载态/乐观态缓存。
- 不在本阶段实现跨设备文件内容同步;本设计只定义用户快捷入口在 control-plane 的一致性。
## 用户模型
星标置顶等价于“我的 Sidebar 快捷入口”:
- 页面快捷入口:`kind=page`,目标是某个 workspace 下的页面 identity。
- 文件夹快捷入口:`kind=folder`,目标是某个 workspace 下的相对路径 scope。
- 快捷入口属于用户,不属于文件夹,不影响其他用户。
- 如果某台设备没有对应 workspace 或没有该 folder 相对路径,星标项保留但显示失效态,允许移除或重新绑定。
## SQLite 数据模型
新增 control-plane 表:`sidebar_shortcuts`
建议字段:
```sql
CREATE TABLE sidebar_shortcuts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
root_uri TEXT,
kind TEXT NOT NULL, -- page | folder
source_kind TEXT NOT NULL DEFAULT 'local_folder',
target_id TEXT NOT NULL,
relative_path TEXT,
document_id TEXT,
title TEXT NOT NULL,
icon TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(user_id, workspace_id, kind, target_id)
);
```
`target_id` 规范:
- 页面:优先 `document_id`,例如 `local-md:design%2FREADME.md`
- 文件夹:`local-dir:<encoded relative path>` 或稳定的 `local:folder:<relative path>` 归一化值。
`relative_path` 用于 scoped Explorer 与跨设备路径映射,必须保存 workspace 内相对路径,不保存绝对本机路径作为主键。
`root_uri` 必须作为显式字段保存和返回,而不是只塞在 `metadata_json` 中。原因:
- 星标文件夹点击必须直接消费保存时的 workspace root 上下文,不能从 `workspace_id` 字符串反推本机路径。
- local workspace id 是派生标识,不是路径编码协议;用它反推 `file:///...` 会把 UI fallback 变成事实源。
- `metadata_json.rootUri` 只作为兼容冗余字段,不能作为唯一来源。
长期跨设备映射仍以 `workspace_id + relative_path` 为主;当前设备打开本地 workspace 时必须把实际 `root_uri` 一起写入 shortcut,缺失时应显示失效/需重绑定状态,而不是自动猜测路径。
## API / 命令面
新增最小 API
- `GET /api/sidebar/shortcuts?workspaceId=...`
- 返回当前用户当前 workspace 的星标快捷入口。
- `POST /api/sidebar/shortcuts`
- upsert 快捷入口。
- body 包含 `workspaceId/rootUri/kind/sourceKind/targetId/relativePath/documentId/title/icon`
- `DELETE /api/sidebar/shortcuts/:id`
- 移除当前用户自己的快捷入口。
实现边界:
- API 只操作 control-plane 用户快捷入口表。
- API 不写本地文件,不改 Markdown,不改 `.mnote` 元数据。
- 写操作要求真实登录用户;dev fallback 不应静默创建跨用户快捷入口。
- 每次写入追加 audit`sidebar.shortcut.created` / `sidebar.shortcut.removed`
- 后续需要实时同步时,通过 `outbox_events` 或现有 WS delta 广播 `sidebar.shortcut.*`
## Projection 与 Sidebar 渲染
`WorkspaceShellProjection.starred_items` 需要从 `documents[].is_starred` 迁移为 control-plane `sidebar_shortcuts` 投影。
投影项扩展:
- `id`
- `title`
- `kind: page | folder`
- `href`
- `workspaceId`
- `sourceKind`
- `targetId`
- `relativePath`
- `rootUri`
- `documentId`
- `active`
- `unavailable`
页面快捷入口:
- `href` 指向当前页面打开 URL。
- 点击后走现有 document open 主链。
文件夹快捷入口:
- `href` 可为当前页面 URL 加 query,例如 `?treeView=filetree&filetreeScope=design`
- 更推荐 runtime 拦截点击,调用 `openScopedFileTree(relativePath)`,避免不必要的页面跳转。
- 点击后切换到 Explorer tab,并显示 scoped Explorer。
## Scoped Explorer 行为
文件夹星标点击后:
1. Sidebar 切到 Explorer tab。
2. Explorer 标题显示 scoped 状态,例如 `Explorer / design`
3. FileTree root 只渲染该文件夹下的 children。
4. 子目录继续按需加载。
5. 提供“返回工作区根”入口;点击后恢复 root Explorer。
数据读取:
- scoped root 为 `relativePath=design` 时,调用现有 `load_local_folder_file_tree_children_snapshot(rootUri, "design")` 或对应 API。
- 不调用完整 root snapshot。
- 不预加载 scoped root 之外的兄弟目录。
URL / 状态:
- scoped 状态可以写入 URL query`treeView=filetree&filetreeScope=design`
- 刷新页面后恢复 scoped Explorer。
- 切回“我的页面”不清除 scope;点击“返回工作区根”才清除 scope。
## 顶栏星标与公开状态
顶栏星标按钮:
- 未星标:空心星;tooltip 对齐 Wolai:“点击 加入星标置顶,可在左侧边栏顶部快速访问”。
- 已星标:高亮星;tooltip “取消星标置顶”。
- 点击当前页面星标只操作 `sidebar_shortcuts`
顶栏公开状态:
- `wolai-public-pill` 只有当当前页面存在 active share link / public permission 时显示。
- 没有公开状态时隐藏,不显示“全网公开”。
- 公开分享弹窗仍属于 share link / permission 领域,不与星标快捷入口混用。
## FileTree 右键菜单
文件夹行增加:
- 未星标:`加入星标置顶`
- 已星标:`取消星标置顶`
启用条件:
- `sourceKind == local_folder`
- `rowKind == folder | directory | index`
- 当前用户对 workspace 有至少 read access;写入快捷入口只需要用户偏好写权限,不要求文件系统写权限。
不支持项:
- 普通附件文件暂不加入星标置顶。
- 多选批量星标暂不做。
## 失效与重命名处理
MVP 处理:
- 文件夹被删除:快捷入口保留,显示失效态;点击提示“文件夹不存在”,允许移除。
- 文件夹重命名:如果重命名由 MNote 发起,可同步更新对应 `relative_path/title/target_id`;如果外部重命名,先显示失效态。
- 页面被删除:页面快捷入口显示失效态,允许移除。
长期可选:
- 引入 local object stable id 后,星标目标可由 stable object id 跟随重命名。
## 验收标准
- 无公开分享状态时,顶栏不显示“全网公开”。
- 当前页面点击星标后,Sidebar 星标置顶出现该页面;再次点击移除。
- 右键 `design/` 文件夹可加入星标置顶。
- 点击 `design/` 星标后,切到 Explorer scoped root,只加载并显示 `design/` 下内容,不显示 workspace root 的其他大目录。
- 刷新页面后,`filetreeScope=design` 能恢复 scoped Explorer。
- 同一用户重新登录后,星标快捷入口仍存在。
- 不同用户登录同一 workspace 时,星标列表互不污染。
- 文件夹不存在时,星标项显示失效态并可移除。
## 测试计划
Rust/control-plane
- migration 创建 `sidebar_shortcuts`
- store 可 upsert/list/delete 当前用户快捷入口。
- unique 约束防止同一用户同一 workspace 重复固定同一目标。
- 不同用户同一目标互不影响。
mnote-web
- API 鉴权:只能读写当前用户自己的快捷入口。
- `WorkspaceShellProjection` 从 shortcuts 投影 starred rows。
- local folder scoped snapshot 不扫描完整 root。
浏览器 smoke
- 顶栏公开 pill 隐藏断言。
- 顶栏星标页面 add/remove。
- FileTree 文件夹右键 add/remove。
- 点击星标文件夹进入 scoped Explorer,并断言 root siblings 不渲染。
- 刷新恢复 scoped Explorer。
## 实施顺序
1. control-plane migration / model / store:新增 `sidebar_shortcuts`
2. mnote-web APIlist/upsert/delete shortcuts。
3. workspace shell projection:星标区改读 shortcuts,并保留历史 `is_starred` fixture 兼容测试。
4. 顶栏:星标按钮接 API;公开 pill 改为条件显示。
5. FileTree 右键菜单:文件夹加入/取消星标置顶。
6. Scoped Explorer runtime:支持 `filetreeScope`,只加载 scoped children,并提供返回 root。
7. smoke 与 Rust 测试补齐。
## 决策记录
- 2026-05-26:星标置顶确定为用户级 Sidebar 快捷入口,不是文件夹/页面自身属性。
- 2026-05-26:星标快捷入口必须写入 SQLite control-plane,满足同一用户跨设备一致显示。
- 2026-05-26:文件夹星标点击进入 scoped Explorer,避免大 workspace root 扫描。
- 2026-06-01:只读复核确认 `sidebar_shortcuts` migration/store/API、显式 `rootUri`、scoped Explorer 与 `task492-sidebar-starred-shortcuts-smoke.js` 已覆盖本文主验收;本文归档到 `done/`