Skip to content

Commit 1a1ca0d

Browse files
authored
Merge pull request #1218 from getlarge/issue-1211-diary-detail-filter-bar
feat(console): diary detail filter bar + libs/diary-ui shared with MCP diary app
2 parents 7756997 + 4f77135 commit 1a1ca0d

45 files changed

Lines changed: 3000 additions & 690 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/console/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
1818
COPY apps/console/package.json apps/console/
1919
COPY libs/design-system/package.json libs/design-system/
2020
COPY libs/task-ui/package.json libs/task-ui/
21+
COPY libs/diary-ui/package.json libs/diary-ui/
2122
COPY libs/api-client/package.json libs/api-client/
2223
COPY libs/models/package.json libs/models/
2324

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
12
import { beforeEach, describe, expect, it, vi } from 'vitest';
23

4+
import { createTestWrapper } from './test-query-client.js';
5+
36
const mockListDiaries = vi.fn();
47
const mockListDiaryEntries = vi.fn();
58
const mockListDiaryTags = vi.fn();
@@ -8,18 +11,42 @@ vi.mock('@moltnet/api-client', () => ({
811
listDiaries: (...args: unknown[]) => mockListDiaries(...args),
912
listDiaryEntries: (...args: unknown[]) => mockListDiaryEntries(...args),
1013
listDiaryTags: (...args: unknown[]) => mockListDiaryTags(...args),
11-
getDiary: vi.fn(),
12-
getDiaryEntryById: vi.fn(),
13-
verifyDiaryEntryById: vi.fn(),
14+
// Generated *Options helpers from @moltnet/api-client/query are imported
15+
// into hooks.ts but not exercised by useDiarySummaries — stubs are enough
16+
// to avoid the resolver erroring.
17+
searchDiary: vi.fn(),
18+
}));
19+
20+
vi.mock('@moltnet/api-client/query', () => ({
21+
getDiaryOptions: vi.fn(() => ({
22+
queryKey: ['getDiary'],
23+
queryFn: vi.fn(),
24+
})),
25+
getDiaryEntryByIdOptions: vi.fn(() => ({
26+
queryKey: ['getDiaryEntryById'],
27+
queryFn: vi.fn(),
28+
})),
29+
listDiaryEntriesInfiniteOptions: vi.fn(() => ({
30+
queryKey: ['listDiaryEntries'],
31+
queryFn: vi.fn(),
32+
})),
33+
listDiaryTagsOptions: vi.fn(() => ({
34+
queryKey: ['listDiaryTags'],
35+
queryFn: vi.fn(),
36+
})),
37+
verifyDiaryEntryByIdOptions: vi.fn(() => ({
38+
queryKey: ['verifyDiaryEntryById'],
39+
queryFn: vi.fn(),
40+
})),
1441
}));
1542

1643
vi.mock('../src/api.js', () => ({
1744
getApiClient: () => ({}),
1845
}));
1946

20-
import { fetchDiarySummaries } from '../src/diaries/api.js';
47+
import { useDiarySummaries } from '../src/diaries/hooks.js';
2148

