Skip to content

Commit 58ffb2c

Browse files
KATO-Hiroclaude
andcommitted
feat: add URL param persistence, admin visibility, and filter fixes for workbooks list
- Restore filter state via nav link click instead of direct URL navigation in E2E test - Pass includeUnpublished flag to getAvailableSolutionCategories for admin users - Fix empty state check in CreatedByUserTable to use visible (readable) count - Add keyed #each blocks in CurriculumWorkBookList and SolutionWorkBookList - Move import to top of kanban.ts to fix declaration order - Update coding-style rule with CodeRabbit Findings exception - Fix code block formatting in svelte-components rule - Add E2E note about avoiding src/ imports in testing rule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0181c5e commit 58ffb2c

11 files changed

Lines changed: 67 additions & 17 deletions

File tree

.claude/rules/coding-style.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ Shared helper functions (used by two or more exports) should be grouped at the e
7575

7676
Write all project documentation (plans, dev-notes, guides, refactor notes) in Japanese. Write all source code comments, TSDoc, commit messages, and test titles in English. This keeps documentation readable for the team while keeping code comments universally accessible and searchable.
7777

78+
**Exception**: The `## CodeRabbit Findings` section in `refactor.md` must quote findings verbatim in their original language (English). Do not translate CodeRabbit output.
79+
7880
### TSDoc
7981

8082
Add TSDoc comments to every exported function, type, and class. The minimum required fields are `@param` (for non-obvious parameters) and `@returns` (when the return value is not evident from the type). One-liner `/** ... */` is sufficient for simple cases; use multi-line only when behavior needs explanation.

.claude/rules/svelte-components.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ When copying button styles from a reference component, always check all three ax
4141

4242
Plain `let` or `const` in Svelte 5 component `<script>` executes once at component creation. Values derived from props or server data must use `$derived()`:
4343

