Skip to content

Commit c66df9c

Browse files
committed
fix(notes): 审查即修——批量删除勾选视图裁剪与豁免/注释同步
新增代码七维审查(v0.12.7+v0.12.8 批):逻辑性 P2×1(勾选集合在搜索/标签/组过滤/刷新变化后未裁剪——批量删除可波及当前视图外笔记,即修:notes 变化裁剪为可见子集,'只删可见子集'安全边界)+ 规范 P3×2(注释版本标签 v0.12.7→v0.12.8;NotesPage 361 行豁免登记值同步);接入/牵连/性能/冗余/安全通过。新增 NoteListView 5 项交互单测。tsc 零错误、vitest 97 通过、build 通过。
1 parent 4ef7e85 commit c66df9c

4 files changed

Lines changed: 121 additions & 5 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* NoteListView.test.tsx — 笔记列表勾选批量删除交互测试(v0.12.8 审查补测)。
4+
*
5+
* @ai-context: 覆盖关键路径——行勾选出现批量栏 / 全选三态 / 删除执行后清空勾选
6+
* (resolve=true)/ 取消确认保留勾选(resolve=false)/ 列表数据变化
7+
* 后裁剪不可见勾选(只删可见子集安全边界)。纯 UI 组件无 invoke。
8+
*/
9+
import { afterEach, describe, expect, it, vi } from "vitest";
10+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
11+
import type { Note } from "../types";
12+
import NoteListView from "./NoteListView";
13+
14+
function makeNote(id: number, title: string): Note {
15+
return {
16+
id,
17+
title,
18+
content: "",
19+
source: "manual",
20+
tags: "[]",
21+
pin: 0,
22+
created_at: 0,
23+
updated_at: 0,
24+
};
25+
}
26+
27+
const notesA = [makeNote(1, "笔记一"), makeNote(2, "笔记二")];
28+
29+
const baseProps = {
30+
keyword: "",
31+
tagFilter: null,
32+
sortMode: "updated-desc" as const,
33+
allTags: [] as string[],
34+
selectedId: null,
35+
status: "",
36+
onKeywordChange: vi.fn(),
37+
onTagFilterChange: vi.fn(),
38+
onSortModeChange: vi.fn(),
39+
onSelect: vi.fn(),
40+
onCreate: vi.fn(),
41+
onRefresh: vi.fn(),
42+
onOpenSession: vi.fn(),
43+
};
44+
45+
function renderList(overrides: Partial<Parameters<typeof NoteListView>[0]> = {}) {
46+
return render(<NoteListView notes={notesA} {...baseProps} onBatchDelete={vi.fn().mockResolvedValue(true)} {...overrides} />);
47+
}
48+
49+
afterEach(() => cleanup());
50+
51+
describe("NoteListView 勾选批量删除", () => {
52+
it("行勾选后出现批量栏,计数正确(Arrange/Act/Assert)", () => {
53+
renderList();
54+
expect(screen.queryByText(/已选/)).toBeNull(); // 初始无批量栏
55+
fireEvent.click(screen.getAllByRole("checkbox")[0]);
56+
expect(screen.getByText(/已选 1 个/)).toBeTruthy();
57+
});
58+
59+
it("全选框三态:勾一后全选,再点全选清空", () => {
60+
renderList();
61+
fireEvent.click(screen.getAllByRole("checkbox")[0]); // 勾 1 行 → 批量栏出现
62+
const selectAll = screen.getByTitle("全选当前列表的笔记");
63+
expect((selectAll as HTMLInputElement).checked).toBe(false);
64+
fireEvent.click(selectAll); // 未全选 → 全选
65+
expect(screen.getByText(/已选 2 个/)).toBeTruthy();
66+
fireEvent.click(screen.getByTitle("全选当前列表的笔记")); // 已全选 → 清空
67+
expect(screen.queryByText(/已选/)).toBeNull();
68+
});
69+
70+
it("删除执行成功(resolve=true)后清空勾选", async () => {
71+
const onBatchDelete = vi.fn().mockResolvedValue(true);
72+
renderList({ onBatchDelete });
73+
fireEvent.click(screen.getAllByRole("checkbox")[0]);
74+
fireEvent.click(screen.getByText("批量删除"));
75+
expect(onBatchDelete).toHaveBeenCalledWith([1]);
76+
await vi.waitFor(() => expect(screen.queryByText(/已选/)).toBeNull());
77+
});
78+
79+
it("取消确认(resolve=false)保留勾选", async () => {
80+
const onBatchDelete = vi.fn().mockResolvedValue(false);
81+
renderList({ onBatchDelete });
82+
fireEvent.click(screen.getAllByRole("checkbox")[0]);
83+
fireEvent.click(screen.getByText("批量删除"));
84+
await vi.waitFor(() => expect(onBatchDelete).toHaveBeenCalled());
85+
expect(screen.getByText(/已选 1 个/)).toBeTruthy();
86+
});
87+
88+
it("列表数据变化后裁剪不可见勾选(只删可见子集)", async () => {
89+
const { rerender } = renderList();
90+
fireEvent.click(screen.getAllByRole("checkbox")[0]);
91+
fireEvent.click(screen.getAllByRole("checkbox")[1]);
92+
expect(screen.getByText(/已选 2 个/)).toBeTruthy();
93+
// 模拟切过滤视图/删除刷新:notes 只剩 1 条 → 勾选裁剪到可见子集
94+
const onBatchDelete = vi.fn().mockResolvedValue(true);
95+
rerender(<NoteListView notes={[makeNote(1, "笔记一")]} {...baseProps} onBatchDelete={onBatchDelete} />);
96+
expect(screen.getByText(/已选 1 个/)).toBeTruthy();
97+
fireEvent.click(screen.getByText("批量删除"));
98+
expect(onBatchDelete).toHaveBeenCalledWith([1]); // 被裁剪的不可见笔记不在删除集
99+
});
100+
});