22-
describe('fetchDiarySummaries', () => {
49+
describe('useDiarySummaries', () => {
2350
beforeEach(() => {
2451
vi.clearAllMocks();
2552
mockListDiaryEntries.mockResolvedValue({
@@ -33,7 +60,13 @@ describe('fetchDiarySummaries', () => {
3360
it('forwards x-moltnet-team-id header when teamId is provided', async () => {
3461
mockListDiaries.mockResolvedValue({ data: { items: [] } });
3562

36-
await fetchDiarySummaries('team-alpha');
63+
const { result } = renderHook(() => useDiarySummaries('team-alpha'), {
64+
wrapper: createTestWrapper(),
65+
});
66+
67+
await waitFor(() => {
68+
expect(result.current.isSuccess).toBe(true);
69+
});
3770

3871
expect(mockListDiaries).toHaveBeenCalledWith(
3972
expect.objectContaining({
@@ -45,20 +78,94 @@ describe('fetchDiarySummaries', () => {
4578
it('omits the team header when teamId is null', async () => {
4679
mockListDiaries.mockResolvedValue({ data: { items: [] } });
4780

48-
await fetchDiarySummaries(null);
81+
const { result } = renderHook(() => useDiarySummaries(null), {
82+
wrapper: createTestWrapper(),
83+
});
84+
85+
await waitFor(() => {
86+
expect(result.current.isSuccess).toBe(true);
87+
});
4988

5089
expect(mockListDiaries).toHaveBeenCalledWith(
5190
expect.objectContaining({ headers: undefined }),
5291
);
5392
});
5493

55-
it('omits the team header when teamId is undefined', async () => {
56-
mockListDiaries.mockResolvedValue({ data: { items: [] } });
94+
it('aggregates entryCount, tagCount, and latestEntryAt per diary', async () => {
95+
mockListDiaries.mockResolvedValue({
96+
data: {
97+
items: [
98+
{
99+
id: 'd1',
100+
name: 'b-diary',
101+
visibility: 'private',
102+
teamId: 't1',
103+
},
104+
{
105+
id: 'd2',
106+
name: 'a-diary',
107+
visibility: 'private',
108+
teamId: 't1',
109+
},
110+
],
111+
},
112+
});
57113

58-
await fetchDiarySummaries();
114+
mockListDiaryEntries.mockImplementation(
115+
({ path }: { path: { diaryId: string } }) => {
116+
if (path.diaryId === 'd1') {
117+
return Promise.resolve({
118+
data: {
119+
items: [{ createdAt: '2026-05-22T10:00:00Z' }],
120+
total: 5,
121+
},
122+
});
123+
}
124+
return Promise.resolve({
125+
data: { items: [], total: 0 },
126+
});
127+
},
128+
);
59129

60-
expect(mockListDiaries).toHaveBeenCalledWith(
61-
expect.objectContaining({ headers: undefined }),
130+
mockListDiaryTags.mockImplementation(
131+
({ path }: { path: { diaryId: string } }) => {
132+
if (path.diaryId === 'd1') {
133+
return Promise.resolve({
134+
data: {
135+
tags: [
136+
{ tag: 'auth', count: 3 },
137+
{ tag: 'db', count: 2 },
138+
],
139+
total: 2,
140+
},
141+
});
142+
}
143+
return Promise.resolve({ data: { tags: [], total: 0 } });
144+
},
62145
);
146+
147+
const { result } = renderHook(() => useDiarySummaries('t1'), {
148+
wrapper: createTestWrapper(),
149+
});
150+
151+
await waitFor(() => {
152+
expect(result.current.isSuccess).toBe(true);
153+
});
154+
155+
const summaries = result.current.data ?? [];
156+
// d1 has the most-recent latestEntryAt → sorts before d2.
157+
expect(summaries.map((s) => s.id)).toEqual(['d1', 'd2']);
158+
expect(summaries[0]).toMatchObject({
159+
id: 'd1',
160+
entryCount: 5,
161+
tagCount: 2,
162+
latestEntryAt: '2026-05-22T10:00:00Z',
163+
});
164+
expect(summaries[1]).toMatchObject({
165+
id: 'd2',
166+
entryCount: 0,
167+
tagCount: 0,
168+
latestEntryAt: null,
169+
});
63170
});
64171
});

apps/console/e2e/diary-browser.e2e.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ test.describe.serial('Diary browser', () => {
154154
await expect(
155155
page.getByRole('heading', { name: seeded.populatedDiaryName }),
156156
).toBeVisible();
157-
await expect(page.getByRole('heading', { name: 'Tags' })).toBeVisible();
157+
// FilterBar replaces the old "Tags" card; the search input is the anchor.
158+
await expect(
159+
page.getByRole('searchbox', { name: /search entries/i }),
160+
).toBeVisible();
158161
await expect(page.getByRole('button', { name: 'Grid' })).toBeVisible();
159162
await expect(page.getByRole('button', { name: 'Timeline' })).toBeVisible();
160163
await expect(page.getByText(seeded.entryTitle)).toBeVisible();
@@ -165,8 +168,10 @@ test.describe.serial('Diary browser', () => {
165168
await page.goto(`${CONSOLE_URL}/diaries/${seeded.populatedDiaryId}`);
166169

167170
await page.getByText(seeded.entryTag).first().click();
171+
// URL contract changed in PR #1218: tag= → tags= (legacy alias still parsed
172+
// on read; serializer always emits tags=).
168173
await expect(page).toHaveURL(
169-
new RegExp(`tag=${encodeURIComponent(seeded.entryTag)}`),
174+
new RegExp(`tags=${encodeURIComponent(seeded.entryTag)}`),
170175
);
171176
await expect(page.getByText(seeded.entryTitle)).toBeVisible();
172177

0 commit comments

Comments
 (0)