sonamu 0.10.7 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/api/decorators.d.ts.map +1 -1
  2. package/dist/api/decorators.js +5 -12
  3. package/dist/bin/cli.js +10 -253
  4. package/dist/database/base-model.d.ts +1 -0
  5. package/dist/database/base-model.d.ts.map +1 -1
  6. package/dist/database/base-model.js +14 -8
  7. package/dist/database/db.d.ts +2 -1
  8. package/dist/database/db.d.ts.map +1 -1
  9. package/dist/database/db.js +13 -3
  10. package/dist/database/puri-wrapper.d.ts.map +1 -1
  11. package/dist/database/puri-wrapper.js +4 -11
  12. package/dist/database/transaction-context.d.ts +8 -3
  13. package/dist/database/transaction-context.d.ts.map +1 -1
  14. package/dist/database/transaction-context.js +12 -7
  15. package/dist/migration/code-generation.js +2 -2
  16. package/dist/naite/naite-reporter.d.ts +8 -3
  17. package/dist/naite/naite-reporter.d.ts.map +1 -1
  18. package/dist/naite/naite-reporter.js +9 -4
  19. package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
  20. package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
  21. package/dist/ui-web/index.html +2 -2
  22. package/package.json +3 -3
  23. package/src/api/decorators.ts +11 -22
  24. package/src/bin/cli.ts +12 -375
  25. package/src/database/__tests__/transaction-scope.test.ts +227 -0
  26. package/src/database/base-model.ts +31 -6
  27. package/src/database/db.ts +17 -2
  28. package/src/database/puri-wrapper.ts +5 -22
  29. package/src/database/transaction-context.ts +14 -6
  30. package/src/migration/code-generation.ts +1 -1
  31. package/src/naite/naite-reporter.ts +8 -3
  32. package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
  33. package/src/skills/AGENTS.md +0 -108
  34. package/src/skills/sonamu/SKILL.md +0 -75
  35. package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
  36. package/src/skills/sonamu-api/SKILL.md +0 -480
  37. package/src/skills/sonamu-auth/SKILL.md +0 -327
  38. package/src/skills/sonamu-auth/references/plugins.md +0 -310
  39. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
  40. package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
  41. package/src/skills/sonamu-config/SKILL.md +0 -203
  42. package/src/skills/sonamu-config/references/database.md +0 -452
  43. package/src/skills/sonamu-config/references/environments.md +0 -178
  44. package/src/skills/sonamu-config/references/server-options.md +0 -400
  45. package/src/skills/sonamu-entity/SKILL.md +0 -180
  46. package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
  47. package/src/skills/sonamu-entity/references/design-guides.md +0 -243
  48. package/src/skills/sonamu-entity/references/field-types.md +0 -170
  49. package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
  50. package/src/skills/sonamu-entity/references/relations.md +0 -463
  51. package/src/skills/sonamu-entity/references/subset.md +0 -156
  52. package/src/skills/sonamu-fixture/SKILL.md +0 -180
  53. package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
  54. package/src/skills/sonamu-fixture/references/cone.md +0 -298
  55. package/src/skills/sonamu-frontend/SKILL.md +0 -142
  56. package/src/skills/sonamu-frontend/references/components.md +0 -323
  57. package/src/skills/sonamu-frontend/references/examples.md +0 -64
  58. package/src/skills/sonamu-frontend/references/hooks.md +0 -273
  59. package/src/skills/sonamu-frontend/references/runtime.md +0 -165
  60. package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
  61. package/src/skills/sonamu-i18n/SKILL.md +0 -287
  62. package/src/skills/sonamu-migration/SKILL.md +0 -316
  63. package/src/skills/sonamu-naite/SKILL.md +0 -266
  64. package/src/skills/sonamu-query/SKILL.md +0 -48
  65. package/src/skills/sonamu-query/references/model-patterns.md +0 -390
  66. package/src/skills/sonamu-query/references/model.md +0 -366
  67. package/src/skills/sonamu-query/references/puri.md +0 -413
  68. package/src/skills/sonamu-query/references/search.md +0 -238
  69. package/src/skills/sonamu-query/references/upsert.md +0 -324
  70. package/src/skills/sonamu-tasks/SKILL.md +0 -236
  71. package/src/skills/sonamu-testing/SKILL.md +0 -251
  72. package/src/skills/sonamu-testing/references/devrunner.md +0 -405
  73. package/src/skills/sonamu-testing/references/helpers.md +0 -185
  74. package/src/skills/sonamu-testing/references/patterns.md +0 -263
  75. package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
  76. package/src/skills/sonamu-testing/references/quick-start.md +0 -285
  77. package/src/skills/sonamu-testing/references/type-safety.md +0 -172
  78. package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
  79. package/src/skills/sonamu-vector/SKILL.md +0 -222
