Skip to content

Commit a21af26

Browse files
committed
fix(notes): add_knowledge_node 契约 nodeType 与编辑完成即时刷新
1 parent 81a83a5 commit a21af26

6 files changed

Lines changed: 119 additions & 12 deletions

File tree

‎app/src/components/KnowledgeSystemWizard.test.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ describe("KnowledgeSystemWizard 三步校验", () => {
106106
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("create_knowledge_system", {
107107
name: "全局体系", kind: "global", coreQuestion: "如何练好化妆",
108108
}));
109-
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, type: "domain_entry", text: "化妆" }));
110-
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, type: "domain_entry", text: "打光" }));
111-
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, type: "scenario", text: "画好一个眼影" }));
109+
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, nodeType: "domain_entry", text: "化妆" }));
110+
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, nodeType: "domain_entry", text: "打光" }));
111+
await waitFor(() => expect(invokeMock).toHaveBeenCalledWith("add_knowledge_node", { systemId: 1, nodeType: "scenario", text: "画好一个眼影" }));
112112

113113
// 顺序契约:create 在 add 之前
114114
const createIdx = invokeMock.mock.calls.findIndex((c) => c[0] === "create_knowledge_system");

‎app/src/components/KnowledgeSystemWizard.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export default function KnowledgeSystemWizard({ onClose, onCreated }: Props) {
102102
});
103103
// ② 非空领域入口逐条落节点(已去重;顺序即输入顺序)
104104
for (const text of filledEntries) {
105-
await invoke("add_knowledge_node", { systemId: system.id, type: "domain_entry" satisfies KnowledgeNodeType, text });
105+
await invoke("add_knowledge_node", { systemId: system.id, nodeType: "domain_entry" satisfies KnowledgeNodeType, text });
106106
}
107107
// ③ 第一条输出(若有)落为「场景」节点(挂全局体系,根节点)
108108
if (firstOutput.trim()) {
109-
await invoke("add_knowledge_node", { systemId: system.id, type: "scenario" satisfies KnowledgeNodeType, text: firstOutput.trim() });
109+
await invoke("add_knowledge_node", { systemId: system.id, nodeType: "scenario" satisfies KnowledgeNodeType, text: firstOutput.trim() });
110110
}
111111
onCreated(system);
112112
} catch (e) {

‎app/src/components/KnowledgeTreeView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default function KnowledgeTreeView({ systemId, nodes, links, selectedNode
9797
const confirmAdd = async () => {
9898
if (!addDraft || !addText.trim()) return;
9999
try {
100-
await invoke("add_knowledge_node", { systemId, parentId: addDraft.parentId, type: addDraft.type, text: addText.trim() });
100+
await invoke("add_knowledge_node", { systemId, parentId: addDraft.parentId, nodeType: addDraft.type, text: addText.trim() });
101101
setAddDraft(null);
102102
setAddText("");
103103
onChanged();

‎app/src/components/NoteEditView.tsx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,14 @@ export default function NoteEditView({ note, onCancel }: Props) {
216216
}
217217
};
218218

219-
// 完成编辑(保存草稿后退出——v0.10.1 F4:原「取消」语义矛盾,改名诚实化)
220-
const handleDone = () => {
221-
if (dirty) void saveDraft(false);
222-
onCancel();
219+
// 完成编辑(先 await 保存再退出——v0.13.6:原异步不等待,退出后右栏/列表
220+
// 读到旧值;保存失败也退出(status 已展示),不卡死编辑态)
221+
const handleDone = async () => {
222+
try {
223+
if (dirty) await saveDraft(false);
224+
} finally {
225+
onCancel();
226+
}
223227
};
224228

225229
// v0.10.1 F1:卸载时 dirty 自动保存(切笔记/切页不丢编辑内容——
@@ -270,7 +274,7 @@ export default function NoteEditView({ note, onCancel }: Props) {
270274
>
271275
{saving ? "保存中…" : "💾 保存(Ctrl+S)"}
272276
</button>
273-
<button onClick={handleDone} style={{ ...TOOLBAR_BTN, color: "#6b7280" }}>完成(Ctrl+E)</button>
277+
<button onClick={() => void handleDone()} style={{ ...TOOLBAR_BTN, color: "#6b7280" }}>完成(Ctrl+E)</button>
274278
</div>
275279

276280
{/* 标题 */}

‎app/src/pages/NotesPage.test.tsx‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* NotesPage.test.tsx — 笔记编辑完成即时刷新测试(v0.13.6 REQ-223 M1-B)。
4+
*
5+
* @ai-context: 覆盖用户实测 P0——编辑标题「完成」后右栏阅读视图仍显示旧标题。
6+
* 根因:保存未 await + selected 不重取。断言 invoke 顺序契约:
7+
* update_note → get_note,且右栏 h2 呈现新标题(刷新回填生效)。
8+
*/
9+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
11+
import type { Note } from "../types";
12+
13+
const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() }));
14+
const { confirmMock } = vi.hoisted(() => ({ confirmMock: vi.fn() }));
15+
vi.mock("@tauri-apps/api/core", () => ({ invoke: invokeMock }));
16+
vi.mock("@tauri-apps/plugin-dialog", () => ({ confirm: confirmMock }));
17+
// EnrichPanel 挂载期 useAiTaskPolling 会 listen 事件——mock 返回解绑函数
18+
vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn(() => Promise.resolve(() => {})) }));
19+
20+
import NotesPage from "./NotesPage";
21+
22+
const baseNote = (title: string): Note => ({
23+
id: 1,
24+
title,
25+
content: "# 正文\n内容段落",
26+
source: "session",
27+
session_id: 42,
28+
rule_version: null,
29+
purify_stats: null,
30+
tags: "[]",
31+
properties: null,
32+
pin: 0,
33+
group_id: null,
34+
created_at: 1,
35+
updated_at: 2,
36+
});
37+
38+
describe("NotesPage 编辑完成即时刷新", () => {
39+
beforeEach(() => {
40+
invokeMock.mockReset();
41+
confirmMock.mockReset();
42+
// 模拟库内状态:初始"旧标题",update_note 后 get_note 返回"新标题"
43+
let store: Note = baseNote("旧标题");
44+
invokeMock.mockImplementation(async (cmd: string, args?: Record<string, unknown>) => {
45+
switch (cmd) {
46+
case "list_notes":
47+
return [store];
48+
case "list_note_groups":
49+
return [];
50+
case "count_due_cards":
51+
return 0;
52+
case "list_fragments":
53+
return [];
54+
case "get_feature_flags":
55+
return { feedCapture: true };
56+
case "update_note":
57+
store = { ...store, title: args?.title as string };
58+
return true;
59+
case "get_note":
60+
return store;
61+
default:
62+
// 子面板(EnrichPanel/VersionPanel 等)挂载期查询——空值兜底不抛错
63+
if (cmd === "note_versions_list") return [];
64+
if (cmd === "note_versions_usage") return [];
65+
return null;
66+
}
67+
});
68+
});
69+
70+
afterEach(() => cleanup());
71+
72+
it("编辑标题→完成:update_note 先于 get_note,右栏立即显示新标题", async () => {
73+
render(<NotesPage />);
74+
// 列表出现并选中笔记(点击列表行)
75+
const rowTitle = await screen.findByText("旧标题");
76+
fireEvent.click(rowTitle);
77+
await waitFor(() => expect(screen.getByRole("heading", { name: "旧标题" })).toBeTruthy());
78+
79+
// 进入编辑 → 改标题 → 完成
80+
fireEvent.click(screen.getByRole("button", { name: /编辑/ }));
81+
const titleInput = await screen.findByPlaceholderText("笔记标题");
82+
fireEvent.change(titleInput, { target: { value: "新标题" } });
83+
fireEvent.click(screen.getByRole("button", { name: /完成/ }));
84+
85+
// 断言:update_note 先发生,随后 get_note 回填——右栏 h2 呈现新标题
86+
await waitFor(() => {
87+
const calls = invokeMock.mock.calls.filter((c) => c[0] === "update_note" || c[0] === "get_note");
88+
const up = calls.findIndex((c) => c[0] === "update_note");
89+
const get = calls.findIndex((c) => c[0] === "get_note");
90+
expect(up).toBeGreaterThanOrEqual(0);
91+
expect(get).toBeGreaterThan(up);
92+
});
93+
await waitFor(() => expect(screen.getByRole("heading", { name: "新标题" })).toBeTruthy());
94+
});
95+
});

