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.
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +5 -12
- package/dist/bin/cli.js +10 -253
- package/dist/database/base-model.d.ts +1 -0
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +14 -8
- package/dist/database/db.d.ts +2 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +13 -3
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +4 -11
- package/dist/database/transaction-context.d.ts +8 -3
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/transaction-context.js +12 -7
- package/dist/migration/code-generation.js +2 -2
- package/dist/naite/naite-reporter.d.ts +8 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +9 -4
- package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
- package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/package.json +3 -3
- package/src/api/decorators.ts +11 -22
- package/src/bin/cli.ts +12 -375
- package/src/database/__tests__/transaction-scope.test.ts +227 -0
- package/src/database/base-model.ts +31 -6
- package/src/database/db.ts +17 -2
- package/src/database/puri-wrapper.ts +5 -22
- package/src/database/transaction-context.ts +14 -6
- package/src/migration/code-generation.ts +1 -1
- package/src/naite/naite-reporter.ts +8 -3
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
- package/src/skills/AGENTS.md +0 -108
- package/src/skills/sonamu/SKILL.md +0 -75
- package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
- package/src/skills/sonamu-api/SKILL.md +0 -480
- package/src/skills/sonamu-auth/SKILL.md +0 -327
- package/src/skills/sonamu-auth/references/plugins.md +0 -310
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
- package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
- package/src/skills/sonamu-config/SKILL.md +0 -203
- package/src/skills/sonamu-config/references/database.md +0 -452
- package/src/skills/sonamu-config/references/environments.md +0 -178
- package/src/skills/sonamu-config/references/server-options.md +0 -400
- package/src/skills/sonamu-entity/SKILL.md +0 -180
- package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
- package/src/skills/sonamu-entity/references/design-guides.md +0 -243
- package/src/skills/sonamu-entity/references/field-types.md +0 -170
- package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
- package/src/skills/sonamu-entity/references/relations.md +0 -463
- package/src/skills/sonamu-entity/references/subset.md +0 -156
- package/src/skills/sonamu-fixture/SKILL.md +0 -180
- package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
- package/src/skills/sonamu-fixture/references/cone.md +0 -298
- package/src/skills/sonamu-frontend/SKILL.md +0 -142
- package/src/skills/sonamu-frontend/references/components.md +0 -323
- package/src/skills/sonamu-frontend/references/examples.md +0 -64
- package/src/skills/sonamu-frontend/references/hooks.md +0 -273
- package/src/skills/sonamu-frontend/references/runtime.md +0 -165
- package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
- package/src/skills/sonamu-i18n/SKILL.md +0 -287
- package/src/skills/sonamu-migration/SKILL.md +0 -316
- package/src/skills/sonamu-naite/SKILL.md +0 -266
- package/src/skills/sonamu-query/SKILL.md +0 -48
- package/src/skills/sonamu-query/references/model-patterns.md +0 -390
- package/src/skills/sonamu-query/references/model.md +0 -366
- package/src/skills/sonamu-query/references/puri.md +0 -413
- package/src/skills/sonamu-query/references/search.md +0 -238
- package/src/skills/sonamu-query/references/upsert.md +0 -324
- package/src/skills/sonamu-tasks/SKILL.md +0 -236
- package/src/skills/sonamu-testing/SKILL.md +0 -251
- package/src/skills/sonamu-testing/references/devrunner.md +0 -405
- package/src/skills/sonamu-testing/references/helpers.md +0 -185
- package/src/skills/sonamu-testing/references/patterns.md +0 -263
- package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
- package/src/skills/sonamu-testing/references/quick-start.md +0 -285
- package/src/skills/sonamu-testing/references/type-safety.md +0 -172
- package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
- 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
|