@@ -1,323 +0,0 @@
1
- # Components
2
-
3
- ## IdAsyncSelect
4
-
5
- Component for asynchronously searching and selecting Entity records. The generic type must be specified according to the Entity's Primary Key type.
6
-
7
- ### Basic Usage
8
-
9
- IdAsyncSelect is typically used as a per-Entity wrapper component:
10
-
11
- ```typescript
12
- import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
13
- import { UserAsyncIdConfig } from "@/services/services.generated";
14
- import type { UserSubsetKey, UserSubsetMapping } from "@/services/sonamu.generated";
15
- import type { UserListParams } from "@/services/user/user.types";
16
-
17
- export type UserIdAsyncSelectProps<T extends UserSubsetKey> = {
18
- subset: T;
19
- baseListParams?: UserListParams;
20
- displayField?: keyof UserSubsetMapping[T] & string;
21
- valueField?: keyof UserSubsetMapping[T] & string;
22
- placeholder?: string;
23
- clearable?: boolean;
24
- disabled?: boolean;
25
- className?: string;
26
- multiple?: boolean;
27
- value?: number | number[] | null; // Number PK
28
- onValueChange?: (value: number | number[] | undefined) => void;
29
- };
30
-
31
- export function UserIdAsyncSelect<T extends UserSubsetKey>({
32
- subset,
33
- value,
34
- onValueChange,
35
- baseListParams,
36
- displayField = "name",
37
- valueField = "id",
38
- placeholder = "User",
39
- clearable,
40
- disabled,
41
- className,
42
- multiple = false,
43
- }: UserIdAsyncSelectProps<T>) {
44
- return (
45
- <IdAsyncSelect<number> // Number PK
46
- config={UserAsyncIdConfig}
47
- subset={subset}
48
- baseListParams={baseListParams}
49
- displayField={displayField}
50
- valueField={valueField}
51
- placeholder={placeholder}
52
- clearable={clearable}
53
- disabled={disabled}
54
- className={className}
55
- multiple={multiple}
56
- value={value}
57
- onValueChange={onValueChange}
58
- />
59
- );
60
- }
61
- ```
62
-
63
- **Key Props:**
64
-
65
- - `config`: Auto-generated AsyncIdConfig (EntityAsyncIdConfig form)
66
- - `subset`: Subset key to query
67
- - `baseListParams`: List filter parameters
68
- - `displayField`: Field name to display (default varies by Entity)
69
- - `valueField`: Field name used as value (default: "id")
70
- - `multiple`: Whether multi-selection is enabled
71
- - `value`: Currently selected value (number or string depending on PK type)
72
- - `onValueChange`: Value change handler
73
-
74
- ### Cascade Dropdown Pattern (Hierarchical Selection)
75
-
76
- When lower-level lists should change based on higher-level selection (e.g. Department → Division → Lab), pass `baseListParams` dynamically.
77
-
78
- **Key behavior**: When the `baseListParams` prop changes, the React Query inside `IdAsyncSelect` automatically re-fetches with the new parameters. (Bug fixed in v0.2.5+ — previous versions only used the initial value and did not reflect changes)
79
-
80
- ```tsx
81
- // Example: 3-level cascade Department → Division → Lab
82
- function UserForm() {
83
- const { form, register, setForm } = useTypeForm(UserSaveParams, {
84
- dept_id: null,
85
- division_id: null,
86
- lab_id: null,
87
- });
88
-
89
- return (
90
- <form>
91
- {/* Level 1: Department selection (full list → preload or default IdAsyncSelect) */}
92
- <DepartmentIdAsyncSelect
93
- subset="A"
94
- {...register("dept_id")}
95
- onValueChange={(v) => {
96
- // Reset lower values when department changes
97
- setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
98
- }}
99
- />
100
-
101
- {/* Level 2: Division selection (only divisions in selected department) */}
102
- <DivisionIdAsyncSelect
103
- subset="A"
104
- baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
105
- disabled={!form.dept_id}
106
- {...register("division_id")}
107
- onValueChange={(v) => {
108
- // Reset lab when division changes
109
- setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
110
- }}
111
- />
112
-
113
- {/* Level 3: Lab selection (only labs in selected division) */}
114
- <LabIdAsyncSelect
115
- subset="A"
116
- baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
117
- disabled={!form.division_id}
118
- {...register("lab_id")}
119
- />
120
- </form>
121
- );
122
- }
123
- ```
124
-
125
- **Notes**:
126
-
127
- - You must explicitly reset lower values to `null` when a higher-level value changes. IdAsyncSelect does not reset automatically.
128
- - Using the `disabled` prop to disable lower levels when the parent is not selected improves UX.
129
- - If `baseListParams` is `undefined`, IdAsyncSelect stays in enabled=false state and does not fetch.
130
-
131
- **Items to specify in Spec** (recommended to add to acceptanceCriteria in spec.json when cascade is present):
132
-
133
- ```json
134
- "acceptanceCriteria": [
135
- "When a department is selected, only divisions belonging to that department appear in the dropdown",
136
- "When a division is selected, only labs belonging to that division appear in the dropdown",
137
- "When the department changes, the lower division/lab selections are reset"
138
- ]
139
- ```
140
-
141
- ### IMPORTANT: String Primary Key Support
142
-
143
- Most Entities use Number PK (`IdAsyncSelect<number>`), but better-auth related Entities use String PK.
144
-
145
- **String PK Entities**: User, Account, Session, Verification
146
-
147
- **Points to change** (manual modification required after scaffolding):
148
-
149
- ```typescript
150
- // Number PK (default)
151
- value?: number | number[] | null;
152
- onValueChange?: (value: number | number[] | undefined) => void;
153
- <IdAsyncSelect<number> config={PostAsyncIdConfig} ... />
154
-
155
- // String PK (User, Account, etc.) — change all 3 places to string
156
- value?: string | string[] | null;
157
- onValueChange?: (value: string | string[] | undefined) => void;
158
- <IdAsyncSelect<string> config={AccountAsyncIdConfig} ... />
159
- ```
160
-
161
- ### Usage in Forms
162
-
163
- ```tsx
164
- function PostForm() {
165
- const { form, setForm, register } = useTypeForm(PostSaveParams, {
166
- title: "",
167
- author_id: 0, // or "" for string PK
168
- });
169
-
170
- return (
171
- <form>
172
- <Input {...register("title")} />
173
-
174
- {/* Number PK */}
175
- <UserIdAsyncSelect subset="A" {...register("author_id")} />
176
-
177
- {/* String PK */}
178
- <AccountIdAsyncSelect subset="A" {...register("account_id")} />
179
- </form>
180
- );
181
- }
182
- ```
183
-
184
- ## FileInput
185
-
186
- File upload component (image/general files, eager/lazy modes)
187
-
188
- ```typescript
189
- import { FileInput } from "@sonamu-kit/react-components/components";
190
- import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
191
-
192
- function ProfileForm() {
193
- const { form, setForm, register, submit } = useTypeForm(ProfileSaveParams, {
194
- avatar: null, // SonamuFile | File | null
195
- documents: [], // (SonamuFile | File)[]
196
- });
197
-
198
- return (
199
- <form>
200
- {/* Single image - eager upload */}
201
- <FileInput
202
- {...register("avatar")}
203
- uploadMode="eager"
204
- viewMode="image"
205
- placeholder="Profile image"
206
- accept="image/*"
207
- previewSize="md"
208
- />
209
-
210
- {/* Multiple files - lazy upload */}
211
- <FileInput
212
- {...register("documents")}
213
- uploadMode="lazy"
214
- viewMode="file"
215
- multiple
216
- maxFiles={5}
217
- placeholder="Attach documents"
218
- />
219
-
220
- <button onClick={submit(async (form) => {
221
- // lazy mode: auto-uploads on submit
222
- await ProfileService.save([form]);
223
- })}>Save</button>
224
- </form>
225
- );
226
- }
227
- ```
228
-
229
- **Props:**
230
-
231
- - `uploadMode`: `"eager"` (upload immediately) | `"lazy"` (upload on submit)
232
- - `viewMode`: `"image"` (image preview) | `"file"` (filename)
233
- - `multiple`: Whether multiple files can be selected
234
- - `maxFiles`: Maximum number of files
235
- - `previewSize`: `"sm" | "md" | "lg" | "xl"`
236
- - `clearable`: Whether the X button can remove the file
237
-
238
- **IMPORTANT**: uploader function must be configured in SonamuProvider (see below)
239
-
240
- ## Select (Multi-select Mode)
241
-
242
- Setting `multiple: true` on the `Select` component enables multi-select mode.
243
-
244
- ```typescript
245
- import { Select } from "@sonamu-kit/react-components/components";
246
-
247
- function TagForm() {
248
- const { register } = useTypeForm(PostSaveParams, {
249
- tag_ids: [], // number[]
250
- });
251
-
252
- const items = [
253
- { value: 1, label: "JavaScript" },
254
- { value: 2, label: "TypeScript" },
255
- { value: 3, label: "React" },
256
- { value: 4, label: "Vue" },
257
- ];
258
-
259
- return (
260
- <Select
261
- {...register("tag_ids")}
262
- items={items}
263
- multiple
264
- placeholder="Select tags"
265
- />
266
- );
267
- }
268
- ```
269
-
270
- **Multi-select specific Props:**
271
-
272
- - `multiple`: `true` (enables multi-select)
273
- - `maxCount`: Maximum number of badges to display
274
- - `hideSelectAll`: Hide the select all button
275
- - `searchable`: Enable search input
276
-
277
- **Common Props:**
278
-
279
- - `items`: `SelectItemDef[]` (values only or `{ value, label, disabled }` form)
280
- - `placeholder`: Text shown before selection
281
- - `clearable`: X button to deselect all
282
- - `disabled`: Disable the component
283
- - `renderItem`: Custom render function
284
- - `async`: When set to `true`, supports async search via `onSearch` callback
285
-
286
- ## EnumSelect
287
-
288
- Select integrated with Zod enum (label mapping)
289
-
290
- ```typescript
291
- import { EnumSelect } from "@sonamu-kit/react-components/components";
292
- import { z } from "zod";
293
-
294
- const StatusEnum = z.enum(["draft", "published", "archived"]);
295
-
296
- const statusLabels = {
297
- draft: "Draft",
298
- published: "Published",
299
- archived: "Archived",
300
- } as const;
301
-
302
- function PostForm() {
303
- const { register } = useTypeForm(PostSaveParams, {
304
- status: "draft",
305
- });
306
-
307
- return (
308
- <EnumSelect
309
- {...register("status")}
310
- enum={StatusEnum}
311
- labels={statusLabels}
312
- placeholder="Select status"
313
- clearable
314
- />
315
- );
316
- }
317
- ```
318
-
319
- **Key points:**
320
-
321
- - Zod enum type safety
322
- - Display name mapping via labels object
323
- - Automatically converts enum.options to items
@@ -1,64 +0,0 @@
1
- # Full Component Implementation Examples
2
-
3
- ## Full Component Implementation Examples
4
-
5
- ### List Page
6
-
7
- ```typescript
8
- function ConsultationListPage() {
9
- const [params, setParams] = useState({ num: 20, page: 1 });
10
- const { data, isLoading } = ConsultationService.useConsultations("P", params);
11
-
12
- return (
13
- <div>
14
- {/* Manage selection with useSelection */}
15
- {data?.rows.map((row) => (
16
- <div key={row.id}>{row.title} - {row.status}</div>
17
- ))}
18
- {/* Pagination: manipulate params.page */}
19
- </div>
20
- );
21
- }
22
- ```
23
-
24
- ### Edit Page
25
-
26
- ```typescript
27
- function ConsultationFormPage() {
28
- const { id } = useParams();
29
- const { form, setForm, register, submit } = useTypeForm(ConsultationSaveParams, {
30
- title: "", content: "", status: "pending", user_id: 0,
31
- });
32
-
33
- // Edit mode: load data
34
- useEffect(() => {
35
- if (id) ConsultationService.getConsultation("A", Number(id)).then((row) => setForm((prev) => ({ ...prev, ...row })));
36
- }, [id]);
37
-
38
- const saveMutation = ConsultationService.useSaveMutation();
39
- const handleSubmit = submit(async (form) => {
40
- const [cId] = await saveMutation.mutateAsync({ spa: [form] });
41
- navigate(`/consultations/${cId}`);
42
- });
43
-
44
- return (
45
- <form>
46
- <Input {...register("title")} />
47
- <Textarea {...register("content")} />
48
- <Select {...register("status")} items={[{value:"pending",label:"Pending"},{value:"completed",label:"Completed"}]} />
49
- <button onClick={handleSubmit} disabled={saveMutation.isPending}>Save</button>
50
- </form>
51
- );
52
- }
53
- ```
54
-
55
- ### Cache Invalidation
56
-
57
- ```typescript
58
- const queryClient = useQueryClient();
59
- await ConsultationService.changeStatus(id, newStatus, "Status change");
60
- queryClient.invalidateQueries({
61
- queryKey: ["Consultation", "findById", "A", id],
62
- });
63
- queryClient.invalidateQueries({ queryKey: ["Consultation", "findMany"] });
64
- ```
@@ -1,273 +0,0 @@
1
- # Form and List Hooks
2
-
3
- ## useTypeForm
4
-
5
- Type-safe form management based on Zod schemas (react-components v0.1.8+)
6
-
7
- ### Return Values
8
-
9
- ```typescript
10
- const { form, setForm, register, submit, addError, removeError, clearError, reset } = useTypeForm(
11
- Schema,
12
- defaultValue,
13
- );
14
- ```
15
-
16
- | Return Value | Type | Description |
17
- | ------------- | --------------------------------------------------- | --------------------------------- |
18
- | `form` | `z.infer<Schema>` | Current form data |
19
- | `setForm` | `React.Dispatch<SetStateAction<...>>` | Form state update function |
20
- | `register` | `(field: string) => RegisterReturn` | Field registration function |
21
- | `submit` | `(callback) => () => Promise<R>` | Submit handler factory |
22
- | `addError` | `(path: string, error: string \| ErrorObj) => void` | Manually add an error |
23
- | `removeError` | `(path: string) => void` | Remove error for a specific field |
24
- | `clearError` | `() => void` | Clear all errors |
25
- | `reset` | `() => void` | Reset form to defaultValue |
26
-
27
- ### register Return Object
28
-
29
- ```typescript
30
- register(fieldName) // Returns:
31
- {
32
- value: any, // Current field value
33
- onValueChange: (value: any) => void, // Value change handler
34
- error?: { content: string } // Error object (if present)
35
- }
36
- ```
37
-
38
- ### Basic Usage
39
-
40
- ```tsx
41
- import { useTypeForm } from "@sonamu-kit/react-components/lib";
42
- import { Input } from "@sonamu-kit/react-components/components";
43
- import { UserSaveParams } from "@/services/user/user.types";
44
-
45
- function RegisterForm() {
46
- const { form, setForm, register, submit } = useTypeForm(UserSaveParams, {
47
- email: "",
48
- username: "",
49
- password: "",
50
- });
51
-
52
- const handleSubmit = submit(async (form) => {
53
- await UserService.save([form]);
54
- });
55
-
56
- // Method 1: spread operator (recommended)
57
- const emailProps = register("email");
58
-
59
- return (
60
- <form>
61
- <Input {...emailProps} />
62
- {emailProps.error && <span className="error">{emailProps.error.content}</span>}
63
-
64
- {/* Method 2: inline (for short cases) */}
65
- <Input {...register("username")} />
66
- {register("username").error && (
67
- <span className="error">{register("username").error.content}</span>
68
- )}
69
-
70
- <button onClick={handleSubmit}>Register</button>
71
- </form>
72
- );
73
- }
74
- ```
75
-
76
- ### IMPORTANT: react-components UI Component Usage
77
-
78
- All UI components in react-components follow the `value/onValueChange` pattern:
79
-
80
- ```tsx
81
- import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/components";
82
-
83
- // Input (string)
84
- <Input {...register("email")} />
85
-
86
- // Textarea (string)
87
- <Textarea {...register("content")} />
88
-
89
- // Checkbox (boolean)
90
- <Checkbox {...register("agreed")} />
91
-
92
- // Select (using items prop)
93
- <Select
94
- {...register("status")}
95
- items={[
96
- { value: "active", label: "Active" },
97
- { value: "inactive", label: "Inactive" }
98
- ]}
99
- placeholder="Select status"
100
- />
101
-
102
- // Select simple form (string[] | number[])
103
- <Select
104
- {...register("priority")}
105
- items={["high", "medium", "low"]}
106
- placeholder="Priority"
107
- />
108
- ```
109
-
110
- **Select component key props:**
111
-
112
- - `items`: Array of selectable items (`V[]` or `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
113
- - `placeholder`: Text shown before selection
114
- - `clearable`: Whether the X button can deselect
115
- - `renderItem`: Custom render function
116
-
117
- ### IMPORTANT: Form Required Field Initial Values
118
-
119
- Fields defined as required in SaveParams **must be included** in the form initial values:
120
-
121
- | Type | Initial Value |
122
- | ----------------- | ------------------------------ |
123
- | string (required) | `""` |
124
- | number (required) | `0` |
125
- | Date (required) | `new Date()` |
126
- | enum (required) | Default value (e.g. `"draft"`) |
127
- | FK (required) | `0` |
128
- | nullable | `null` |
129
-
130
- ```typescript
131
- const { form, setForm, register } = useTypeForm(TaskSaveParams, {
132
- title: "", // string required
133
- status: "draft", // enum required
134
- budget: 0, // number required
135
- begin_date: new Date(), // Date required
136
- description: null, // nullable
137
- institution_id: 0, // FK required
138
- });
139
- ```
140
-
141
- ### IMPORTANT: Accessing Relation Objects When Loading Data
142
-
143
- If a scaffolded form accesses relation objects like `row.collection?.id`, that relation must be included in subset A.
144
-
145
- **Error**: `Property 'collection' does not exist on type` → Add `"collection.id"` to subset A in entity.json
146
-
147
- ```json
148
- // question.entity.json > subsets > A
149
- ["id", "content", "collection.id", "collection.title", "parent.id", "answer_group.id"]
150
- ```
151
-
152
- **Alternative**: If the FK is already on `row`, using `...row` alone is sufficient without accessing the relation (no subset modification needed)
153
-
154
- ### IMPORTANT: SD() Translation Key for FK Fields
155
-
156
- Scaffolded forms use `SD("entity.Task.institution_id")`, but `sd.generated.ts` only generates keys without `_id`.
157
-
158
- **Fix**: Manually add the `_id` key to `ko.ts`
159
-
160
- ```typescript
161
- // packages/api/src/i18n/ko.ts
162
- "entity.Task.institution_id": "Institution",
163
- "entity.Question.collection_id": "Collection",
164
- ```
165
-
166
- Since `ko.ts` is copied from api → web, you only need to add it once.
167
-
168
- ## useListParams
169
-
170
- List parameter management synchronized with URL query parameters (pagination, filtering)
171
-
172
- ```typescript
173
- import { useListParams } from "@sonamu-kit/react-components/lib";
174
- import { z } from "zod";
175
-
176
- const ListParamsSchema = z.object({
177
- page: z.coerce.number().default(1),
178
- num: z.coerce.number().default(20),
179
- search: z.string().optional(),
180
- status: z.enum(["active", "inactive"]).optional(),
181
- });
182
-
183
- function UserListPage() {
184
- const { listParams, setListParams, register } = useListParams(
185
- ListParamsSchema,
186
- { page: 1, num: 20 }
187
- );
188
-
189
- const { data } = UserService.useUsers("P", listParams);
190
-
191
- return (
192
- <div>
193
- {/* Search (resets page=1 on change) */}
194
- <Input {...register("search")} placeholder="Search" />
195
-
196
- {/* Filter (resets page=1 on change) */}
197
- <Select {...register("status")} items={["active", "inactive"]} />
198
-
199
- {/* Pagination (changes page only) */}
200
- <button onClick={() => setListParams({ ...listParams, page: listParams.page - 1 })}>
201
- Previous
202
- </button>
203
- <span>Page {listParams.page}</span>
204
- <button onClick={() => setListParams({ ...listParams, page: listParams.page + 1 })}>
205
- Next
206
- </button>
207
- </div>
208
- );
209
- }
210
- ```
211
-
212
- **Key points:**
213
-
214
- - Automatically syncs with URL (`?page=2&status=active`)
215
- - `register` automatically resets page to 1 when any field other than page changes
216
- - Type safety guaranteed by Zod schema
217
-
218
- ## useSelection
219
-
220
- Checkbox multi-selection management (supports Shift-click range selection)
221
-
222
- ```typescript
223
- import { useSelection } from "@sonamu-kit/react-components/lib";
224
-
225
- function UserListPage() {
226
- const { data } = UserService.useUsers("P", { num: 20, page: 1 });
227
- const userIds = data?.rows.map(row => row.id) ?? [];
228
-
229
- const {
230
- getSelected,
231
- toggle,
232
- selectedKeys,
233
- selectAll,
234
- deselectAll,
235
- isAllSelected,
236
- handleCheckboxClick
237
- } = useSelection(userIds);
238
-
239
- const handleDelete = async () => {
240
- await UserService.del(selectedKeys);
241
- deselectAll();
242
- };
243
-
244
- return (
245
- <div>
246
- <Checkbox
247
- value={isAllSelected}
248
- onValueChange={isAllSelected ? deselectAll : selectAll}
249
- label="Select all"
250
- />
251
- <button onClick={handleDelete} disabled={selectedKeys.length === 0}>
252
- Delete selected ({selectedKeys.length})
253
- </button>
254
-
255
- {data?.rows.map((user, index) => (
256
- <div key={user.id} onClick={(e) => handleCheckboxClick(e, index)}>
257
- <Checkbox
258
- value={getSelected(user.id)}
259
- onValueChange={() => toggle(user.id)}
260
- />
261
- <span>{user.name}</span>
262
- </div>
263
- ))}
264
- </div>
265
- );
266
- }
267
- ```
268
-
269
- **Key points:**
270
-
271
- - Range selection with Shift + click
272
- - `selectedKeys`: Array of currently selected keys
273
- - `isAllSelected`: Whether all items are selected