‎app/src/pages/NotesPage.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
133133
setEditing(true);
134134
} else if (e.key === "Escape" && editingRef.current) {
135135
e.preventDefault();
136+
// v0.13.6:ESC 退出编辑同样刷新(列表 + 选中笔记)——原实现连列表都不刷
136137
setEditing(false);
138+
void handleNoteChangedRef.current();
137139
}
138140
};
139141
window.addEventListener("keydown", handler);
@@ -216,6 +218,11 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
216218
}
217219
}, [keyword, tagFilter, sortMode, load]);
218220

221+
// v0.13.6:编辑退出三出口(完成/Ctrl+E/ESC)统一刷新——ESC 是 []-deps 窗口监听,
222+
// 经 ref 取最新 handleNoteChanged(防闭包持有旧 keyword/tagFilter 快照)
223+
const handleNoteChangedRef = useRef(handleNoteChanged);
224+
useEffect(() => { handleNoteChangedRef.current = handleNoteChanged; }, [handleNoteChanged]);
225+
219226
// 新建笔记(v0.12.2 去摩擦:零对话框——新建即编辑;落未归组「全部笔记」可见)
220227
const handleCreate = () => {
221228
invoke<Note>("create_note", { new: { title: "未命名笔记", content: "", source: "manual" } })
@@ -324,8 +331,9 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
324331
key={selected.id}
325332
note={selected}
326333
onCancel={() => {
334+
// v0.13.6:完成编辑 → 列表重载 + 选中笔记重取(右栏立即显示新标题/正文)
327335
setEditing(false);
328-
void load(keyword, tagFilter, sortMode);
336+
void handleNoteChanged();
329337
}}
330338
/>
331339
}

0 commit comments

Comments
 (0)