‎app/src/components/NoteListView.tsx‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
* @ai-context: 纯展示/受控组件——数据与状态全部由 NotesPage 编排(参照
55
* SessionsPage → SessionListPanel/SessionDetailPanel 既有模式)。
66
* 行 id=`note-row-{id}` 供 focusNoteId 跨页直达滚动定位。
7-
* @ai-context: v0.12.7 勾选批量删除(与会话管理台同逻辑:行内勾选 +
7+
* @ai-context: v0.12.8 勾选批量删除(与会话管理台同逻辑:行内勾选 +
88
* 底部批量操作栏;副作用经 onBatchDelete 上抛父层确认/invoke)。
99
*/
10-
import { useState } from "react";
10+
import { useEffect, useState } from "react";
1111
import type { Note } from "../types";
1212

1313
export type SortMode = "updated-desc" | "pin-first" | "created-desc";
@@ -67,6 +67,22 @@ export default function NoteListView({
6767

6868
const clearSelection = () => setSelected(new Set());
6969

70+
// v0.12.8 审查即修:列表数据变化(搜索/标签/排序/组过滤/删除后刷新)后裁剪勾选,
71+
// 防止批量删除波及当前视图外的笔记——"只删可见子集"安全边界
72+
useEffect(() => {
73+
setSelected((cur) => {
74+
if (cur.size === 0) return cur;
75+
const visible = new Set(notes.map((n) => n.id));
76+
let changed = false;
77+
const next = new Set<number>();
78+
for (const id of cur) {
79+
if (visible.has(id)) next.add(id);
80+
else changed = true;
81+
}
82+
return changed ? next : cur;
83+
});
84+
}, [notes]);
85+
7086
return (
7187
<div style={{ width: 320, flexShrink: 0, borderRight: "1px solid #e5e7eb", display: "flex", flexDirection: "column" }}>
7288
<div style={{ padding: "10px 14px", borderBottom: "1px solid #e5e7eb", fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
@@ -180,7 +196,7 @@ export default function NoteListView({
180196
);
181197
})}
182198
</div>
183-
{/* v0.12.7:勾选批量删除栏(与会话管理台同模式——全选三态 + 计数 + 批量删除) */}
199+
{/* v0.12.8:勾选批量删除栏(与会话管理台同模式——全选三态 + 计数 + 批量删除) */}
184200
{selected.size > 0 && (
185201
<div style={{ borderTop: "1px solid #e5e7eb", padding: 8, display: "flex", gap: 6, alignItems: "center", background: "#fff" }}>
186202
<input

‎app/src/pages/NotesPage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
157157
};
158158

159159
/**
160-
* v0.12.7:列表级批量删除(用户要求:与「会话」管理台同操作逻辑——勾选 +
160+
* v0.12.8:列表级批量删除(用户要求:与「会话」管理台同操作逻辑——勾选 +
161161
* 确认 + 逐条 invoke,无需先打开笔记;删除选中笔记同步清空右栏选中态)。
162162
* 返回是否执行了删除(取消确认返回 false——父面板据此保留/清空勾选)。
163163
*/

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
| app/src/components/SessionDetailPanel.tsx | 371 | 会话详情面板:质量报告/段列表/OCR 概览/操作区单一面板完整交互流内聚(前端审查登记) | 若再增长:质量报告区拆至 SessionQualityReport.tsx |
6868
| app/src/components/ProfileDetector.tsx | 330 | 档案检测组件:投票/确认流/记忆偏好 UI + v0.11.5 Task 5 冲突提示内聚(实测 2026-08-22) | 若再增长:确认流拆至 ProfileConfirmFlow.tsx |
6969
| app/src/components/NoteEditView.tsx | —— | v0.12.2 行数修正:实测 299 行(登记值 315 过期),本版新增 autoFocus 一行后仍 300 行以内——登记移除,见"已拆分/登记移除记录" | —— |
70-
| app/src/pages/NotesPage.tsx | 334 | v0.12.2 三栏编排(GroupSidebar/FeedFragmentList/NoteListView 插槽 + 收件箱动线 onPromoted 接线 + 新建零对话框)——编排层内聚(数据获取/选中态/快捷键/辅助面板插槽),组件已全部下沉拆件;2026-08-23 审查修复(onPromoted 同步清搜索态/onOpenInbox 清组过滤)+6 行 | 若再增长:键盘监听与收件箱动线回调拆至 useNotesPageShortcuts.ts / useInboxFlow.ts |
70+
| app/src/pages/NotesPage.tsx | 361 | v0.12.2 三栏编排(GroupSidebar/FeedFragmentList/NoteListView 插槽 + 收件箱动线 onPromoted 接线 + 新建零对话框)——编排层内聚(数据获取/选中态/快捷键/辅助面板插槽),组件已全部下沉拆件;2026-08-23 审查修复(onPromoted 同步清搜索态/onOpenInbox 清组过滤)+6 行;v0.12.8 列表级批量删除(runBatchDelete 确认+逐条 invoke+刷新,confirm 导入,+27 行) | 若再增长:键盘监听与收件箱动线回调拆至 useNotesPageShortcuts.ts / useInboxFlow.ts |
7171
| app/src/components/AiProviderSettings.tsx | 329 | v0.11.6 M1 code-review 修复(2026-08-22):删除/清钥加 window.confirm、window.prompt 改卡片内联 password 输入(+2 state + 内联表单)、模型列表 input 改 textarea、fallbackOrder 透传 initial、run 置"处理中"反馈、预设双源 presetOptions 后端拉取——修复净增约 13 行越线(实测 329,含 4 行豁免头注释) | 若再增长:内联密钥表单拆至 AiProviderKeyInput.tsx |
7272

7373
> 前端 **拆分中**(Task #9 笔记域修复进行中,暂不登记行数):`app/src/types.ts`——待前端拆分完成后以实测行数重新评估。

0 commit comments

Comments
 (0)