sonamu 0.8.24 → 0.8.26
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/__tests__/config.test.js +189 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +7 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +14 -10
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/knex-adapter.d.ts +23 -0
- package/dist/auth/knex-adapter.d.ts.map +1 -0
- package/dist/auth/knex-adapter.js +163 -0
- package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
- package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
- package/dist/bin/cli.js +47 -9
- package/dist/bin/ts-loader-register.js +3 -29
- package/dist/bin/ts-loader-registration.d.ts +2 -0
- package/dist/bin/ts-loader-registration.d.ts.map +1 -0
- package/dist/bin/ts-loader-registration.js +42 -0
- package/dist/cone/cone-generator.js +3 -3
- package/dist/database/puri-subset.test-d.js +9 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/testing/fixture-generator.js +5 -5
- package/dist/ui/ai-client.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -14
- package/dist/ui/cdd-service.d.ts +15 -18
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +246 -222
- package/dist/ui/cdd-types.d.ts +41 -68
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +2 -2
- package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
- package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
- package/dist/ui-web/index.html +2 -2
- package/package.json +6 -2
- package/src/api/__tests__/config.test.ts +225 -0
- package/src/api/config.ts +10 -4
- package/src/api/sonamu.ts +16 -13
- package/src/auth/index.ts +1 -0
- package/src/auth/knex-adapter.ts +208 -0
- package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
- package/src/bin/cli.ts +52 -9
- package/src/bin/ts-loader-register.ts +2 -32
- package/src/bin/ts-loader-registration.ts +55 -0
- package/src/cone/cone-generator.ts +2 -2
- package/src/database/puri-subset.test-d.ts +102 -0
- package/src/database/puri-subset.types.ts +1 -1
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +179 -137
- package/src/skills/sonamu/ai-agents.md +69 -69
- package/src/skills/sonamu/api.md +147 -147
- package/src/skills/sonamu/auth-migration.md +220 -220
- package/src/skills/sonamu/auth-plugins.md +83 -83
- package/src/skills/sonamu/auth.md +106 -106
- package/src/skills/sonamu/cdd.md +65 -200
- package/src/skills/sonamu/cone.md +138 -138
- package/src/skills/sonamu/config.md +191 -191
- package/src/skills/sonamu/create-sonamu.md +66 -66
- package/src/skills/sonamu/database.md +158 -158
- package/src/skills/sonamu/entity-basic.md +292 -293
- package/src/skills/sonamu/entity-relations.md +246 -246
- package/src/skills/sonamu/entity-validation-checklist.md +124 -124
- package/src/skills/sonamu/fixture-cli.md +231 -231
- package/src/skills/sonamu/framework-change.md +37 -37
- package/src/skills/sonamu/frontend.md +223 -223
- package/src/skills/sonamu/i18n.md +82 -82
- package/src/skills/sonamu/migration.md +77 -77
- package/src/skills/sonamu/model.md +222 -222
- package/src/skills/sonamu/naite.md +86 -86
- package/src/skills/sonamu/project-init.md +228 -228
- package/src/skills/sonamu/puri.md +122 -122
- package/src/skills/sonamu/scaffolding.md +154 -154
- package/src/skills/sonamu/skill-contribution.md +124 -124
- package/src/skills/sonamu/subset.md +46 -46
- package/src/skills/sonamu/tasks.md +82 -82
- package/src/skills/sonamu/testing-devrunner.md +147 -147
- package/src/skills/sonamu/testing.md +673 -673
- package/src/skills/sonamu/upsert.md +79 -79
- package/src/skills/sonamu/vector.md +67 -67
- package/src/testing/fixture-generator.ts +4 -4
- package/src/ui/ai-client.ts +1 -1
- package/src/ui/api.ts +18 -17
- package/src/ui/cdd-service.ts +264 -254
- package/src/ui/cdd-types.ts +40 -75
- package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
- package/src/skills/sonamu/workflow.md +0 -317
|
@@ -1,86 +1,86 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sonamu-frontend
|
|
3
|
-
description: Sonamu
|
|
3
|
+
description: Sonamu frontend integration. Auto-generated Service, TanStack Query hooks, useTypeForm/useListParams/useSelection, FileInput, MultiSelect, SonamuProvider (react-components v0.1.8+). Use when calling APIs, building forms, handling file uploads, or managing list/selection states.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Frontend Service
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Quick Reference
|
|
9
9
|
|
|
10
10
|
### Hooks
|
|
11
11
|
|
|
12
|
-
| Hook |
|
|
13
|
-
| --------------- |
|
|
14
|
-
| `useTypeForm` |
|
|
15
|
-
| `useListParams` | URL
|
|
16
|
-
| `useSelection` |
|
|
17
|
-
| `useModal` |
|
|
18
|
-
| `useToast` |
|
|
19
|
-
|
|
20
|
-
###
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
| ------------- |
|
|
24
|
-
| `Input` |
|
|
25
|
-
| `Textarea` |
|
|
26
|
-
| `Checkbox` |
|
|
27
|
-
| `Select` |
|
|
28
|
-
| `MultiSelect` |
|
|
29
|
-
| `EnumSelect` | Enum
|
|
30
|
-
| `FileInput` |
|
|
31
|
-
|
|
32
|
-
### Service (
|
|
33
|
-
|
|
34
|
-
|
|
|
35
|
-
| ----------------- |
|
|
36
|
-
| `get{Entity}` |
|
|
37
|
-
| `get{Entities}` |
|
|
38
|
-
| `save` |
|
|
39
|
-
| `del` |
|
|
40
|
-
| `use{Entity}` |
|
|
41
|
-
| `use{Entities}` |
|
|
42
|
-
| `useSaveMutation` |
|
|
43
|
-
|
|
44
|
-
###
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
| ------------------ |
|
|
48
|
-
| `dateF` |
|
|
49
|
-
| `datetimeF` |
|
|
50
|
-
| `numF` |
|
|
51
|
-
| `hidden` |
|
|
52
|
-
| `arrayableToArray` |
|
|
53
|
-
|
|
54
|
-
###
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
| ---------------- |
|
|
58
|
-
| `SonamuProvider` |
|
|
59
|
-
| `uploader` |
|
|
60
|
-
| `auth` |
|
|
61
|
-
| `SD` |
|
|
12
|
+
| Hook | Purpose | Key Return Values |
|
|
13
|
+
| --------------- | ------------------------------------ | -------------------------------------------------------- |
|
|
14
|
+
| `useTypeForm` | Form state management (Zod-based) | form, setForm, register, submit, addError, reset |
|
|
15
|
+
| `useListParams` | URL-synced list parameters | listParams, setListParams, register |
|
|
16
|
+
| `useSelection` | Checkbox multi-selection | selectedKeys, toggle, selectAll, deselectAll |
|
|
17
|
+
| `useModal` | Modal state management | open, modal |
|
|
18
|
+
| `useToast` | Toast notifications | toast |
|
|
19
|
+
|
|
20
|
+
### Components
|
|
21
|
+
|
|
22
|
+
| Component | Purpose | Key Props |
|
|
23
|
+
| ------------- | -------------------- | --------------------------------------------------- |
|
|
24
|
+
| `Input` | Text input | value, onValueChange |
|
|
25
|
+
| `Textarea` | Multi-line input | value, onValueChange |
|
|
26
|
+
| `Checkbox` | Checkbox | value (boolean), onValueChange, label |
|
|
27
|
+
| `Select` | Single select | items, value, onValueChange, placeholder, clearable |
|
|
28
|
+
| `MultiSelect` | Multi-select | options, value (array), onValueChange, maxCount |
|
|
29
|
+
| `EnumSelect` | Enum select | enum, labels, value, onValueChange |
|
|
30
|
+
| `FileInput` | File upload | uploadMode, viewMode, multiple, maxFiles |
|
|
31
|
+
|
|
32
|
+
### Service (Auto-generated)
|
|
33
|
+
|
|
34
|
+
| Method | Purpose | Example |
|
|
35
|
+
| ----------------- | ------------------------ | -------------------------------------- |
|
|
36
|
+
| `get{Entity}` | Fetch single record | `UserService.getUser("A", 123)` |
|
|
37
|
+
| `get{Entities}` | Fetch list | `UserService.getUsers("P", params)` |
|
|
38
|
+
| `save` | Save (create/update) | `UserService.save([data])` |
|
|
39
|
+
| `del` | Delete | `UserService.del([1, 2, 3])` |
|
|
40
|
+
| `use{Entity}` | Single fetch hook | `UserService.useUser("A", id)` |
|
|
41
|
+
| `use{Entities}` | List fetch hook | `UserService.useUsers("P", params)` |
|
|
42
|
+
| `useSaveMutation` | Save mutation | `UserService.useSaveMutation()` |
|
|
43
|
+
|
|
44
|
+
### Utilities
|
|
45
|
+
|
|
46
|
+
| Function | Purpose | Example |
|
|
47
|
+
| ------------------ | ---------------------------- | ---------------------------------------------------- |
|
|
48
|
+
| `dateF` | Date formatting | `dateF(new Date())` → `"2024-01-15"` |
|
|
49
|
+
| `datetimeF` | Datetime formatting | `datetimeF(new Date())` → `"2024-01-15 10:30:00"` |
|
|
50
|
+
| `numF` | Number formatting | `numF(1234567)` → `"1,234,567"` |
|
|
51
|
+
| `hidden` | Conditional hidden class | `hidden(true)` → `"hidden"` |
|
|
52
|
+
| `arrayableToArray` | Convert to array | `arrayableToArray("a")` → `["a"]` |
|
|
53
|
+
|
|
54
|
+
### Configuration
|
|
55
|
+
|
|
56
|
+
| Item | Description | Required |
|
|
57
|
+
| ---------------- | ------------------------------------------------ | ----------------------------------------- |
|
|
58
|
+
| `SonamuProvider` | Global configuration Provider (uploader, auth, SD) | Required (uploader required for FileInput) |
|
|
59
|
+
| `uploader` | File upload function | Required when using FileInput |
|
|
60
|
+
| `auth` | Authentication state and functions | Optional |
|
|
61
|
+
| `SD` | Internationalization function | Optional |
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
65
|
# Frontend Service
|
|
66
66
|
|
|
67
|
-
##
|
|
67
|
+
## Basic Usage
|
|
68
68
|
|
|
69
69
|
```typescript
|
|
70
70
|
import { UserService } from "@/services/services.generated";
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Single fetch (Subset required) - get{Entity} form
|
|
73
73
|
const user = await UserService.getUser("A", 123);
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// List fetch - get{Entities} form
|
|
76
76
|
const { rows, total } = await UserService.getUsers("P", { num: 20, page: 1 });
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Save
|
|
79
79
|
const [userId] = await UserService.save([
|
|
80
80
|
{ email: "new@test.com", username: "newuser" },
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
|
-
//
|
|
83
|
+
// Delete
|
|
84
84
|
const count = await UserService.del([1, 2, 3]);
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -90,7 +90,7 @@ const count = await UserService.del([1, 2, 3]);
|
|
|
90
90
|
|
|
91
91
|
```typescript
|
|
92
92
|
function UserProfile({ userId }: { userId: number }) {
|
|
93
|
-
// use{Entity}
|
|
93
|
+
// use{Entity} form (single), use{Entities} form (list)
|
|
94
94
|
const { data: user, isLoading, error } = UserService.useUser("A", userId);
|
|
95
95
|
|
|
96
96
|
if (isLoading) return <div>Loading...</div>;
|
|
@@ -113,11 +113,11 @@ function EditProfile() {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
return <button disabled={saveMutation.isPending}
|
|
116
|
+
return <button disabled={saveMutation.isPending}>Save</button>;
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
###
|
|
120
|
+
### Conditional Fetching
|
|
121
121
|
|
|
122
122
|
```typescript
|
|
123
123
|
const { data } = UserService.useUser("A", userId!, {
|
|
@@ -125,7 +125,7 @@ const { data } = UserService.useUser("A", userId!, {
|
|
|
125
125
|
});
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
###
|
|
128
|
+
### Cache Invalidation
|
|
129
129
|
|
|
130
130
|
```typescript
|
|
131
131
|
const queryClient = useQueryClient();
|
|
@@ -134,9 +134,9 @@ queryClient.invalidateQueries({ queryKey: ["User", "findById", "A", userId] });
|
|
|
134
134
|
|
|
135
135
|
## useTypeForm
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
Type-safe form management based on Zod schemas (react-components v0.1.8+)
|
|
138
138
|
|
|
139
|
-
###
|
|
139
|
+
### Return Values
|
|
140
140
|
|
|
141
141
|
```typescript
|
|
142
142
|
const {
|
|
@@ -151,29 +151,29 @@ const {
|
|
|
151
151
|
} = useTypeForm(Schema, defaultValue);
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
|
|
|
155
|
-
| ------------- | --------------------------------------------------- |
|
|
156
|
-
| `form` | `z.infer<Schema>` |
|
|
157
|
-
| `setForm` | `React.Dispatch<SetStateAction<...>>` |
|
|
158
|
-
| `register` | `(field: string) => RegisterReturn` |
|
|
159
|
-
| `submit` | `(callback) => () => Promise<R>` |
|
|
160
|
-
| `addError` | `(path: string, error: string \| ErrorObj) => void` |
|
|
161
|
-
| `removeError` | `(path: string) => void` |
|
|
162
|
-
| `clearError` | `() => void` |
|
|
163
|
-
| `reset` | `() => void` |
|
|
154
|
+
| Return Value | Type | Description |
|
|
155
|
+
| ------------- | --------------------------------------------------- | ---------------------------------------- |
|
|
156
|
+
| `form` | `z.infer<Schema>` | Current form data |
|
|
157
|
+
| `setForm` | `React.Dispatch<SetStateAction<...>>` | Form state update function |
|
|
158
|
+
| `register` | `(field: string) => RegisterReturn` | Field registration function |
|
|
159
|
+
| `submit` | `(callback) => () => Promise<R>` | Submit handler factory |
|
|
160
|
+
| `addError` | `(path: string, error: string \| ErrorObj) => void` | Manually add an error |
|
|
161
|
+
| `removeError` | `(path: string) => void` | Remove error for a specific field |
|
|
162
|
+
| `clearError` | `() => void` | Clear all errors |
|
|
163
|
+
| `reset` | `() => void` | Reset form to defaultValue |
|
|
164
164
|
|
|
165
|
-
### register
|
|
165
|
+
### register Return Object
|
|
166
166
|
|
|
167
167
|
```typescript
|
|
168
168
|
register(fieldName) // Returns:
|
|
169
169
|
{
|
|
170
|
-
value: any, //
|
|
171
|
-
onValueChange: (value: any) => void, //
|
|
172
|
-
error?: { content: string } //
|
|
170
|
+
value: any, // Current field value
|
|
171
|
+
onValueChange: (value: any) => void, // Value change handler
|
|
172
|
+
error?: { content: string } // Error object (if present)
|
|
173
173
|
}
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
###
|
|
176
|
+
### Basic Usage
|
|
177
177
|
|
|
178
178
|
```tsx
|
|
179
179
|
import { useTypeForm } from "@sonamu-kit/react-components/lib";
|
|
@@ -191,7 +191,7 @@ function RegisterForm() {
|
|
|
191
191
|
await UserService.save([form]);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// Method 1: spread operator (recommended)
|
|
195
195
|
const emailProps = register("email");
|
|
196
196
|
|
|
197
197
|
return (
|
|
@@ -201,21 +201,21 @@ function RegisterForm() {
|
|
|
201
201
|
<span className="error">{emailProps.error.content}</span>
|
|
202
202
|
)}
|
|
203
203
|
|
|
204
|
-
{/*
|
|
204
|
+
{/* Method 2: inline (for short cases) */}
|
|
205
205
|
<Input {...register("username")} />
|
|
206
206
|
{register("username").error && (
|
|
207
207
|
<span className="error">{register("username").error.content}</span>
|
|
208
208
|
)}
|
|
209
209
|
|
|
210
|
-
<button onClick={handleSubmit}
|
|
210
|
+
<button onClick={handleSubmit}>Register</button>
|
|
211
211
|
</form>
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
### IMPORTANT: react-components UI
|
|
216
|
+
### IMPORTANT: react-components UI Component Usage
|
|
217
217
|
|
|
218
|
-
react-components
|
|
218
|
+
All UI components in react-components follow the `value/onValueChange` pattern:
|
|
219
219
|
|
|
220
220
|
```tsx
|
|
221
221
|
import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/components";
|
|
@@ -229,41 +229,41 @@ import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/
|
|
|
229
229
|
// Checkbox (boolean)
|
|
230
230
|
<Checkbox {...register("agreed")} />
|
|
231
231
|
|
|
232
|
-
// Select (items prop
|
|
232
|
+
// Select (using items prop)
|
|
233
233
|
<Select
|
|
234
234
|
{...register("status")}
|
|
235
235
|
items={[
|
|
236
|
-
{ value: "active", label: "
|
|
237
|
-
{ value: "inactive", label: "
|
|
236
|
+
{ value: "active", label: "Active" },
|
|
237
|
+
{ value: "inactive", label: "Inactive" }
|
|
238
238
|
]}
|
|
239
|
-
placeholder="
|
|
239
|
+
placeholder="Select status"
|
|
240
240
|
/>
|
|
241
241
|
|
|
242
|
-
// Select
|
|
242
|
+
// Select simple form (string[] | number[])
|
|
243
243
|
<Select
|
|
244
244
|
{...register("priority")}
|
|
245
245
|
items={["high", "medium", "low"]}
|
|
246
|
-
placeholder="
|
|
246
|
+
placeholder="Priority"
|
|
247
247
|
/>
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
**Select
|
|
250
|
+
**Select component key props:**
|
|
251
251
|
|
|
252
|
-
- `items`:
|
|
253
|
-
- `placeholder`:
|
|
254
|
-
- `clearable`:
|
|
255
|
-
- `renderItem`:
|
|
252
|
+
- `items`: Array of selectable items (`V[]` or `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
|
|
253
|
+
- `placeholder`: Text shown before selection
|
|
254
|
+
- `clearable`: Whether the X button can deselect
|
|
255
|
+
- `renderItem`: Custom render function
|
|
256
256
|
|
|
257
257
|
### IMPORTANT: Form Required Field Initial Values
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
Fields defined as required in SaveParams **must be included** in the form initial values:
|
|
260
260
|
|
|
261
|
-
|
|
|
261
|
+
| Type | Initial Value |
|
|
262
262
|
| ----------------- | ---------------------- |
|
|
263
263
|
| string (required) | `""` |
|
|
264
264
|
| number (required) | `0` |
|
|
265
265
|
| Date (required) | `new Date()` |
|
|
266
|
-
| enum (required) |
|
|
266
|
+
| enum (required) | Default value (e.g. `"draft"`) |
|
|
267
267
|
| FK (required) | `0` |
|
|
268
268
|
| nullable | `null` |
|
|
269
269
|
|
|
@@ -280,9 +280,9 @@ const { form, setForm, register } = useTypeForm(TaskSaveParams, {
|
|
|
280
280
|
|
|
281
281
|
### IMPORTANT: Accessing Relation Objects When Loading Data
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
If a scaffolded form accesses relation objects like `row.collection?.id`, that relation must be included in subset A.
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
**Error**: `Property 'collection' does not exist on type` → Add `"collection.id"` to subset A in entity.json
|
|
286
286
|
|
|
287
287
|
```json
|
|
288
288
|
// question.entity.json > subsets > A
|
|
@@ -296,25 +296,25 @@ const { form, setForm, register } = useTypeForm(TaskSaveParams, {
|
|
|
296
296
|
]
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
**Alternative**: If the FK is already on `row`, using `...row` alone is sufficient without accessing the relation (no subset modification needed)
|
|
300
300
|
|
|
301
301
|
### IMPORTANT: SD() Translation Key for FK Fields
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
Scaffolded forms use `SD("entity.Task.institution_id")`, but `sd.generated.ts` only generates keys without `_id`.
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
**Fix**: Manually add the `_id` key to `ko.ts`
|
|
306
306
|
|
|
307
307
|
```typescript
|
|
308
308
|
// packages/api/src/i18n/ko.ts
|
|
309
|
-
"entity.Task.institution_id": "
|
|
310
|
-
"entity.Question.collection_id": "
|
|
309
|
+
"entity.Task.institution_id": "Institution",
|
|
310
|
+
"entity.Question.collection_id": "Collection",
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
`ko.ts
|
|
313
|
+
Since `ko.ts` is copied from api → web, you only need to add it once.
|
|
314
314
|
|
|
315
315
|
## useListParams
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
List parameter management synchronized with URL query parameters (pagination, filtering)
|
|
318
318
|
|
|
319
319
|
```typescript
|
|
320
320
|
import { useListParams } from "@sonamu-kit/react-components/lib";
|
|
@@ -337,34 +337,34 @@ function UserListPage() {
|
|
|
337
337
|
|
|
338
338
|
return (
|
|
339
339
|
<div>
|
|
340
|
-
{/*
|
|
341
|
-
<Input {...register("search")} placeholder="
|
|
340
|
+
{/* Search (resets page=1 on change) */}
|
|
341
|
+
<Input {...register("search")} placeholder="Search" />
|
|
342
342
|
|
|
343
|
-
{/*
|
|
343
|
+
{/* Filter (resets page=1 on change) */}
|
|
344
344
|
<Select {...register("status")} items={["active", "inactive"]} />
|
|
345
345
|
|
|
346
|
-
{/*
|
|
346
|
+
{/* Pagination (changes page only) */}
|
|
347
347
|
<button onClick={() => setListParams({ ...listParams, page: listParams.page - 1 })}>
|
|
348
|
-
|
|
348
|
+
Previous
|
|
349
349
|
</button>
|
|
350
350
|
<span>Page {listParams.page}</span>
|
|
351
351
|
<button onClick={() => setListParams({ ...listParams, page: listParams.page + 1 })}>
|
|
352
|
-
|
|
352
|
+
Next
|
|
353
353
|
</button>
|
|
354
354
|
</div>
|
|
355
355
|
);
|
|
356
356
|
}
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
**Key points:**
|
|
360
360
|
|
|
361
|
-
-
|
|
362
|
-
- `register
|
|
363
|
-
-
|
|
361
|
+
- Automatically syncs with URL (`?page=2&status=active`)
|
|
362
|
+
- `register` automatically resets page to 1 when any field other than page changes
|
|
363
|
+
- Type safety guaranteed by Zod schema
|
|
364
364
|
|
|
365
365
|
## useSelection
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
Checkbox multi-selection management (supports Shift-click range selection)
|
|
368
368
|
|
|
369
369
|
```typescript
|
|
370
370
|
import { useSelection } from "@sonamu-kit/react-components/lib";
|
|
@@ -393,10 +393,10 @@ function UserListPage() {
|
|
|
393
393
|
<Checkbox
|
|
394
394
|
value={isAllSelected}
|
|
395
395
|
onValueChange={isAllSelected ? deselectAll : selectAll}
|
|
396
|
-
label="
|
|
396
|
+
label="Select all"
|
|
397
397
|
/>
|
|
398
398
|
<button onClick={handleDelete} disabled={selectedKeys.length === 0}>
|
|
399
|
-
|
|
399
|
+
Delete selected ({selectedKeys.length})
|
|
400
400
|
</button>
|
|
401
401
|
|
|
402
402
|
{data?.rows.map((user, index) => (
|
|
@@ -413,19 +413,19 @@ function UserListPage() {
|
|
|
413
413
|
}
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
-
|
|
416
|
+
**Key points:**
|
|
417
417
|
|
|
418
|
-
-
|
|
419
|
-
- `selectedKeys`:
|
|
420
|
-
- `isAllSelected`:
|
|
418
|
+
- Range selection with Shift + click
|
|
419
|
+
- `selectedKeys`: Array of currently selected keys
|
|
420
|
+
- `isAllSelected`: Whether all items are selected
|
|
421
421
|
|
|
422
422
|
## IdAsyncSelect
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
Component for asynchronously searching and selecting Entity records. The generic type must be specified according to the Entity's Primary Key type.
|
|
425
425
|
|
|
426
|
-
###
|
|
426
|
+
### Basic Usage
|
|
427
427
|
|
|
428
|
-
IdAsyncSelect
|
|
428
|
+
IdAsyncSelect is typically used as a per-Entity wrapper component:
|
|
429
429
|
|
|
430
430
|
```typescript
|
|
431
431
|
import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
|
|
@@ -454,7 +454,7 @@ export function UserIdAsyncSelect<T extends UserSubsetKey>({
|
|
|
454
454
|
baseListParams,
|
|
455
455
|
displayField = "name",
|
|
456
456
|
valueField = "id",
|
|
457
|
-
placeholder = "
|
|
457
|
+
placeholder = "User",
|
|
458
458
|
clearable,
|
|
459
459
|
disabled,
|
|
460
460
|
className,
|
|
@@ -479,25 +479,25 @@ export function UserIdAsyncSelect<T extends UserSubsetKey>({
|
|
|
479
479
|
}
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
-
|
|
482
|
+
**Key Props:**
|
|
483
483
|
|
|
484
|
-
- `config`:
|
|
485
|
-
- `subset`:
|
|
486
|
-
- `baseListParams`:
|
|
487
|
-
- `displayField`:
|
|
488
|
-
- `valueField`:
|
|
489
|
-
- `multiple`:
|
|
490
|
-
- `value`:
|
|
491
|
-
- `onValueChange`:
|
|
484
|
+
- `config`: Auto-generated AsyncIdConfig (EntityAsyncIdConfig form)
|
|
485
|
+
- `subset`: Subset key to query
|
|
486
|
+
- `baseListParams`: List filter parameters
|
|
487
|
+
- `displayField`: Field name to display (default varies by Entity)
|
|
488
|
+
- `valueField`: Field name used as value (default: "id")
|
|
489
|
+
- `multiple`: Whether multi-selection is enabled
|
|
490
|
+
- `value`: Currently selected value (number or string depending on PK type)
|
|
491
|
+
- `onValueChange`: Value change handler
|
|
492
492
|
|
|
493
|
-
### Cascade Dropdown
|
|
493
|
+
### Cascade Dropdown Pattern (Hierarchical Selection)
|
|
494
494
|
|
|
495
|
-
|
|
495
|
+
When lower-level lists should change based on higher-level selection (e.g. Department → Division → Lab), pass `baseListParams` dynamically.
|
|
496
496
|
|
|
497
|
-
|
|
497
|
+
**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)
|
|
498
498
|
|
|
499
499
|
```tsx
|
|
500
|
-
//
|
|
500
|
+
// Example: 3-level cascade Department → Division → Lab
|
|
501
501
|
function UserForm() {
|
|
502
502
|
const { form, register, setForm } = useTypeForm(UserSaveParams, {
|
|
503
503
|
dept_id: null,
|
|
@@ -507,29 +507,29 @@ function UserForm() {
|
|
|
507
507
|
|
|
508
508
|
return (
|
|
509
509
|
<form>
|
|
510
|
-
{/* 1
|
|
510
|
+
{/* Level 1: Department selection (full list → preload or default IdAsyncSelect) */}
|
|
511
511
|
<DepartmentIdAsyncSelect
|
|
512
512
|
subset="A"
|
|
513
513
|
{...register("dept_id")}
|
|
514
514
|
onValueChange={(v) => {
|
|
515
|
-
//
|
|
515
|
+
// Reset lower values when department changes
|
|
516
516
|
setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
|
|
517
517
|
}}
|
|
518
518
|
/>
|
|
519
519
|
|
|
520
|
-
{/* 2
|
|
520
|
+
{/* Level 2: Division selection (only divisions in selected department) */}
|
|
521
521
|
<DivisionIdAsyncSelect
|
|
522
522
|
subset="A"
|
|
523
523
|
baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
|
|
524
524
|
disabled={!form.dept_id}
|
|
525
525
|
{...register("division_id")}
|
|
526
526
|
onValueChange={(v) => {
|
|
527
|
-
//
|
|
527
|
+
// Reset lab when division changes
|
|
528
528
|
setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
|
|
529
529
|
}}
|
|
530
530
|
/>
|
|
531
531
|
|
|
532
|
-
{/* 3
|
|
532
|
+
{/* Level 3: Lab selection (only labs in selected division) */}
|
|
533
533
|
<LabIdAsyncSelect
|
|
534
534
|
subset="A"
|
|
535
535
|
baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
|
|
@@ -541,41 +541,41 @@ function UserForm() {
|
|
|
541
541
|
}
|
|
542
542
|
```
|
|
543
543
|
|
|
544
|
-
|
|
545
|
-
-
|
|
546
|
-
- `disabled` prop
|
|
547
|
-
- `baseListParams
|
|
544
|
+
**Notes**:
|
|
545
|
+
- You must explicitly reset lower values to `null` when a higher-level value changes. IdAsyncSelect does not reset automatically.
|
|
546
|
+
- Using the `disabled` prop to disable lower levels when the parent is not selected improves UX.
|
|
547
|
+
- If `baseListParams` is `undefined`, IdAsyncSelect stays in enabled=false state and does not fetch.
|
|
548
548
|
|
|
549
|
-
**
|
|
549
|
+
**Items to specify in Spec** (recommended to add to acceptanceCriteria in spec.json when cascade is present):
|
|
550
550
|
```json
|
|
551
551
|
"acceptanceCriteria": [
|
|
552
|
-
"
|
|
553
|
-
"
|
|
554
|
-
"
|
|
552
|
+
"When a department is selected, only divisions belonging to that department appear in the dropdown",
|
|
553
|
+
"When a division is selected, only labs belonging to that division appear in the dropdown",
|
|
554
|
+
"When the department changes, the lower division/lab selections are reset"
|
|
555
555
|
]
|
|
556
556
|
```
|
|
557
557
|
|
|
558
558
|
### IMPORTANT: String Primary Key Support
|
|
559
559
|
|
|
560
|
-
|
|
560
|
+
Most Entities use Number PK (`IdAsyncSelect<number>`), but better-auth related Entities use String PK.
|
|
561
561
|
|
|
562
|
-
**String PK
|
|
562
|
+
**String PK Entities**: User, Account, Session, Verification
|
|
563
563
|
|
|
564
|
-
|
|
564
|
+
**Points to change** (manual modification required after scaffolding):
|
|
565
565
|
|
|
566
566
|
```typescript
|
|
567
|
-
// Number PK (
|
|
567
|
+
// Number PK (default)
|
|
568
568
|
value?: number | number[] | null;
|
|
569
569
|
onValueChange?: (value: number | number[] | undefined) => void;
|
|
570
570
|
<IdAsyncSelect<number> config={PostAsyncIdConfig} ... />
|
|
571
571
|
|
|
572
|
-
// String PK (User, Account
|
|
572
|
+
// String PK (User, Account, etc.) — change all 3 places to string
|
|
573
573
|
value?: string | string[] | null;
|
|
574
574
|
onValueChange?: (value: string | string[] | undefined) => void;
|
|
575
575
|
<IdAsyncSelect<string> config={AccountAsyncIdConfig} ... />
|
|
576
576
|
```
|
|
577
577
|
|
|
578
|
-
###
|
|
578
|
+
### Usage in Forms
|
|
579
579
|
|
|
580
580
|
```tsx
|
|
581
581
|
function PostForm() {
|
|
@@ -600,7 +600,7 @@ function PostForm() {
|
|
|
600
600
|
|
|
601
601
|
## FileInput
|
|
602
602
|
|
|
603
|
-
|
|
603
|
+
File upload component (image/general files, eager/lazy modes)
|
|
604
604
|
|
|
605
605
|
```typescript
|
|
606
606
|
import { FileInput } from "@sonamu-kit/react-components/components";
|
|
@@ -614,30 +614,30 @@ function ProfileForm() {
|
|
|
614
614
|
|
|
615
615
|
return (
|
|
616
616
|
<form>
|
|
617
|
-
{/*
|
|
617
|
+
{/* Single image - eager upload */}
|
|
618
618
|
<FileInput
|
|
619
619
|
{...register("avatar")}
|
|
620
620
|
uploadMode="eager"
|
|
621
621
|
viewMode="image"
|
|
622
|
-
placeholder="
|
|
622
|
+
placeholder="Profile image"
|
|
623
623
|
accept="image/*"
|
|
624
624
|
previewSize="md"
|
|
625
625
|
/>
|
|
626
626
|
|
|
627
|
-
{/*
|
|
627
|
+
{/* Multiple files - lazy upload */}
|
|
628
628
|
<FileInput
|
|
629
629
|
{...register("documents")}
|
|
630
630
|
uploadMode="lazy"
|
|
631
631
|
viewMode="file"
|
|
632
632
|
multiple
|
|
633
633
|
maxFiles={5}
|
|
634
|
-
placeholder="
|
|
634
|
+
placeholder="Attach documents"
|
|
635
635
|
/>
|
|
636
636
|
|
|
637
637
|
<button onClick={submit(async (form) => {
|
|
638
|
-
// lazy
|
|
638
|
+
// lazy mode: auto-uploads on submit
|
|
639
639
|
await ProfileService.save([form]);
|
|
640
|
-
})}
|
|
640
|
+
})}>Save</button>
|
|
641
641
|
</form>
|
|
642
642
|
);
|
|
643
643
|
}
|
|
@@ -645,18 +645,18 @@ function ProfileForm() {
|
|
|
645
645
|
|
|
646
646
|
**Props:**
|
|
647
647
|
|
|
648
|
-
- `uploadMode`: `"eager"` (
|
|
649
|
-
- `viewMode`: `"image"` (
|
|
650
|
-
- `multiple`:
|
|
651
|
-
- `maxFiles`:
|
|
648
|
+
- `uploadMode`: `"eager"` (upload immediately) | `"lazy"` (upload on submit)
|
|
649
|
+
- `viewMode`: `"image"` (image preview) | `"file"` (filename)
|
|
650
|
+
- `multiple`: Whether multiple files can be selected
|
|
651
|
+
- `maxFiles`: Maximum number of files
|
|
652
652
|
- `previewSize`: `"sm" | "md" | "lg" | "xl"`
|
|
653
|
-
- `clearable`: X
|
|
653
|
+
- `clearable`: Whether the X button can remove the file
|
|
654
654
|
|
|
655
|
-
**IMPORTANT**:
|
|
655
|
+
**IMPORTANT**: uploader function must be configured in SonamuProvider (see below)
|
|
656
656
|
|
|
657
|
-
## Select (
|
|
657
|
+
## Select (Multi-select Mode)
|
|
658
658
|
|
|
659
|
-
|
|
659
|
+
Setting `multiple: true` on the `Select` component enables multi-select mode.
|
|
660
660
|
|
|
661
661
|
```typescript
|
|
662
662
|
import { Select } from "@sonamu-kit/react-components/components";
|
|
@@ -678,31 +678,31 @@ function TagForm() {
|
|
|
678
678
|
{...register("tag_ids")}
|
|
679
679
|
items={items}
|
|
680
680
|
multiple
|
|
681
|
-
placeholder="
|
|
681
|
+
placeholder="Select tags"
|
|
682
682
|
/>
|
|
683
683
|
);
|
|
684
684
|
}
|
|
685
685
|
```
|
|
686
686
|
|
|
687
|
-
|
|
687
|
+
**Multi-select specific Props:**
|
|
688
688
|
|
|
689
|
-
- `multiple`: `true` (
|
|
690
|
-
- `maxCount`:
|
|
691
|
-
- `hideSelectAll`:
|
|
692
|
-
- `searchable`:
|
|
689
|
+
- `multiple`: `true` (enables multi-select)
|
|
690
|
+
- `maxCount`: Maximum number of badges to display
|
|
691
|
+
- `hideSelectAll`: Hide the select all button
|
|
692
|
+
- `searchable`: Enable search input
|
|
693
693
|
|
|
694
|
-
|
|
694
|
+
**Common Props:**
|
|
695
695
|
|
|
696
|
-
- `items`: `SelectItemDef[]` (
|
|
697
|
-
- `placeholder`:
|
|
698
|
-
- `clearable`: X
|
|
699
|
-
- `disabled`:
|
|
700
|
-
- `renderItem`:
|
|
701
|
-
- `async`: `true
|
|
696
|
+
- `items`: `SelectItemDef[]` (values only or `{ value, label, disabled }` form)
|
|
697
|
+
- `placeholder`: Text shown before selection
|
|
698
|
+
- `clearable`: X button to deselect all
|
|
699
|
+
- `disabled`: Disable the component
|
|
700
|
+
- `renderItem`: Custom render function
|
|
701
|
+
- `async`: When set to `true`, supports async search via `onSearch` callback
|
|
702
702
|
|
|
703
703
|
## EnumSelect
|
|
704
704
|
|
|
705
|
-
Zod enum
|
|
705
|
+
Select integrated with Zod enum (label mapping)
|
|
706
706
|
|
|
707
707
|
```typescript
|
|
708
708
|
import { EnumSelect } from "@sonamu-kit/react-components/components";
|
|
@@ -711,9 +711,9 @@ import { z } from "zod";
|
|
|
711
711
|
const StatusEnum = z.enum(["draft", "published", "archived"]);
|
|
712
712
|
|
|
713
713
|
const statusLabels = {
|
|
714
|
-
draft: "
|
|
715
|
-
published: "
|
|
716
|
-
archived: "
|
|
714
|
+
draft: "Draft",
|
|
715
|
+
published: "Published",
|
|
716
|
+
archived: "Archived",
|
|
717
717
|
} as const;
|
|
718
718
|
|
|
719
719
|
function PostForm() {
|
|
@@ -726,30 +726,30 @@ function PostForm() {
|
|
|
726
726
|
{...register("status")}
|
|
727
727
|
enum={StatusEnum}
|
|
728
728
|
labels={statusLabels}
|
|
729
|
-
placeholder="
|
|
729
|
+
placeholder="Select status"
|
|
730
730
|
clearable
|
|
731
731
|
/>
|
|
732
732
|
);
|
|
733
733
|
}
|
|
734
734
|
```
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
**Key points:**
|
|
737
737
|
|
|
738
|
-
- Zod enum
|
|
739
|
-
-
|
|
740
|
-
- enum.options
|
|
738
|
+
- Zod enum type safety
|
|
739
|
+
- Display name mapping via labels object
|
|
740
|
+
- Automatically converts enum.options to items
|
|
741
741
|
|
|
742
742
|
## SonamuProvider
|
|
743
743
|
|
|
744
|
-
|
|
744
|
+
Global configuration used across react-components
|
|
745
745
|
|
|
746
746
|
```typescript
|
|
747
|
-
// App.tsx
|
|
747
|
+
// App.tsx or root component
|
|
748
748
|
import { SonamuProvider } from "@sonamu-kit/react-components/contexts";
|
|
749
749
|
import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
|
|
750
750
|
|
|
751
751
|
function App() {
|
|
752
|
-
//
|
|
752
|
+
// File uploader function (used by FileInput, useTypeForm)
|
|
753
753
|
const uploader = async (files: File[]): Promise<SonamuFile[]> => {
|
|
754
754
|
const formData = new FormData();
|
|
755
755
|
files.forEach(file => formData.append("files", file));
|
|
@@ -762,7 +762,7 @@ function App() {
|
|
|
762
762
|
return response.json();
|
|
763
763
|
};
|
|
764
764
|
|
|
765
|
-
//
|
|
765
|
+
// Authentication state (optional)
|
|
766
766
|
const auth = {
|
|
767
767
|
user: currentUser,
|
|
768
768
|
loading: isLoading,
|
|
@@ -771,7 +771,7 @@ function App() {
|
|
|
771
771
|
refetch: async () => { /* ... */ },
|
|
772
772
|
};
|
|
773
773
|
|
|
774
|
-
//
|
|
774
|
+
// Internationalization function (optional)
|
|
775
775
|
const SD = (key: string) => dictionary[key] ?? key;
|
|
776
776
|
|
|
777
777
|
return (
|
|
@@ -782,13 +782,13 @@ function App() {
|
|
|
782
782
|
}
|
|
783
783
|
```
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
**Required Props:**
|
|
786
786
|
|
|
787
|
-
- `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - FileInput
|
|
788
|
-
- `auth`:
|
|
789
|
-
- `SD`:
|
|
787
|
+
- `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - Used by FileInput
|
|
788
|
+
- `auth`: Authentication state and functions (optional)
|
|
789
|
+
- `SD`: Internationalization function (optional)
|
|
790
790
|
|
|
791
|
-
##
|
|
791
|
+
## Utility Functions
|
|
792
792
|
|
|
793
793
|
```typescript
|
|
794
794
|
import {
|
|
@@ -800,27 +800,27 @@ import {
|
|
|
800
800
|
sqlDateToDateString,
|
|
801
801
|
} from "@sonamu-kit/react-components/lib";
|
|
802
802
|
|
|
803
|
-
//
|
|
803
|
+
// Date formatting
|
|
804
804
|
dateF(new Date()); // "2024-01-15"
|
|
805
805
|
dateF("2024-01-15T10:30:00"); // "2024-01-15"
|
|
806
806
|
datetimeF(new Date()); // "2024-01-15 10:30:00"
|
|
807
807
|
|
|
808
|
-
//
|
|
808
|
+
// Number formatting
|
|
809
809
|
numF(1234567); // "1,234,567"
|
|
810
810
|
|
|
811
|
-
//
|
|
811
|
+
// Conditional hidden class
|
|
812
812
|
<div className={hidden(isHidden)}>...</div>
|
|
813
813
|
|
|
814
814
|
// SQL date → date string
|
|
815
815
|
sqlDateToDateString("2024-01-15T10:30:00.000Z"); // "2024-01-15"
|
|
816
816
|
|
|
817
|
-
//
|
|
817
|
+
// Convert to array
|
|
818
818
|
arrayableToArray("single"); // ["single"]
|
|
819
819
|
arrayableToArray(["a", "b"]); // ["a", "b"]
|
|
820
820
|
arrayableToArray(undefined); // []
|
|
821
821
|
```
|
|
822
822
|
|
|
823
|
-
##
|
|
823
|
+
## Error Handling
|
|
824
824
|
|
|
825
825
|
```typescript
|
|
826
826
|
import { isSonamuError } from "@/lib/sonamu.shared";
|
|
@@ -857,9 +857,9 @@ registerSSR({
|
|
|
857
857
|
});
|
|
858
858
|
```
|
|
859
859
|
|
|
860
|
-
##
|
|
860
|
+
## Initial Project Setup
|
|
861
861
|
|
|
862
|
-
**→ `create-sonamu.md
|
|
862
|
+
**→ See the "Project Name Change" section in `create-sonamu.md`** (change 4 files: index.html, \_\_root.tsx, index.tsx, Sidebar.tsx)
|
|
863
863
|
|
|
864
864
|
## Rules
|
|
865
865
|
|
|
@@ -869,9 +869,9 @@ registerSSR({
|
|
|
869
869
|
|
|
870
870
|
---
|
|
871
871
|
|
|
872
|
-
##
|
|
872
|
+
## Full Component Implementation Examples
|
|
873
873
|
|
|
874
|
-
###
|
|
874
|
+
### List Page
|
|
875
875
|
|
|
876
876
|
```typescript
|
|
877
877
|
function ConsultationListPage() {
|
|
@@ -880,17 +880,17 @@ function ConsultationListPage() {
|
|
|
880
880
|
|
|
881
881
|
return (
|
|
882
882
|
<div>
|
|
883
|
-
{/*
|
|
883
|
+
{/* Manage selection with useSelection */}
|
|
884
884
|
{data?.rows.map((row) => (
|
|
885
885
|
<div key={row.id}>{row.title} - {row.status}</div>
|
|
886
886
|
))}
|
|
887
|
-
{/*
|
|
887
|
+
{/* Pagination: manipulate params.page */}
|
|
888
888
|
</div>
|
|
889
889
|
);
|
|
890
890
|
}
|
|
891
891
|
```
|
|
892
892
|
|
|
893
|
-
###
|
|
893
|
+
### Edit Page
|
|
894
894
|
|
|
895
895
|
```typescript
|
|
896
896
|
function ConsultationFormPage() {
|
|
@@ -899,7 +899,7 @@ function ConsultationFormPage() {
|
|
|
899
899
|
title: "", content: "", status: "pending", user_id: 0,
|
|
900
900
|
});
|
|
901
901
|
|
|
902
|
-
//
|
|
902
|
+
// Edit mode: load data
|
|
903
903
|
useEffect(() => {
|
|
904
904
|
if (id) ConsultationService.getConsultation("A", Number(id)).then((row) => setForm((prev) => ({ ...prev, ...row })));
|
|
905
905
|
}, [id]);
|
|
@@ -914,18 +914,18 @@ function ConsultationFormPage() {
|
|
|
914
914
|
<form>
|
|
915
915
|
<Input {...register("title")} />
|
|
916
916
|
<Textarea {...register("content")} />
|
|
917
|
-
<Select {...register("status")} items={[{value:"pending",label:"
|
|
918
|
-
<button onClick={handleSubmit} disabled={saveMutation.isPending}
|
|
917
|
+
<Select {...register("status")} items={[{value:"pending",label:"Pending"},{value:"completed",label:"Completed"}]} />
|
|
918
|
+
<button onClick={handleSubmit} disabled={saveMutation.isPending}>Save</button>
|
|
919
919
|
</form>
|
|
920
920
|
);
|
|
921
921
|
}
|
|
922
922
|
```
|
|
923
923
|
|
|
924
|
-
###
|
|
924
|
+
### Cache Invalidation
|
|
925
925
|
|
|
926
926
|
```typescript
|
|
927
927
|
const queryClient = useQueryClient();
|
|
928
|
-
await ConsultationService.changeStatus(id, newStatus, "
|
|
928
|
+
await ConsultationService.changeStatus(id, newStatus, "Status change");
|
|
929
929
|
queryClient.invalidateQueries({
|
|
930
930
|
queryKey: ["Consultation", "findById", "A", id],
|
|
931
931
|
});
|