✨ feat(prototype): 代码阅读原型加审查工单:侧栏「选工单」弹出审查列工单,选中后树里只剩 PR 改过的文件(A / M / D 着色),「显示全部」退出

This commit is contained in:
cruldra committed 2026-09-27 17:17:37 +08:00
1 parent 198b3f7319
commit 6a1750b2b1
3 files changed
+171 -30

No files matched your search

@@ -32,6 +32,8 @@
| `Prototype / 筛选 · 默认` | `envLock.ts` | 预览顶部固定一条筛选栏:「筛选」+ 当前文件出现过的属性各一个下拉框(种类、访问级别、异步、仅类型)+ 右边「共 33 块」 |
| `Prototype / 筛选 · 按访问级别` | 同上 | 访问级别下拉菜单打开,只勾「公共」:两个私有常量和私有函数 `chmodAll`(连同函数体)藏起来;条件、返回这些没有访问级别的块照常显示;右边「已隐藏 3 块 · 清除」 |
| `Prototype / 筛选 · 按种类` | 同上 | 种类下拉菜单打开,去掉「注释」「导入」:6 块藏起来 |
| `Prototype / 审查 · 选工单` | luffy 的 `routes.py`(审查列的工单是虚构的示例) | 侧栏标题栏第一个按钮「选工单」,点开 VS Code 顶部的快速选择,列出看板「审查」列的工单:第一行工单号 + 标题,后面淡色 PR 号;第二行改了几个文件、本机有没有这张工单的工作副本(没有的选中后先拉 PR 分支建一个只读的) |
| `Prototype / 审查 · 改动的文件` | 同上,改动的文件是虚构的 | 标题栏「代码阅读」后面淡色写「审查 #446」,下面一行工单标题和 PR;树里只剩 PR 改过的 6 个文件,目录层级照留、全部展开,文件名按改动着色:修改 M 黄、新增 A 绿、删除 D 红且划掉(删掉的文件打不开);「新建」两个按钮换成「显示全部」(退出审查)。右边照常打开工作副本里最新的文件,不标差异 |
| `Prototype / 文件注释 · 悬停出按钮` | luffy 的 `campaign` 目录 | 鼠标停在侧栏某一行,行尾出现一排操作按钮,第一个「添加注释」,按钮下面是提示 |
| `Prototype / 文件注释 · 填写` | 同上 | 点「添加注释」后 VS Code 顶部弹出单行输入框,标题带上目录 / 文件名;回车保存,Esc 取消,清空后保存即删除 |
| `Prototype / 文件注释 · 已保存` | 同上 | 有注释的目录 / 文件在名字后面灰字显示注释,太长截断;鼠标停在有注释的行上,按钮换成「编辑注释」,悬停提示里是路径和注释全文 |
@@ -58,9 +60,10 @@
- `Function / <函数名>`、`Import / <来源>`、`Const / <名称>`、`Variable / <名称>`:每块一套 `State=preview | edit`,预览是属性表,编辑是这一块的源码。行数因块而异,所以每块一个组件集;重名的变量后面加行号。
- `Reader / BlockTree`:每个示例文件一整棵代码块树,预览画板直接放它的实例。
- `Reader / ModeToggle`:编辑器右上角的「编辑 / 预览」。
- `Explorer / TreeRow`:侧栏文件树的一行,文件夹展开 / 收起 / 展开且悬停,文件默认 / 选中 / 有改动 / 有改动且选中 / 悬停,共 8 个。名字后面的注释层默认藏起来,有注释的实例里打开;悬停状态行尾带「添加注释」「编辑注释」两个按钮,实例里按有没有注释显示其中一个。
- `Explorer / Header`:侧栏顶部。
- `Explorer / TreeRow`:侧栏文件树的一行,文件夹展开 / 收起 / 展开且悬停,文件默认 / 选中 / 有改动 / 有改动且选中 / 悬停 / 新增 / 删除,共 10 个。名字后面的注释层默认藏起来,有注释的实例里打开;悬停状态行尾带「添加注释」「编辑注释」两个按钮,实例里按有没有注释显示其中一个。
- `Explorer / Header`:`State=default | review`,侧栏顶部,第一个按钮「选工单」;审查时标题后面写工单号,「新建」两个按钮换成「显示全部」。
- `Explorer / Sidebar`:`File=<文件>`,选中某个示例文件时的整个侧栏,各窗口共用;文件注释那几张要改行的状态,单独画。
- `Explorer / ReviewSidebar`:`File=routes`,审查工单时的侧栏。
- `Reader / TableCard`:SQLAlchemy 规则认出的数据表卡片。
- `Reader / EnumCard`:枚举卡片。
- `Reader / CeleryAppCard`、`Reader / BeatCard`:Celery 规则的两张卡片。
@@ -88,6 +91,7 @@
- 编辑器右上角「编辑」→ 整个文件回到 VS Code 编辑器;「预览」→ 回到代码块树。
- 侧栏点 `envLock.ts` / `issueLock.ts` → 在编辑器里打开那个文件。
- 从「筛选」起点开始:点「访问级别」「种类」→ 打开各自的下拉菜单并按勾选筛过;点「清除」→ 回到不筛。
- 从「审查工单」起点(`routes.py · 预览`)开始:点侧栏标题栏第一个按钮「选工单」→ 弹框;点第一张工单 → 树里只剩改过的文件;侧栏标题栏的「×」(显示全部)→ 退出审查。
- 从「文件注释」起点开始:点 `campaign` 行尾的「添加注释」→ 顶部输入框;点输入框 → 保存后的侧栏。
- 编辑区和侧栏可以上下滚动。
@@ -116,4 +120,5 @@ node --test test.js
- [ ] 遍历:小框和前后文字在同一行里对齐、太长时整体换行;循环图标和「循环」块同色
- [ ] 筛选:筛选栏固定在预览顶部、和代码块左右对齐;下拉菜单盖在代码块上面;藏掉的块不留空位,左边层级竖线跟着变短
- [ ] 文件注释:悬停行的按钮贴在行尾、不挤掉名字;注释灰字紧跟名字、长了截断;顶部输入框居中,看得出是 VS Code 自带的样子
- [ ] 审查:选工单弹框居中在顶部、像 VS Code 自带的快速选择;侧栏标题栏工单号不挤掉按钮;A / M / D 颜色和 VS Code 源代码管理里一致
- [ ] 演示:块的编辑 / 预览、右上角编辑 / 预览、侧栏切文件都能点通
+129 -22
View File
@@ -48,6 +48,8 @@ const COLORS = {
link: "#4daafc", // textLink.foreground
toggleOn: "#37373d",
gitModified: "#e2c08d", // gitDecoration.modifiedResourceForeground
gitAdded: "#81b88b", // gitDecoration.addedResourceForeground
gitDeleted: "#c74e39", // gitDecoration.deletedResourceForeground
code: "#d4d4d4",
comment: "#6a9955",
folder: "#8b9ba8",
@@ -830,6 +832,7 @@ const LUCIDE = {
"ellipsis": [["circle", { cx: "12", cy: "12", r: "1" }], ["circle", { cx: "19", cy: "12", r: "1" }], ["circle", { cx: "5", cy: "12", r: "1" }]],
"columns-2": [["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M12 3v18" }]],
"x": [["path", { d: "M18 6 6 18" }], ["path", { d: "m6 6 12 12" }]],
"git-pull-request": [["circle", { cx: "18", cy: "18", r: "3" }], ["circle", { cx: "6", cy: "6", r: "3" }], ["path", { d: "M13 6h3a2 2 0 0 1 2 2v7" }], ["path", { d: "M6 9v12" }]],
"files": [["path", { d: "M20 7h-3a2 2 0 0 1-2-2V2" }], ["path", { d: "M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z" }], ["path", { d: "M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8" }]],
"search": [["circle", { cx: "11", cy: "11", r: "8" }], ["path", { d: "m21 21-4.3-4.3" }]],
"git-branch": [["path", { d: "M6 3v12" }], ["circle", { cx: "18", cy: "6", r: "3" }], ["circle", { cx: "6", cy: "18", r: "3" }], ["path", { d: "M18 9a9 9 0 0 1-9 9" }]],
@@ -1102,6 +1105,40 @@ function askPanel(ctx, state) {
return node;
}
// ─── 审查工单 ────────────────────────────────────────────────────────────────
//
// 侧栏标题栏「选工单」列出看板「审查」列的工单。选一个后目录树只剩这张工单的 PR 改过的文件:保留目录层级、全部展开,
// 文件名按改动着色(修改 M、新增 A、删除 D 并划掉)。打开的文件读本机这张工单的工作副本(PR 的最新代码):
// 已有工作副本就用它,没有就拉 PR 分支建一个只读的。删掉的文件没有最新版,树里只列出来、打不开。
// 示例数据:审查列的工单和改动的文件都是虚构的。
const REVIEW_ISSUES = [
{ number: 446, title: "用户资料:上传头像,收紧非管理员改资料的权限", pr: 447, files: 6, local: true },
{ number: 452, title: "桌面端:照 Figma 原型重做设置页", pr: 455, files: 12, local: false },
{ number: 448, title: "会话消息区 UI 优化", pr: 450, files: 7, local: true },
{ number: 444, title: "算法备案:补齐文本 AI 生成标识", pr: 449, files: 3, local: false },
{ number: 438, title: "发布链路可靠性设计重审", pr: 440, files: 9, local: false },
];
const REVIEW = {
issue: REVIEW_ISSUES[0],
file: "routes",
// PR 改过的文件,按目录树排好;file 指向有源码快照的示例文件
rows: [
{ depth: 1, type: "folder", name: "backend", open: true },
{ depth: 2, type: "folder", name: "src", open: true },
{ depth: 3, type: "folder", name: "luffy_agent", open: true },
{ depth: 4, type: "folder", name: "core", open: true },
{ depth: 5, type: "file", name: "storage.py", git: "M" },
{ depth: 4, type: "folder", name: "user", open: true },
{ depth: 5, type: "file", name: "legacy_profile.py", git: "D" },
{ depth: 5, type: "file", name: "routes.py", file: "routes", git: "M" },
{ depth: 5, type: "file", name: "schemas.py", git: "M" },
{ depth: 5, type: "file", name: "services.py", git: "M" },
{ depth: 2, type: "folder", name: "tests", open: true },
{ depth: 3, type: "folder", name: "user", open: true },
{ depth: 4, type: "file", name: "test_avatar.py", git: "A" },
],
};
// ─── 筛选 ────────────────────────────────────────────────────────────────────
//
// 每块带几条「属性 = 值」,由语言插件从语法树换算成各语言统一的属性,翻译插件给属性和值起显示名;
@@ -1548,14 +1585,17 @@ function modeToggle(ctx, mode) {
* Explorer / TreeRow — 文件夹展开 / 收起,文件默认 / 选中 / 有改动 / 有改动且选中,外加鼠标悬停。名字和扩展名在实例里改。
* 名字后面的 Note 是这一项的注释(VS Code 树的 description),默认藏起来,有注释的实例里打开。
* 悬停时行尾出现一排操作按钮(VS Code 树的行内操作):没注释时「添加注释」,有注释时实例里换成「编辑注释」。
* 审查工单时文件按 PR 里的改动着色:修改 M、新增 A、删除 D(名字划掉)。
*/
const ROW_VARIANTS = [
["folder", "closed"], ["folder", "open"], ["folder", "open-hover"],
["file", "default"], ["file", "selected"], ["file", "modified"], ["file", "modified-selected"], ["file", "hover"],
["file", "added"], ["file", "deleted"],
];
const GIT_STATES = { modified: ["M", COLORS.gitModified], added: ["A", COLORS.gitAdded], deleted: ["D", COLORS.gitDeleted] };
function treeRow(ctx, type, state) {
const selected = state.endsWith("selected");
const modified = state.startsWith("modified");
const git = GIT_STATES[state.replace("-selected", "")];
const hovered = state.endsWith("hover");
const node = frame(ctx, `Type=${type}, State=${state}`, { component: true, dir: "HORIZONTAL", width: SIDEBAR_W, py: 3, gap: 6,
align: "CENTER", fill: selected ? COLORS.selection : hovered ? COLORS.hover : null, stroke: selected ? COLORS.focus : null });
@@ -1569,12 +1609,13 @@ function treeRow(ctx, type, state) {
const ext = append(node, frame(ctx, "Ext", { dir: "HORIZONTAL", width: 16, height: 16, align: "CENTER", justify: "CENTER" }));
text(ctx, ext, "Ext label", "TS", { hug: true, medium: true, size: 8, lineHeight: 10, color: COLORS.ts });
}
text(ctx, node, "Name", type === "folder" ? "folder" : "file.ts", { hug: true, size: 13, lineHeight: 16,
color: modified ? COLORS.gitModified : COLORS.fg });
const name = text(ctx, node, "Name", type === "folder" ? "folder" : "file.ts", { hug: true, size: 13, lineHeight: 16,
color: git ? git[1] : COLORS.fg });
if (state === "deleted") name.textDecoration = "STRIKETHROUGH";
const note = text(ctx, node, "Note", "注释", { size: 12, lineHeight: 16, singleLine: true, color: COLORS.desc });
note.opacity = 0.8;
note.visible = false;
if (modified) text(ctx, node, "Git", "M", { hug: true, size: 12, lineHeight: 16, color: COLORS.gitModified });
if (git) text(ctx, node, "Git", git[0], { hug: true, size: 12, lineHeight: 16, color: git[1] });
if (hovered) {
const actions = append(node, frame(ctx, "Actions", { dir: "HORIZONTAL", gap: 2, align: "CENTER" }));
const copy = append(actions, frame(ctx, "Action / 复制", { dir: "HORIZONTAL", height: 22, px: 3, gap: 1, radius: 5, align: "CENTER" }));
@@ -1602,15 +1643,51 @@ function quickInput(ctx) {
return node;
}
/** Explorer / Header — 侧栏顶部:视图标题 + 新建文件 / 新建文件夹 / 刷新 / 全部折叠 */
function explorerHeader(ctx) {
const node = frame(ctx, "State=default", { component: true, dir: "HORIZONTAL", width: SIDEBAR_W, py: 9, gap: 8,
/**
* Explorer / Header — State=default | review:侧栏顶部,视图标题 + 一排按钮。
* 第一个按钮「选工单」。审查工单时标题后面淡色写工单号(视图的 description),「新建」两个按钮换成「显示全部」(退出审查)。
*/
function explorerHeader(ctx, state) {
const review = state === "review";
const node = frame(ctx, `State=${state}`, { component: true, dir: "HORIZONTAL", width: SIDEBAR_W, py: 9, gap: 8,
align: "CENTER", justify: "SPACE_BETWEEN", fill: COLORS.side });
node.paddingLeft = 20;
node.paddingRight = 10;
text(ctx, node, "Title", "代码阅读", { hug: true, size: 11, lineHeight: 16, color: COLORS.fg });
const heading = append(node, frame(ctx, "Heading", { dir: "HORIZONTAL", gap: 6, align: "CENTER" }));
text(ctx, heading, "Title", "代码阅读", { hug: true, size: 11, lineHeight: 16, color: COLORS.fg });
if (review) text(ctx, heading, "Description", `审查 #${REVIEW.issue.number}`, { hug: true, size: 11, lineHeight: 16, color: COLORS.desc });
const actions = append(node, frame(ctx, "Actions", { dir: "HORIZONTAL", gap: 10, align: "CENTER" }));
for (const name of ["file-plus", "folder-plus", "refresh-cw", "chevrons-down-up", "ellipsis"]) append(actions, icon(ctx, name, COLORS.fg, 16));
const buttons = [["git-pull-request", "选工单"], ...(review ? [["x", "显示全部"]] : [["file-plus"], ["folder-plus"]]),
["refresh-cw"], ["chevrons-down-up"], ["ellipsis"]];
for (const [name, label] of buttons) append(actions, icon(ctx, name, COLORS.fg, 16, label ? { name: `Action / ${label}` } : {}));
return node;
}
/**
* 选工单:点侧栏「选工单」后 VS Code 顶部弹出的快速选择,列出看板「审查」列的工单。
* 每项第一行是工单号和标题,后面淡色写 PR 号;第二行写改了几个文件、本机有没有这张工单的工作副本。
*/
function reviewPicker(ctx, screen, width) {
const node = frame(ctx, "Review picker", { width: QUICK_INPUT_W, pad: 6, gap: 6, radius: 8, fill: COLORS.widget, stroke: COLORS.widgetBorder });
text(ctx, node, "Title", "选工单 · 看板「审查」列", { size: 12, lineHeight: 16, align: "CENTER", color: COLORS.fg });
const field = filled(ctx, node, "Field", { dir: "HORIZONTAL", px: 6, py: 4, gap: 2, radius: 4, align: "CENTER", fill: COLORS.input, stroke: COLORS.focus });
append(field, frame(ctx, "Caret", { width: 1, height: 16, fill: COLORS.fgStrong }));
text(ctx, field, "Placeholder", "输入工单号或标题筛选", { hug: true, size: 13, lineHeight: 18, color: COLORS.dim });
const list = filled(ctx, node, "List", {});
REVIEW_ISSUES.forEach((issue, index) => {
const on = index === 0;
const item = filled(ctx, list, `Issue / #${issue.number}`, { px: 8, py: 4, gap: 1, radius: 4,
fill: on ? COLORS.selection : null, stroke: on ? COLORS.focus : null });
const title = `#${issue.number} ${issue.title}`;
const label = text(ctx, item, "Label", `${title} PR #${issue.pr}`, { size: 13, lineHeight: 18, singleLine: true, color: COLORS.fgStrong });
label.setRangeFills(title.length, label.characters.length, paint(COLORS.desc));
text(ctx, item, "Detail", `改了 ${issue.files} 个文件 · ${issue.local ? "本机有工作副本" : "本机没有工作副本,选中后拉取 PR 分支"}`,
{ size: 12, lineHeight: 16, color: COLORS.desc });
});
screen.appendChild(node);
node.layoutPositioning = "ABSOLUTE";
node.x = (width - QUICK_INPUT_W) / 2;
node.y = TITLE_H + 6;
return node;
}
@@ -1726,8 +1803,9 @@ function createComponents(ctx, page) {
lib.row = make("Explorer / TreeRow", ROW_VARIANTS.map(([type, state]) => `${type}/${state}`), 2,
(entry) => treeRow(ctx, ...entry.split("/")));
lib.quickInput = make("Window / QuickInput", ["typing"], 1, () => quickInput(ctx));
lib.explorerHeader = make("Explorer / Header", ["default"], 1, () => explorerHeader(ctx));
lib.explorerHeader = make("Explorer / Header", ["default", "review"], 2, (state) => explorerHeader(ctx, state));
lib.sidebar = make("Explorer / Sidebar", fileKeys, 3, (fileKey) => sidebarComponent(ctx, lib, fileKey));
lib.reviewSidebar = make("Explorer / ReviewSidebar", [REVIEW.file], 1, (fileKey) => sidebarComponent(ctx, lib, fileKey, { review: true }));
lib.windowTitle = make("Window / TitleBar", ["active"], 1, () => windowTitle(ctx));
lib.activity = make("Window / ActivityBar", ["reader"], 1, () => activityBar(ctx));
lib.status = make("Window / StatusBar", MODES, 1, (mode) => statusBar(ctx, mode));
@@ -1758,6 +1836,8 @@ function rowVariant(item, selected, hover) {
if (item.name === hover) return item.type === "folder" ? "folder/open-hover" : "file/hover";
if (item.type === "folder") return `folder/${item.open ? "open" : "closed"}`;
const isSelected = item.file === selected;
if (item.git === "A") return "file/added";
if (item.git === "D") return "file/deleted";
if (item.git) return isSelected ? "file/modified-selected" : "file/modified";
return isSelected ? "file/selected" : "file/default";
}
@@ -1766,14 +1846,15 @@ function rowVariant(item, selected, hover) {
* 侧栏:「代码阅读」视图,外观照搬 VS Code 资源管理器。
* 普通窗口放 Explorer / Sidebar 的实例(每个示例文件一个变体,窗口之间共用);
* 文件注释那几张要改行的悬停状态和注释,单独画。o.hover:鼠标停在哪一行;o.notes:各行的注释。
* o.review:审查工单时的侧栏(Explorer / ReviewSidebar),树里只有 PR 改过的文件。
*/
function sidebar(ctx, parent, lib, selected, o = {}) {
if (!o.hover && !o.notes) {
const instance = lib.sidebar.get(selected).createInstance();
const instance = (o.review ? lib.reviewSidebar : lib.sidebar).get(selected).createInstance();
instance.name = "Sidebar";
append(parent, instance);
instance.layoutSizingVertical = "FILL";
const rows = new Map(WORKSPACES[FILES[selected].workspace].rows.filter((item) => item.file)
const rows = new Map(sidebarRows(selected, o).filter((item) => item.file)
.map((item) => [item.file, instance.findOne((node) => node.name === `Row / ${item.name}`)]));
return { pane: instance, rows };
}
@@ -1782,10 +1863,17 @@ function sidebar(ctx, parent, lib, selected, o = {}) {
return { pane, rows: sidebarContent(ctx, pane, lib, selected, o) };
}
/** Explorer / Sidebar — File=<文件>:选中这个文件时的整个侧栏。 */
function sidebarComponent(ctx, lib, selected) {
function sidebarRows(selected, o) {
return o.review ? REVIEW.rows : WORKSPACES[FILES[selected].workspace].rows;
}
/**
* Explorer / Sidebar — File=<文件>:选中这个文件时的整个侧栏。
* Explorer / ReviewSidebar — File=<文件>:审查工单时的侧栏,标题栏写工单号,下面一行工单标题和 PR(视图的 message),树里只有 PR 改过的文件。
*/
function sidebarComponent(ctx, lib, selected, o = {}) {
const node = frame(ctx, `File=${selected}`, { component: true, width: SIDEBAR_W, height: MAIN_H, fill: COLORS.side });
sidebarContent(ctx, node, lib, selected, {});
sidebarContent(ctx, node, lib, selected, o);
return node;
}
@@ -1793,15 +1881,23 @@ function sidebarContent(ctx, pane, lib, selected, o) {
const workspace = WORKSPACES[FILES[selected].workspace];
pane.clipsContent = true;
pane.overflowDirection = "VERTICAL";
const header = lib.explorerHeader.get("default").createInstance();
const header = lib.explorerHeader.get(o.review ? "review" : "default").createInstance();
header.name = "Header";
append(pane, header, true);
if (o.review) {
const { issue } = REVIEW;
const message = filled(ctx, pane, "Message", { py: 4 });
message.paddingLeft = 20;
message.paddingRight = 12;
text(ctx, message, "Text", `#${issue.number} ${issue.title} · PR #${issue.pr} · 改了 ${issue.files} 个文件`,
{ size: 12, lineHeight: 17, color: COLORS.desc });
}
const root = filled(ctx, pane, "Root", { dir: "HORIZONTAL", py: 3, gap: 6, align: "CENTER" });
root.paddingLeft = 8;
append(root, icon(ctx, "chevron-down", COLORS.fg, 16));
text(ctx, root, "Name", workspace.root, { hug: true, medium: true, size: 13, lineHeight: 16, color: COLORS.fgStrong });
const rows = new Map();
for (const item of workspace.rows) {
for (const item of sidebarRows(selected, o)) {
const instance = lib.row.get(rowVariant(item, selected, o.hover)).createInstance();
instance.name = `Row / ${item.name}`;
append(pane, instance, true);
@@ -1907,7 +2003,8 @@ function windowBoard(ctx, page, lib, name, o) {
if (o.mode === "edit") setText(status, "Item / TypeScript", FILES[o.file].lang === "python" ? "Python" : "TypeScript");
status.name = "Status bar";
append(screen, status, true);
const board = { screen, rows: side.rows, titleBar, view, filter };
const board = { screen, sidebar: side.pane, rows: side.rows, titleBar, view, filter };
if (o.picker) board.picker = reviewPicker(ctx, screen, width);
if (o.tooltip) board.tooltip = tooltip(ctx, screen, o.tooltip, WORKSPACES[FILES[o.file].workspace].rows);
if (o.copyMenu) board.copyMenu = copyMenu(ctx, screen, o.copyMenu, WORKSPACES[FILES[o.file].workspace].rows);
if (o.quickInput) {
@@ -1993,6 +2090,10 @@ async function build(api, stage = () => {}) {
const narrow = windowBoard(ctx, page, lib, "envLock.ts · 预览(编辑区变窄)", { file: "envLock", mode: "preview", width: WINDOW_NARROW });
const filters = Object.fromEntries(Object.entries({ none: "默认", access: "按访问级别", kind: "按种类" }).map(([key, label]) =>
[key, windowBoard(ctx, page, lib, `筛选 · ${label}`, { file: FILTER_FILE, mode: "preview", filter: key })]));
const review = {
pick: windowBoard(ctx, page, lib, "审查 · 选工单", { file: REVIEW.file, mode: "preview", picker: true }),
files: windowBoard(ctx, page, lib, "审查 · 改动的文件", { file: REVIEW.file, mode: "preview", review: true }),
};
const askBoard = frame(ctx, "Prototype / 问 AI", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 6, fill: COLORS.editor });
page.appendChild(askBoard);
for (const [line, state] of [[40, "asking"], [41, null], [45, "history"]]) {
@@ -2086,9 +2187,15 @@ async function build(api, stage = () => {}) {
await connect(named(filters.access.filter, "Clear"), filters.none.screen, "NAVIGATE");
await connect(named(filters.kind.filter, "Clear"), filters.none.screen, "NAVIGATE");
// 审查工单:侧栏「选工单」→ 选工单弹框,点工单 → 树里只剩 PR 改过的文件;「显示全部」→ 退出审查
await connect(named(boards.routes.preview.sidebar, "Action / 选工单"), review.pick.screen, "NAVIGATE");
await connect(named(review.pick.picker, `Issue / #${REVIEW.issue.number}`), review.files.screen, "NAVIGATE");
await connect(named(review.files.sidebar, "Action / 显示全部"), boards.routes.preview.screen, "NAVIGATE");
stage("排布画板");
const screens = [...Object.values(boards).flatMap((pair) => [pair.edit.screen, pair.preview.screen]), narrow.screen,
...Object.values(notes).map((board) => board.screen), ...Object.values(filters).map((board) => board.screen), iteration, tableBoard, enumBoard, celeryBoard, askBoard];
...Object.values(notes).map((board) => board.screen), ...Object.values(filters).map((board) => board.screen),
...Object.values(review).map((board) => board.screen), iteration, tableBoard, enumBoard, celeryBoard, askBoard];
let y = 0;
for (const screen of screens) {
screen.x = 0;
@@ -2103,10 +2210,10 @@ async function build(api, stage = () => {}) {
}
const start = boards.envLock.preview.screen;
page.flowStartingPoints = [{ nodeId: start.id, name: "预览 envLock.ts" }, { nodeId: notes.hover.screen.id, name: "文件注释" },
{ nodeId: filters.none.screen.id, name: "筛选" }];
{ nodeId: filters.none.screen.id, name: "筛选" }, { nodeId: boards.routes.preview.screen.id, name: "审查工单" }];
page.selection = [start];
api.viewport.scrollAndZoomIntoView(page.selection);
return { page, components, boards, narrow, notes, filters, iteration, tableBoard, enumBoard, celeryBoard, askBoard, screens };
return { page, components, boards, narrow, notes, filters, review, iteration, tableBoard, enumBoard, celeryBoard, askBoard, screens };
}
async function run(api) {
@@ -2125,6 +2232,6 @@ async function run(api) {
}
if (typeof module !== "undefined") {
module.exports = { ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
module.exports = { REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
}
if (typeof figma !== "undefined") void run(figma);
+35 -6
View File
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { fakeFigma } = require("../../figma-lab/fake-figma");
function action(node) { return node.reactions[0].actions[0]; }
@@ -22,8 +22,9 @@ function countBlocks(nodes) {
// (luffy 只有一个示例文件,侧栏没有可跳的)
// 文件注释:悬停出的「添加注释」→ 填写,输入框回车 → 已保存 = 2
// 筛选:默认栏的「访问级别」「种类」各打开一个下拉菜单,两张筛过的画板「清除」回默认 = 4
// 侧栏复制:已保存那张悬停行的「复制」→ 复制菜单 = 1;合计 107
const EXPECTED_LINKS = 107;
// 侧栏复制:已保存那张悬停行的「复制」→ 复制菜单 = 1
// 审查工单:「选工单」→ 弹框,点工单 → 改动的文件,「显示全部」→ 退出审查 = 3;合计 110
const EXPECTED_LINKS = 110;
const EXPECTED_SETS = [
["Reader / ModeToggle", 2],
@@ -36,10 +37,11 @@ const EXPECTED_SETS = [
["Reader / BeatCard", 1],
["Reader / IterationBlock", 3],
["Reader / FilterBar", 3],
["Explorer / TreeRow", 8],
["Explorer / TreeRow", 10],
["Window / QuickInput", 1],
["Explorer / Header", 1],
["Explorer / Header", 2],
["Explorer / Sidebar", 3],
["Explorer / ReviewSidebar", 1],
["Window / TitleBar", 1],
["Window / ActivityBar", 1],
["Window / StatusBar", 2],
@@ -454,6 +456,33 @@ test("filter menus offer select all / select none", async () => {
assert.equal(named(filters.kind.filter, "Select none").characters, "取消全选");
});
test("review: picking an issue leaves only the PR's changed files in the tree", async () => {
const api = fakeFigma();
const { review, boards, components } = await build(api);
const { lib } = components;
// 选工单:弹框列出审查列的工单,点第一张进审查
const picker = review.pick.picker;
assert.equal(picker.layoutPositioning, "ABSOLUTE");
assert.deepEqual(picker.findAll((node) => node.name.startsWith("Issue / ")).map((node) => node.name),
REVIEW_ISSUES.map((issue) => `Issue / #${issue.number}`));
assert.equal(action(named(boards.routes.preview.sidebar, "Action / 选工单")).destinationId, review.pick.screen.id);
assert.equal(action(named(picker, `Issue / #${REVIEW.issue.number}`)).destinationId, review.files.screen.id);
// 侧栏:标题栏写工单号,树里只有 PR 改过的文件,按改动着色,删掉的文件名字划掉;「显示全部」退出审查
const side = review.files.sidebar;
assert.equal(side.mainComponentId, lib.reviewSidebar.get(REVIEW.file).id);
assert.equal(named(side, "Description").characters, `审查 #${REVIEW.issue.number}`);
const files = side.findAll((node) => node.name.startsWith("Row / ") && node.findOne((child) => child.name === "Ext label"));
assert.deepEqual(files.map((node) => node.name.slice(6)), REVIEW.rows.filter((row) => row.type === "file").map((row) => row.name));
assert.equal(named(named(side, "Row / legacy_profile.py"), "Git").characters, "D");
assert.equal(named(named(side, "Row / legacy_profile.py"), "Name").textDecoration, "STRIKETHROUGH");
assert.equal(named(named(side, "Row / test_avatar.py"), "Git").characters, "A");
assert.equal(named(named(side, "Row / routes.py"), "Git").characters, "M");
assert.equal(action(named(side, "Action / 显示全部")).destinationId, boards.routes.preview.screen.id);
// 右边照常打开最新的文件,不标差异
assert.equal(named(review.files.view, "Block tree / routes.py").mainComponentId, lib.tree.get("routes").node.id);
});
test("boards and sets sit on the page without overlapping", async () => {
const api = fakeFigma();
const result = await build(api);
@@ -476,7 +505,7 @@ test("the plugin stays small enough for Figma to build quickly", async () => {
if (node.type !== "INSTANCE") node.children.forEach(walk);
};
result.page.children.forEach(walk);
// 卡死那次是 5000 多个图层,而且每次读宽度都逼 Figma 重排;侧栏、块右上角按钮做成共用组件后,现在 21 张画板约 2650 个
// 卡死那次是 5000 多个图层,而且每次读宽度都逼 Figma 重排;侧栏、块右上角按钮做成共用组件后,现在 23 张画板约 2800 个
assert.ok(owned < 3000, `page owns ${owned} layers outside instances`);
});