feat(store): selectedNotebookId 변경 시 list/counts 자동 refresh
- selectNotebook: set 후 loadByView(view) + refreshMeta() 자동 호출 (inbox/completed/trash view 한정 list 갱신, 모든 view 에서 counts 갱신) - loadInitial / loadByView / refreshMeta: selectedNotebookId 를 listByStatus / countsByStatus 에 notebookId 옵션으로 전달 - tests: selectNotebook→loadByView+refreshMeta 호출 검증, notebookId 전달 검증, review view 에선 listByStatus 미호출 검증 (4케이스 추가) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,10 +136,11 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
// v0.3.8 — IPC 실패 시 loading=true 영구 stuck 방지. catch 로 reset.
|
||||
set({ loading: true });
|
||||
try {
|
||||
const notebookId = get().selectedNotebookId ?? undefined;
|
||||
const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
|
||||
// inbox 탭은 status='active' 만 표시 — loadByView('inbox') 와 동일 path 로 일관성 확보.
|
||||
// listNotes 는 deleted_at IS NULL 만 필터 (= active+completed+archived 혼재) 이라 부정확.
|
||||
inboxApi.listByStatus('active', { limit: 50 }),
|
||||
inboxApi.listByStatus('active', { limit: 50, notebookId }),
|
||||
inboxApi.getContinuity(),
|
||||
inboxApi.getPendingCount(),
|
||||
inboxApi.getOllamaStatus(),
|
||||
@@ -148,7 +149,7 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
inboxApi.listExpired(),
|
||||
inboxApi.getFailedCount(),
|
||||
inboxApi.listRecallCandidate(),
|
||||
inboxApi.countsByStatus(),
|
||||
inboxApi.countsByStatus({ notebookId }),
|
||||
inboxApi.getSettings()
|
||||
]);
|
||||
set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false });
|
||||
@@ -161,6 +162,7 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
},
|
||||
async refreshMeta() {
|
||||
try {
|
||||
const notebookId = get().selectedNotebookId ?? undefined;
|
||||
const [continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
|
||||
inboxApi.getContinuity(),
|
||||
inboxApi.getPendingCount(),
|
||||
@@ -170,7 +172,7 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
inboxApi.listExpired(),
|
||||
inboxApi.getFailedCount(),
|
||||
inboxApi.listRecallCandidate(),
|
||||
inboxApi.countsByStatus(),
|
||||
inboxApi.countsByStatus({ notebookId }),
|
||||
inboxApi.getSettings()
|
||||
]);
|
||||
set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true });
|
||||
@@ -287,8 +289,9 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
// fail 시 빈 배열로 reset 해서 사용자에게 "비어있음" 으로 표시 (혼동 < stale).
|
||||
const status =
|
||||
view === 'trash' ? 'trashed' : view === 'inbox' ? 'active' : view;
|
||||
const notebookId = get().selectedNotebookId ?? undefined;
|
||||
try {
|
||||
const notes = await inboxApi.listByStatus(status, { limit: 200 });
|
||||
const notes = await inboxApi.listByStatus(status, { limit: 200, notebookId });
|
||||
if (view === 'trash') {
|
||||
set({ trashNotes: notes, trashCount: notes.length });
|
||||
} else {
|
||||
@@ -432,6 +435,12 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
},
|
||||
selectNotebook(id) {
|
||||
set({ selectedNotebookId: id });
|
||||
// v0.4 Task 19 — notebook 전환 시 list + counts 자동 갱신.
|
||||
const v = get().view;
|
||||
if (v === 'inbox' || v === 'completed' || v === 'trash') {
|
||||
void get().loadByView(v);
|
||||
}
|
||||
void get().refreshMeta();
|
||||
},
|
||||
async createNotebook(name, color) {
|
||||
const r = await notebookApi.create({ name, color });
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockListByStatus, mockCountsByStatus } = vi.hoisted(() => ({
|
||||
mockListByStatus: vi.fn(async () => []),
|
||||
mockCountsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 }))
|
||||
}));
|
||||
|
||||
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
inboxApi: {
|
||||
listNotes: vi.fn(async () => []),
|
||||
@@ -12,9 +17,9 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
listExpired: vi.fn(async () => []),
|
||||
getFailedCount: vi.fn(async () => 0),
|
||||
listRecallCandidate: vi.fn(async () => null),
|
||||
countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 })),
|
||||
countsByStatus: mockCountsByStatus,
|
||||
getSettings: vi.fn(async () => ({ ai_enabled: true })),
|
||||
listByStatus: vi.fn(async () => [])
|
||||
listByStatus: mockListByStatus
|
||||
},
|
||||
notebookApi: {
|
||||
list: vi.fn(async () => [
|
||||
@@ -33,7 +38,9 @@ import { useInbox } from '../../src/renderer/inbox/store.js';
|
||||
|
||||
describe('store notebooks', () => {
|
||||
beforeEach(() => {
|
||||
useInbox.setState({ notebooks: [], selectedNotebookId: null, sidebarVisible: false, sidebarWidth: 240 } as never);
|
||||
mockListByStatus.mockClear();
|
||||
mockCountsByStatus.mockClear();
|
||||
useInbox.setState({ notebooks: [], selectedNotebookId: null, sidebarVisible: false, sidebarWidth: 240, view: 'inbox' } as never);
|
||||
});
|
||||
|
||||
it('loadNotebooks 가 notebookApi.list 결과 반영', async () => {
|
||||
@@ -74,4 +81,37 @@ describe('store notebooks', () => {
|
||||
expect(useInbox.getState().notebooks.map((n) => n.id)).toEqual(['nb-1']);
|
||||
expect(useInbox.getState().selectedNotebookId).toBe('nb-1');
|
||||
});
|
||||
|
||||
it('selectNotebook 가 loadByView + refreshMeta 호출', async () => {
|
||||
// inbox view 에서 notebook 전환 → listByStatus + countsByStatus 호출됨.
|
||||
useInbox.setState({ view: 'inbox', selectedNotebookId: null } as never);
|
||||
useInbox.getState().selectNotebook('nb-X');
|
||||
expect(useInbox.getState().selectedNotebookId).toBe('nb-X');
|
||||
// 비동기 완료 대기
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(mockListByStatus).toHaveBeenCalled();
|
||||
expect(mockCountsByStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loadByView 가 selectedNotebookId 를 inboxApi.listByStatus 에 전달', async () => {
|
||||
useInbox.setState({ selectedNotebookId: 'nb-X', view: 'inbox' } as never);
|
||||
await useInbox.getState().loadByView('inbox');
|
||||
expect(mockListByStatus).toHaveBeenCalledWith('active', expect.objectContaining({ notebookId: 'nb-X' }));
|
||||
});
|
||||
|
||||
it('refreshMeta 가 selectedNotebookId 를 inboxApi.countsByStatus 에 전달', async () => {
|
||||
useInbox.setState({ selectedNotebookId: 'nb-X' } as never);
|
||||
await useInbox.getState().refreshMeta();
|
||||
expect(mockCountsByStatus).toHaveBeenCalledWith(expect.objectContaining({ notebookId: 'nb-X' }));
|
||||
});
|
||||
|
||||
it('selectNotebook: review view 에선 loadByView 를 호출하지 않음', async () => {
|
||||
useInbox.setState({ view: 'review-weekly', selectedNotebookId: null } as never);
|
||||
useInbox.getState().selectNotebook('nb-X');
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
// listByStatus 는 호출되지 않아야 함 (review view 는 list 대상 아님)
|
||||
expect(mockListByStatus).not.toHaveBeenCalled();
|
||||
// countsByStatus 는 refreshMeta 경로로 호출됨
|
||||
expect(mockCountsByStatus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user