44-
```svelte
45-
// Bad: captures only the initial value — won't update when data reloads let user =
46-
data.loggedInUser; const categories = availableCategories.filter(...); // Good let user =
47-
$derived(data.loggedInUser); let categories = $derived(availableCategories.filter(...));
44+
```md
45+
// Bad: captures only the initial value — won't update when data reloads
46+
let user = data.loggedInUser; const categories = availableCategories.filter(...);
47+
48+
// Good
49+
let user = $derived(data.loggedInUser); let categories = $derived(availableCategories.filter(...));
4850
```
4951

5052
`pnpm check` warns: "This reference only captures the initial value."

.claude/rules/testing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ test.describe('logged-in user', () => {
153153
Playwright has no native `test.each`. Use `for...of` loops — the official recommended pattern:
154154

155155
```typescript
156-
for (const grade of [TaskGrade.Q10, TaskGrade.Q9, TaskGrade.Q8]) {
156+
// Mirrors TaskGrade from $lib/types/task — do not import from src/ in E2E files
157+
const GRADES = ['Q10', 'Q9', 'Q8'] as const;
158+
159+
for (const grade of GRADES) {
157160
await gradeButton(grade).click();
158161
await expect(page).toHaveURL(`?grades=${grade}`);
159162
}

e2e/workbooks_list.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ test.describe('logged-in user (general)', () => {
163163
await page.goto('/');
164164
await expect(page).toHaveURL('/', { timeout: TIMEOUT });
165165

166-
// Return to /workbooks via nav link (no params)
167-
await page.goto(WORKBOOK_LIST_URL);
166+
// Return to /workbooks by clicking the nav link (no params)
167+
await page.getByRole('link', { name: '問題集', exact: true }).click();
168168

169169
// URL should be restored to the saved filter state
170170
await expect(page).toHaveURL(new RegExp(`tab=${TAB_SOLUTION}`), { timeout: TIMEOUT });

src/features/workbooks/components/list/CreatedByUserTable.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
import { getTaskResult } from '$features/workbooks/utils/workbooks';
2222
2323
let { workbooks, userId, role, taskResults }: SolutionTableProps = $props();
24+
25+
let visibleCount = $derived(
26+
workbooks.filter((workbook) => canRead(workbook.isPublished, userId, workbook.authorId)).length,
27+
);
2428
</script>
2529

26-
{#if workbooks.length === 0}
30+
{#if visibleCount === 0}
2731
<EmptyWorkbookList />
2832
{:else}
2933
<div class="overflow-auto rounded-md border border-gray-200 dark:border-gray-100">

src/features/workbooks/components/list/CurriculumWorkBookList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<div class="mb-6">
7373
<div class="flex items-center space-x-4">
7474
<div class="flex flex-wrap gap-1">
75-
{#each AVAILABLE_GRADES as grade}
75+
{#each AVAILABLE_GRADES as grade (grade)}
7676
<Button
7777
onclick={() => filterByGradeMode(grade)}
7878
color="alternative"

src/features/workbooks/components/list/SolutionWorkBookList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
</script>
4848

4949
<div class="mb-6 flex flex-wrap gap-1">
50-
{#each AVAILABLE_CATEGORIES as category}
50+
{#each AVAILABLE_CATEGORIES as category (category)}
5151
<Button
5252
onclick={() => onCategoryChange(category)}
5353
color="alternative"

src/features/workbooks/services/workbooks.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,37 @@ describe('getAvailableSolutionCategories', () => {
267267

268268
expect(result).toEqual([SolutionCategory.GRAPH]);
269269
});
270+
271+
test('passes isPublished: true filter by default', async () => {
272+
vi.mocked(prisma.workBookPlacement.findMany).mockResolvedValue([]);
273+
274+
await getAvailableSolutionCategories();
275+
276+
expect(prisma.workBookPlacement.findMany).toHaveBeenCalledWith(
277+
expect.objectContaining({
278+
where: expect.objectContaining({
279+
workBook: expect.objectContaining({ isPublished: true }),
280+
}),
281+
}),
282+
);
283+
});
284+
285+
test('omits isPublished filter when includeUnpublished is true', async () => {
286+
vi.mocked(prisma.workBookPlacement.findMany).mockResolvedValue([
287+
{ solutionCategory: SolutionCategory.GRAPH },
288+
] as unknown as Awaited<ReturnType<typeof prisma.workBookPlacement.findMany>>);
289+
290+
const result = await getAvailableSolutionCategories(true);
291+
292+
expect(result).toEqual([SolutionCategory.GRAPH]);
293+
expect(prisma.workBookPlacement.findMany).toHaveBeenCalledWith(
294+
expect.objectContaining({
295+
where: expect.objectContaining({
296+
workBook: expect.not.objectContaining({ isPublished: expect.anything() }),
297+
}),
298+
}),
299+
);
300+
});
270301
});
271302

272303
describe('getWorkBook', () => {

src/features/workbooks/services/workbooks.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,20 @@ export async function getWorkBooksCreatedByUsers(): Promise<WorkbooksWithAuthors
120120
}
121121

122122
/**
123-
* Returns the list of SolutionCategory values that have at least one published
124-
* SOLUTION workbook with a placement record.
123+
* Returns the list of SolutionCategory values that have at least one SOLUTION
124+
* workbook with a placement record.
125+
*
126+
* @param includeUnpublished - When true, includes categories from unpublished workbooks. Defaults to false.
125127
*/
126-
export async function getAvailableSolutionCategories(): Promise<SolutionCategories> {
128+
export async function getAvailableSolutionCategories(
129+
includeUnpublished = false,
130+
): Promise<SolutionCategories> {
127131
const placements = await db.workBookPlacement.findMany({
128132
where: {
129-
workBook: { isPublished: true, workBookType: WorkBookTypeConst.SOLUTION },
133+
workBook: {
134+
...(includeUnpublished ? {} : { isPublished: true }),
135+
workBookType: WorkBookTypeConst.SOLUTION,
136+
},
130137
solutionCategory: { not: null },
131138
},
132139
select: { solutionCategory: true },

src/routes/(admin)/workbooks/order/_types/kanban.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { DragDropManager, Draggable, Droppable } from '@dnd-kit/dom';
22
import type { DragDropEvents } from '@dnd-kit/abstract';
3+
import type { WorkBookTab } from '$features/workbooks/types/workbook';
34

45
// DnD event types derived from dnd-kit abstractions
56
type DndEvents = DragDropEvents<Draggable, Droppable, DragDropManager>;
@@ -9,8 +10,6 @@ export type DragEndEventArg = Parameters<DndEvents['dragend']>[0];
910

1011
export type ColumnKey = 'solutionCategory' | 'taskGrade';
1112

12-
import type { WorkBookTab } from '$features/workbooks/types/workbook';
13-
1413
/** Tabs available on the admin order page — excludes CREATED_BY_USER which has no placement config. */
1514
export type ActiveTab = Exclude<WorkBookTab, 'created_by_user'>;
1615

0 commit comments

Comments
 (0)