sonamu 0.10.5 → 0.10.7
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/base-frame.d.ts +2 -0
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/base-frame.js +8 -1
- package/dist/api/decorators.js +1 -1
- package/dist/api/sonamu.js +1 -1
- package/dist/bin/cli.js +195 -187
- package/dist/cone/cone-generator.js +3 -9
- package/dist/database/base-model.js +2 -2
- package/dist/database/knex.d.ts.map +1 -1
- package/dist/database/knex.js +2 -2
- package/dist/database/puri.d.ts +17 -0
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +102 -3
- package/dist/entity/entity-manager.js +1 -1
- package/dist/index.js +3 -3
- package/dist/migration/code-generation.js +6 -6
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +52 -7
- package/dist/migration/slack-confirm.js +1 -1
- package/dist/naite/naite-reporter.js +1 -1
- package/dist/syncer/checksum.js +1 -1
- package/dist/syncer/code-generator.js +1 -1
- package/dist/syncer/entity-operations.js +1 -1
- package/dist/syncer/syncer-actions.js +1 -1
- package/dist/syncer/syncer.js +1 -1
- package/dist/ui-web/assets/{index-BLX4zN8c.js → index-D0MHYbxl.js} +1 -1
- package/dist/ui-web/assets/index-Dx_JX4aQ.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/formatter.js +1 -1
- package/package.json +3 -3
- package/src/api/base-frame.ts +14 -1
- package/src/bin/cli.ts +268 -280
- package/src/cone/cone-generator.ts +2 -18
- package/src/database/__tests__/puri.test.ts +183 -0
- package/src/database/knex.ts +4 -1
- package/src/database/puri.ts +229 -2
- package/src/database/puri.types.test-d.ts +56 -0
- package/src/migration/code-generation.ts +5 -5
- package/src/migration/migrator.ts +81 -7
- package/src/skills/AGENTS.md +76 -50
- package/src/skills/sonamu/SKILL.md +46 -227
- package/src/skills/{sonamu/ai-agents.md → sonamu-ai-agents/SKILL.md} +3 -3
- package/src/skills/{sonamu/api.md → sonamu-api/SKILL.md} +3 -2
- package/src/skills/{sonamu/auth.md → sonamu-auth/SKILL.md} +9 -4
- package/src/skills/{sonamu/auth-plugins.md → sonamu-auth/references/plugins.md} +2 -7
- package/src/skills/sonamu-auth/references/user-id-migration-followups.md +192 -0
- package/src/skills/{sonamu/auth-migration.md → sonamu-auth/references/user-id-migration.md} +1 -195
- package/src/skills/sonamu-config/SKILL.md +203 -0
- package/src/skills/{sonamu → sonamu-config/references}/database.md +3 -8
- package/src/skills/sonamu-config/references/environments.md +178 -0
- package/src/skills/sonamu-config/references/server-options.md +400 -0
- package/src/skills/sonamu-entity/SKILL.md +180 -0
- package/src/skills/{sonamu/entity-validation-checklist.md → sonamu-entity/references/creation-workflow.md} +117 -8
- package/src/skills/sonamu-entity/references/design-guides.md +243 -0
- package/src/skills/sonamu-entity/references/field-types.md +170 -0
- package/src/skills/sonamu-entity/references/relations-detail.md +245 -0
- package/src/skills/{sonamu/entity-relations.md → sonamu-entity/references/relations.md} +1 -258
- package/src/skills/{sonamu → sonamu-entity/references}/subset.md +1 -11
- package/src/skills/sonamu-fixture/SKILL.md +180 -0
- package/src/skills/{sonamu/fixture-cli.md → sonamu-fixture/references/cli-usage.md} +5 -176
- package/src/skills/{sonamu → sonamu-fixture/references}/cone.md +4 -14
- package/src/skills/sonamu-frontend/SKILL.md +142 -0
- package/src/skills/sonamu-frontend/references/components.md +323 -0
- package/src/skills/sonamu-frontend/references/examples.md +64 -0
- package/src/skills/sonamu-frontend/references/hooks.md +273 -0
- package/src/skills/sonamu-frontend/references/runtime.md +165 -0
- package/src/skills/{sonamu → sonamu-frontend/references}/scaffolding.md +3 -8
- package/src/skills/{sonamu/i18n.md → sonamu-i18n/SKILL.md} +2 -2
- package/src/skills/{sonamu/migration.md → sonamu-migration/SKILL.md} +1 -1
- package/src/skills/{sonamu/naite.md → sonamu-naite/SKILL.md} +5 -5
- package/src/skills/sonamu-query/SKILL.md +48 -0
- package/src/skills/sonamu-query/references/model-patterns.md +390 -0
- package/src/skills/{sonamu → sonamu-query/references}/model.md +1 -401
- package/src/skills/{sonamu → sonamu-query/references}/puri.md +31 -243
- package/src/skills/sonamu-query/references/search.md +238 -0
- package/src/skills/{sonamu → sonamu-query/references}/upsert.md +1 -6
- package/src/skills/{sonamu/tasks.md → sonamu-tasks/SKILL.md} +1 -1
- package/src/skills/sonamu-testing/SKILL.md +251 -0
- package/src/skills/{sonamu/testing-devrunner.md → sonamu-testing/references/devrunner.md} +4 -9
- package/src/skills/sonamu-testing/references/helpers.md +185 -0
- package/src/skills/sonamu-testing/references/patterns.md +263 -0
- package/src/skills/sonamu-testing/references/pitfalls.md +588 -0
- package/src/skills/sonamu-testing/references/quick-start.md +285 -0
- package/src/skills/sonamu-testing/references/type-safety.md +172 -0
- package/src/skills/sonamu-testing/references/writing-plan.md +375 -0
- package/src/skills/{sonamu/vector.md → sonamu-vector/SKILL.md} +1 -1
- package/dist/ui-web/assets/index-Dx4ap5i4.css +0 -1
- package/src/skills/commands/sonamu-skills.md +0 -20
- package/src/skills/project/README.md +0 -19
- package/src/skills/project/architecture.md +0 -373
- package/src/skills/sonamu/cdd.md +0 -129
- package/src/skills/sonamu/config.md +0 -772
- package/src/skills/sonamu/create-sonamu.md +0 -208
- package/src/skills/sonamu/entity-basic.md +0 -678
- package/src/skills/sonamu/framework-change.md +0 -96
- package/src/skills/sonamu/frontend.md +0 -915
- package/src/skills/sonamu/project-init.md +0 -477
- package/src/skills/sonamu/skill-contribution.md +0 -247
- package/src/skills/sonamu/testing.md +0 -2163
|
@@ -1,915 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sonamu-frontend
|
|
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
|
-
---
|
|
5
|
-
|
|
6
|
-
# Frontend Service
|
|
7
|
-
|
|
8
|
-
## Quick Reference
|
|
9
|
-
|
|
10
|
-
### Hooks
|
|
11
|
-
|
|
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
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
# Frontend Service
|
|
66
|
-
|
|
67
|
-
## Basic Usage
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { UserService } from "@/services/services.generated";
|
|
71
|
-
|
|
72
|
-
// Single fetch (Subset required) - get{Entity} form
|
|
73
|
-
const user = await UserService.getUser("A", 123);
|
|
74
|
-
|
|
75
|
-
// List fetch - get{Entities} form
|
|
76
|
-
const { rows, total } = await UserService.getUsers("P", { num: 20, page: 1 });
|
|
77
|
-
|
|
78
|
-
// Save
|
|
79
|
-
const [userId] = await UserService.save([{ email: "new@test.com", username: "newuser" }]);
|
|
80
|
-
|
|
81
|
-
// Delete
|
|
82
|
-
const count = await UserService.del([1, 2, 3]);
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## TanStack Query Hook
|
|
86
|
-
|
|
87
|
-
### useQuery
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
function UserProfile({ userId }: { userId: number }) {
|
|
91
|
-
// use{Entity} form (single), use{Entities} form (list)
|
|
92
|
-
const { data: user, isLoading, error } = UserService.useUser("A", userId);
|
|
93
|
-
|
|
94
|
-
if (isLoading) return <div>Loading...</div>;
|
|
95
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
96
|
-
|
|
97
|
-
return <h1>{user?.username}</h1>;
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### useMutation
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
function EditProfile() {
|
|
105
|
-
const saveMutation = UserService.useSaveMutation();
|
|
106
|
-
|
|
107
|
-
async function handleSubmit(data: UserSaveParams) {
|
|
108
|
-
saveMutation.mutate({ spa: [data] }, {
|
|
109
|
-
onSuccess: ([userId]) => console.log("Saved:", userId),
|
|
110
|
-
onError: (error) => console.error("Failed:", error),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return <button disabled={saveMutation.isPending}>Save</button>;
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Conditional Fetching
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const { data } = UserService.useUser("A", userId!, {
|
|
122
|
-
enabled: userId !== null,
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Cache Invalidation
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
const queryClient = useQueryClient();
|
|
130
|
-
queryClient.invalidateQueries({ queryKey: ["User", "findById", "A", userId] });
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## useTypeForm
|
|
134
|
-
|
|
135
|
-
Type-safe form management based on Zod schemas (react-components v0.1.8+)
|
|
136
|
-
|
|
137
|
-
### Return Values
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
const { form, setForm, register, submit, addError, removeError, clearError, reset } = useTypeForm(
|
|
141
|
-
Schema,
|
|
142
|
-
defaultValue,
|
|
143
|
-
);
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
| Return Value | Type | Description |
|
|
147
|
-
| ------------- | --------------------------------------------------- | --------------------------------- |
|
|
148
|
-
| `form` | `z.infer<Schema>` | Current form data |
|
|
149
|
-
| `setForm` | `React.Dispatch<SetStateAction<...>>` | Form state update function |
|
|
150
|
-
| `register` | `(field: string) => RegisterReturn` | Field registration function |
|
|
151
|
-
| `submit` | `(callback) => () => Promise<R>` | Submit handler factory |
|
|
152
|
-
| `addError` | `(path: string, error: string \| ErrorObj) => void` | Manually add an error |
|
|
153
|
-
| `removeError` | `(path: string) => void` | Remove error for a specific field |
|
|
154
|
-
| `clearError` | `() => void` | Clear all errors |
|
|
155
|
-
| `reset` | `() => void` | Reset form to defaultValue |
|
|
156
|
-
|
|
157
|
-
### register Return Object
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
register(fieldName) // Returns:
|
|
161
|
-
{
|
|
162
|
-
value: any, // Current field value
|
|
163
|
-
onValueChange: (value: any) => void, // Value change handler
|
|
164
|
-
error?: { content: string } // Error object (if present)
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Basic Usage
|
|
169
|
-
|
|
170
|
-
```tsx
|
|
171
|
-
import { useTypeForm } from "@sonamu-kit/react-components/lib";
|
|
172
|
-
import { Input } from "@sonamu-kit/react-components/components";
|
|
173
|
-
import { UserSaveParams } from "@/services/user/user.types";
|
|
174
|
-
|
|
175
|
-
function RegisterForm() {
|
|
176
|
-
const { form, setForm, register, submit } = useTypeForm(UserSaveParams, {
|
|
177
|
-
email: "",
|
|
178
|
-
username: "",
|
|
179
|
-
password: "",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const handleSubmit = submit(async (form) => {
|
|
183
|
-
await UserService.save([form]);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Method 1: spread operator (recommended)
|
|
187
|
-
const emailProps = register("email");
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<form>
|
|
191
|
-
<Input {...emailProps} />
|
|
192
|
-
{emailProps.error && <span className="error">{emailProps.error.content}</span>}
|
|
193
|
-
|
|
194
|
-
{/* Method 2: inline (for short cases) */}
|
|
195
|
-
<Input {...register("username")} />
|
|
196
|
-
{register("username").error && (
|
|
197
|
-
<span className="error">{register("username").error.content}</span>
|
|
198
|
-
)}
|
|
199
|
-
|
|
200
|
-
<button onClick={handleSubmit}>Register</button>
|
|
201
|
-
</form>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### IMPORTANT: react-components UI Component Usage
|
|
207
|
-
|
|
208
|
-
All UI components in react-components follow the `value/onValueChange` pattern:
|
|
209
|
-
|
|
210
|
-
```tsx
|
|
211
|
-
import { Input, Checkbox, Select, Textarea } from "@sonamu-kit/react-components/components";
|
|
212
|
-
|
|
213
|
-
// Input (string)
|
|
214
|
-
<Input {...register("email")} />
|
|
215
|
-
|
|
216
|
-
// Textarea (string)
|
|
217
|
-
<Textarea {...register("content")} />
|
|
218
|
-
|
|
219
|
-
// Checkbox (boolean)
|
|
220
|
-
<Checkbox {...register("agreed")} />
|
|
221
|
-
|
|
222
|
-
// Select (using items prop)
|
|
223
|
-
<Select
|
|
224
|
-
{...register("status")}
|
|
225
|
-
items={[
|
|
226
|
-
{ value: "active", label: "Active" },
|
|
227
|
-
{ value: "inactive", label: "Inactive" }
|
|
228
|
-
]}
|
|
229
|
-
placeholder="Select status"
|
|
230
|
-
/>
|
|
231
|
-
|
|
232
|
-
// Select simple form (string[] | number[])
|
|
233
|
-
<Select
|
|
234
|
-
{...register("priority")}
|
|
235
|
-
items={["high", "medium", "low"]}
|
|
236
|
-
placeholder="Priority"
|
|
237
|
-
/>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**Select component key props:**
|
|
241
|
-
|
|
242
|
-
- `items`: Array of selectable items (`V[]` or `{ value: V, label?: ReactNode, disabled?: boolean }[]`)
|
|
243
|
-
- `placeholder`: Text shown before selection
|
|
244
|
-
- `clearable`: Whether the X button can deselect
|
|
245
|
-
- `renderItem`: Custom render function
|
|
246
|
-
|
|
247
|
-
### IMPORTANT: Form Required Field Initial Values
|
|
248
|
-
|
|
249
|
-
Fields defined as required in SaveParams **must be included** in the form initial values:
|
|
250
|
-
|
|
251
|
-
| Type | Initial Value |
|
|
252
|
-
| ----------------- | ------------------------------ |
|
|
253
|
-
| string (required) | `""` |
|
|
254
|
-
| number (required) | `0` |
|
|
255
|
-
| Date (required) | `new Date()` |
|
|
256
|
-
| enum (required) | Default value (e.g. `"draft"`) |
|
|
257
|
-
| FK (required) | `0` |
|
|
258
|
-
| nullable | `null` |
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
const { form, setForm, register } = useTypeForm(TaskSaveParams, {
|
|
262
|
-
title: "", // string required
|
|
263
|
-
status: "draft", // enum required
|
|
264
|
-
budget: 0, // number required
|
|
265
|
-
begin_date: new Date(), // Date required
|
|
266
|
-
description: null, // nullable
|
|
267
|
-
institution_id: 0, // FK required
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### IMPORTANT: Accessing Relation Objects When Loading Data
|
|
272
|
-
|
|
273
|
-
If a scaffolded form accesses relation objects like `row.collection?.id`, that relation must be included in subset A.
|
|
274
|
-
|
|
275
|
-
**Error**: `Property 'collection' does not exist on type` → Add `"collection.id"` to subset A in entity.json
|
|
276
|
-
|
|
277
|
-
```json
|
|
278
|
-
// question.entity.json > subsets > A
|
|
279
|
-
["id", "content", "collection.id", "collection.title", "parent.id", "answer_group.id"]
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
**Alternative**: If the FK is already on `row`, using `...row` alone is sufficient without accessing the relation (no subset modification needed)
|
|
283
|
-
|
|
284
|
-
### IMPORTANT: SD() Translation Key for FK Fields
|
|
285
|
-
|
|
286
|
-
Scaffolded forms use `SD("entity.Task.institution_id")`, but `sd.generated.ts` only generates keys without `_id`.
|
|
287
|
-
|
|
288
|
-
**Fix**: Manually add the `_id` key to `ko.ts`
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
// packages/api/src/i18n/ko.ts
|
|
292
|
-
"entity.Task.institution_id": "Institution",
|
|
293
|
-
"entity.Question.collection_id": "Collection",
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Since `ko.ts` is copied from api → web, you only need to add it once.
|
|
297
|
-
|
|
298
|
-
## useListParams
|
|
299
|
-
|
|
300
|
-
List parameter management synchronized with URL query parameters (pagination, filtering)
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
import { useListParams } from "@sonamu-kit/react-components/lib";
|
|
304
|
-
import { z } from "zod";
|
|
305
|
-
|
|
306
|
-
const ListParamsSchema = z.object({
|
|
307
|
-
page: z.coerce.number().default(1),
|
|
308
|
-
num: z.coerce.number().default(20),
|
|
309
|
-
search: z.string().optional(),
|
|
310
|
-
status: z.enum(["active", "inactive"]).optional(),
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
function UserListPage() {
|
|
314
|
-
const { listParams, setListParams, register } = useListParams(
|
|
315
|
-
ListParamsSchema,
|
|
316
|
-
{ page: 1, num: 20 }
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const { data } = UserService.useUsers("P", listParams);
|
|
320
|
-
|
|
321
|
-
return (
|
|
322
|
-
<div>
|
|
323
|
-
{/* Search (resets page=1 on change) */}
|
|
324
|
-
<Input {...register("search")} placeholder="Search" />
|
|
325
|
-
|
|
326
|
-
{/* Filter (resets page=1 on change) */}
|
|
327
|
-
<Select {...register("status")} items={["active", "inactive"]} />
|
|
328
|
-
|
|
329
|
-
{/* Pagination (changes page only) */}
|
|
330
|
-
<button onClick={() => setListParams({ ...listParams, page: listParams.page - 1 })}>
|
|
331
|
-
Previous
|
|
332
|
-
</button>
|
|
333
|
-
<span>Page {listParams.page}</span>
|
|
334
|
-
<button onClick={() => setListParams({ ...listParams, page: listParams.page + 1 })}>
|
|
335
|
-
Next
|
|
336
|
-
</button>
|
|
337
|
-
</div>
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Key points:**
|
|
343
|
-
|
|
344
|
-
- Automatically syncs with URL (`?page=2&status=active`)
|
|
345
|
-
- `register` automatically resets page to 1 when any field other than page changes
|
|
346
|
-
- Type safety guaranteed by Zod schema
|
|
347
|
-
|
|
348
|
-
## useSelection
|
|
349
|
-
|
|
350
|
-
Checkbox multi-selection management (supports Shift-click range selection)
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
import { useSelection } from "@sonamu-kit/react-components/lib";
|
|
354
|
-
|
|
355
|
-
function UserListPage() {
|
|
356
|
-
const { data } = UserService.useUsers("P", { num: 20, page: 1 });
|
|
357
|
-
const userIds = data?.rows.map(row => row.id) ?? [];
|
|
358
|
-
|
|
359
|
-
const {
|
|
360
|
-
getSelected,
|
|
361
|
-
toggle,
|
|
362
|
-
selectedKeys,
|
|
363
|
-
selectAll,
|
|
364
|
-
deselectAll,
|
|
365
|
-
isAllSelected,
|
|
366
|
-
handleCheckboxClick
|
|
367
|
-
} = useSelection(userIds);
|
|
368
|
-
|
|
369
|
-
const handleDelete = async () => {
|
|
370
|
-
await UserService.del(selectedKeys);
|
|
371
|
-
deselectAll();
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<div>
|
|
376
|
-
<Checkbox
|
|
377
|
-
value={isAllSelected}
|
|
378
|
-
onValueChange={isAllSelected ? deselectAll : selectAll}
|
|
379
|
-
label="Select all"
|
|
380
|
-
/>
|
|
381
|
-
<button onClick={handleDelete} disabled={selectedKeys.length === 0}>
|
|
382
|
-
Delete selected ({selectedKeys.length})
|
|
383
|
-
</button>
|
|
384
|
-
|
|
385
|
-
{data?.rows.map((user, index) => (
|
|
386
|
-
<div key={user.id} onClick={(e) => handleCheckboxClick(e, index)}>
|
|
387
|
-
<Checkbox
|
|
388
|
-
value={getSelected(user.id)}
|
|
389
|
-
onValueChange={() => toggle(user.id)}
|
|
390
|
-
/>
|
|
391
|
-
<span>{user.name}</span>
|
|
392
|
-
</div>
|
|
393
|
-
))}
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
**Key points:**
|
|
400
|
-
|
|
401
|
-
- Range selection with Shift + click
|
|
402
|
-
- `selectedKeys`: Array of currently selected keys
|
|
403
|
-
- `isAllSelected`: Whether all items are selected
|
|
404
|
-
|
|
405
|
-
## IdAsyncSelect
|
|
406
|
-
|
|
407
|
-
Component for asynchronously searching and selecting Entity records. The generic type must be specified according to the Entity's Primary Key type.
|
|
408
|
-
|
|
409
|
-
### Basic Usage
|
|
410
|
-
|
|
411
|
-
IdAsyncSelect is typically used as a per-Entity wrapper component:
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
|
|
415
|
-
import { UserAsyncIdConfig } from "@/services/services.generated";
|
|
416
|
-
import type { UserSubsetKey, UserSubsetMapping } from "@/services/sonamu.generated";
|
|
417
|
-
import type { UserListParams } from "@/services/user/user.types";
|
|
418
|
-
|
|
419
|
-
export type UserIdAsyncSelectProps<T extends UserSubsetKey> = {
|
|
420
|
-
subset: T;
|
|
421
|
-
baseListParams?: UserListParams;
|
|
422
|
-
displayField?: keyof UserSubsetMapping[T] & string;
|
|
423
|
-
valueField?: keyof UserSubsetMapping[T] & string;
|
|
424
|
-
placeholder?: string;
|
|
425
|
-
clearable?: boolean;
|
|
426
|
-
disabled?: boolean;
|
|
427
|
-
className?: string;
|
|
428
|
-
multiple?: boolean;
|
|
429
|
-
value?: number | number[] | null; // Number PK
|
|
430
|
-
onValueChange?: (value: number | number[] | undefined) => void;
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
export function UserIdAsyncSelect<T extends UserSubsetKey>({
|
|
434
|
-
subset,
|
|
435
|
-
value,
|
|
436
|
-
onValueChange,
|
|
437
|
-
baseListParams,
|
|
438
|
-
displayField = "name",
|
|
439
|
-
valueField = "id",
|
|
440
|
-
placeholder = "User",
|
|
441
|
-
clearable,
|
|
442
|
-
disabled,
|
|
443
|
-
className,
|
|
444
|
-
multiple = false,
|
|
445
|
-
}: UserIdAsyncSelectProps<T>) {
|
|
446
|
-
return (
|
|
447
|
-
<IdAsyncSelect<number> // Number PK
|
|
448
|
-
config={UserAsyncIdConfig}
|
|
449
|
-
subset={subset}
|
|
450
|
-
baseListParams={baseListParams}
|
|
451
|
-
displayField={displayField}
|
|
452
|
-
valueField={valueField}
|
|
453
|
-
placeholder={placeholder}
|
|
454
|
-
clearable={clearable}
|
|
455
|
-
disabled={disabled}
|
|
456
|
-
className={className}
|
|
457
|
-
multiple={multiple}
|
|
458
|
-
value={value}
|
|
459
|
-
onValueChange={onValueChange}
|
|
460
|
-
/>
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
**Key Props:**
|
|
466
|
-
|
|
467
|
-
- `config`: Auto-generated AsyncIdConfig (EntityAsyncIdConfig form)
|
|
468
|
-
- `subset`: Subset key to query
|
|
469
|
-
- `baseListParams`: List filter parameters
|
|
470
|
-
- `displayField`: Field name to display (default varies by Entity)
|
|
471
|
-
- `valueField`: Field name used as value (default: "id")
|
|
472
|
-
- `multiple`: Whether multi-selection is enabled
|
|
473
|
-
- `value`: Currently selected value (number or string depending on PK type)
|
|
474
|
-
- `onValueChange`: Value change handler
|
|
475
|
-
|
|
476
|
-
### Cascade Dropdown Pattern (Hierarchical Selection)
|
|
477
|
-
|
|
478
|
-
When lower-level lists should change based on higher-level selection (e.g. Department → Division → Lab), pass `baseListParams` dynamically.
|
|
479
|
-
|
|
480
|
-
**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)
|
|
481
|
-
|
|
482
|
-
```tsx
|
|
483
|
-
// Example: 3-level cascade Department → Division → Lab
|
|
484
|
-
function UserForm() {
|
|
485
|
-
const { form, register, setForm } = useTypeForm(UserSaveParams, {
|
|
486
|
-
dept_id: null,
|
|
487
|
-
division_id: null,
|
|
488
|
-
lab_id: null,
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
return (
|
|
492
|
-
<form>
|
|
493
|
-
{/* Level 1: Department selection (full list → preload or default IdAsyncSelect) */}
|
|
494
|
-
<DepartmentIdAsyncSelect
|
|
495
|
-
subset="A"
|
|
496
|
-
{...register("dept_id")}
|
|
497
|
-
onValueChange={(v) => {
|
|
498
|
-
// Reset lower values when department changes
|
|
499
|
-
setForm((prev) => ({ ...prev, dept_id: v ?? null, division_id: null, lab_id: null }));
|
|
500
|
-
}}
|
|
501
|
-
/>
|
|
502
|
-
|
|
503
|
-
{/* Level 2: Division selection (only divisions in selected department) */}
|
|
504
|
-
<DivisionIdAsyncSelect
|
|
505
|
-
subset="A"
|
|
506
|
-
baseListParams={form.dept_id ? { department_id: form.dept_id } : undefined}
|
|
507
|
-
disabled={!form.dept_id}
|
|
508
|
-
{...register("division_id")}
|
|
509
|
-
onValueChange={(v) => {
|
|
510
|
-
// Reset lab when division changes
|
|
511
|
-
setForm((prev) => ({ ...prev, division_id: v ?? null, lab_id: null }));
|
|
512
|
-
}}
|
|
513
|
-
/>
|
|
514
|
-
|
|
515
|
-
{/* Level 3: Lab selection (only labs in selected division) */}
|
|
516
|
-
<LabIdAsyncSelect
|
|
517
|
-
subset="A"
|
|
518
|
-
baseListParams={form.division_id ? { division_id: form.division_id } : undefined}
|
|
519
|
-
disabled={!form.division_id}
|
|
520
|
-
{...register("lab_id")}
|
|
521
|
-
/>
|
|
522
|
-
</form>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
**Notes**:
|
|
528
|
-
|
|
529
|
-
- You must explicitly reset lower values to `null` when a higher-level value changes. IdAsyncSelect does not reset automatically.
|
|
530
|
-
- Using the `disabled` prop to disable lower levels when the parent is not selected improves UX.
|
|
531
|
-
- If `baseListParams` is `undefined`, IdAsyncSelect stays in enabled=false state and does not fetch.
|
|
532
|
-
|
|
533
|
-
**Items to specify in Spec** (recommended to add to acceptanceCriteria in spec.json when cascade is present):
|
|
534
|
-
|
|
535
|
-
```json
|
|
536
|
-
"acceptanceCriteria": [
|
|
537
|
-
"When a department is selected, only divisions belonging to that department appear in the dropdown",
|
|
538
|
-
"When a division is selected, only labs belonging to that division appear in the dropdown",
|
|
539
|
-
"When the department changes, the lower division/lab selections are reset"
|
|
540
|
-
]
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### IMPORTANT: String Primary Key Support
|
|
544
|
-
|
|
545
|
-
Most Entities use Number PK (`IdAsyncSelect<number>`), but better-auth related Entities use String PK.
|
|
546
|
-
|
|
547
|
-
**String PK Entities**: User, Account, Session, Verification
|
|
548
|
-
|
|
549
|
-
**Points to change** (manual modification required after scaffolding):
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
// Number PK (default)
|
|
553
|
-
value?: number | number[] | null;
|
|
554
|
-
onValueChange?: (value: number | number[] | undefined) => void;
|
|
555
|
-
<IdAsyncSelect<number> config={PostAsyncIdConfig} ... />
|
|
556
|
-
|
|
557
|
-
// String PK (User, Account, etc.) — change all 3 places to string
|
|
558
|
-
value?: string | string[] | null;
|
|
559
|
-
onValueChange?: (value: string | string[] | undefined) => void;
|
|
560
|
-
<IdAsyncSelect<string> config={AccountAsyncIdConfig} ... />
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Usage in Forms
|
|
564
|
-
|
|
565
|
-
```tsx
|
|
566
|
-
function PostForm() {
|
|
567
|
-
const { form, setForm, register } = useTypeForm(PostSaveParams, {
|
|
568
|
-
title: "",
|
|
569
|
-
author_id: 0, // or "" for string PK
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
return (
|
|
573
|
-
<form>
|
|
574
|
-
<Input {...register("title")} />
|
|
575
|
-
|
|
576
|
-
{/* Number PK */}
|
|
577
|
-
<UserIdAsyncSelect subset="A" {...register("author_id")} />
|
|
578
|
-
|
|
579
|
-
{/* String PK */}
|
|
580
|
-
<AccountIdAsyncSelect subset="A" {...register("account_id")} />
|
|
581
|
-
</form>
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
## FileInput
|
|
587
|
-
|
|
588
|
-
File upload component (image/general files, eager/lazy modes)
|
|
589
|
-
|
|
590
|
-
```typescript
|
|
591
|
-
import { FileInput } from "@sonamu-kit/react-components/components";
|
|
592
|
-
import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
|
|
593
|
-
|
|
594
|
-
function ProfileForm() {
|
|
595
|
-
const { form, setForm, register, submit } = useTypeForm(ProfileSaveParams, {
|
|
596
|
-
avatar: null, // SonamuFile | File | null
|
|
597
|
-
documents: [], // (SonamuFile | File)[]
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
return (
|
|
601
|
-
<form>
|
|
602
|
-
{/* Single image - eager upload */}
|
|
603
|
-
<FileInput
|
|
604
|
-
{...register("avatar")}
|
|
605
|
-
uploadMode="eager"
|
|
606
|
-
viewMode="image"
|
|
607
|
-
placeholder="Profile image"
|
|
608
|
-
accept="image/*"
|
|
609
|
-
previewSize="md"
|
|
610
|
-
/>
|
|
611
|
-
|
|
612
|
-
{/* Multiple files - lazy upload */}
|
|
613
|
-
<FileInput
|
|
614
|
-
{...register("documents")}
|
|
615
|
-
uploadMode="lazy"
|
|
616
|
-
viewMode="file"
|
|
617
|
-
multiple
|
|
618
|
-
maxFiles={5}
|
|
619
|
-
placeholder="Attach documents"
|
|
620
|
-
/>
|
|
621
|
-
|
|
622
|
-
<button onClick={submit(async (form) => {
|
|
623
|
-
// lazy mode: auto-uploads on submit
|
|
624
|
-
await ProfileService.save([form]);
|
|
625
|
-
})}>Save</button>
|
|
626
|
-
</form>
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
**Props:**
|
|
632
|
-
|
|
633
|
-
- `uploadMode`: `"eager"` (upload immediately) | `"lazy"` (upload on submit)
|
|
634
|
-
- `viewMode`: `"image"` (image preview) | `"file"` (filename)
|
|
635
|
-
- `multiple`: Whether multiple files can be selected
|
|
636
|
-
- `maxFiles`: Maximum number of files
|
|
637
|
-
- `previewSize`: `"sm" | "md" | "lg" | "xl"`
|
|
638
|
-
- `clearable`: Whether the X button can remove the file
|
|
639
|
-
|
|
640
|
-
**IMPORTANT**: uploader function must be configured in SonamuProvider (see below)
|
|
641
|
-
|
|
642
|
-
## Select (Multi-select Mode)
|
|
643
|
-
|
|
644
|
-
Setting `multiple: true` on the `Select` component enables multi-select mode.
|
|
645
|
-
|
|
646
|
-
```typescript
|
|
647
|
-
import { Select } from "@sonamu-kit/react-components/components";
|
|
648
|
-
|
|
649
|
-
function TagForm() {
|
|
650
|
-
const { register } = useTypeForm(PostSaveParams, {
|
|
651
|
-
tag_ids: [], // number[]
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
const items = [
|
|
655
|
-
{ value: 1, label: "JavaScript" },
|
|
656
|
-
{ value: 2, label: "TypeScript" },
|
|
657
|
-
{ value: 3, label: "React" },
|
|
658
|
-
{ value: 4, label: "Vue" },
|
|
659
|
-
];
|
|
660
|
-
|
|
661
|
-
return (
|
|
662
|
-
<Select
|
|
663
|
-
{...register("tag_ids")}
|
|
664
|
-
items={items}
|
|
665
|
-
multiple
|
|
666
|
-
placeholder="Select tags"
|
|
667
|
-
/>
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**Multi-select specific Props:**
|
|
673
|
-
|
|
674
|
-
- `multiple`: `true` (enables multi-select)
|
|
675
|
-
- `maxCount`: Maximum number of badges to display
|
|
676
|
-
- `hideSelectAll`: Hide the select all button
|
|
677
|
-
- `searchable`: Enable search input
|
|
678
|
-
|
|
679
|
-
**Common Props:**
|
|
680
|
-
|
|
681
|
-
- `items`: `SelectItemDef[]` (values only or `{ value, label, disabled }` form)
|
|
682
|
-
- `placeholder`: Text shown before selection
|
|
683
|
-
- `clearable`: X button to deselect all
|
|
684
|
-
- `disabled`: Disable the component
|
|
685
|
-
- `renderItem`: Custom render function
|
|
686
|
-
- `async`: When set to `true`, supports async search via `onSearch` callback
|
|
687
|
-
|
|
688
|
-
## EnumSelect
|
|
689
|
-
|
|
690
|
-
Select integrated with Zod enum (label mapping)
|
|
691
|
-
|
|
692
|
-
```typescript
|
|
693
|
-
import { EnumSelect } from "@sonamu-kit/react-components/components";
|
|
694
|
-
import { z } from "zod";
|
|
695
|
-
|
|
696
|
-
const StatusEnum = z.enum(["draft", "published", "archived"]);
|
|
697
|
-
|
|
698
|
-
const statusLabels = {
|
|
699
|
-
draft: "Draft",
|
|
700
|
-
published: "Published",
|
|
701
|
-
archived: "Archived",
|
|
702
|
-
} as const;
|
|
703
|
-
|
|
704
|
-
function PostForm() {
|
|
705
|
-
const { register } = useTypeForm(PostSaveParams, {
|
|
706
|
-
status: "draft",
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
return (
|
|
710
|
-
<EnumSelect
|
|
711
|
-
{...register("status")}
|
|
712
|
-
enum={StatusEnum}
|
|
713
|
-
labels={statusLabels}
|
|
714
|
-
placeholder="Select status"
|
|
715
|
-
clearable
|
|
716
|
-
/>
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
**Key points:**
|
|
722
|
-
|
|
723
|
-
- Zod enum type safety
|
|
724
|
-
- Display name mapping via labels object
|
|
725
|
-
- Automatically converts enum.options to items
|
|
726
|
-
|
|
727
|
-
## SonamuProvider
|
|
728
|
-
|
|
729
|
-
Global configuration used across react-components
|
|
730
|
-
|
|
731
|
-
```typescript
|
|
732
|
-
// App.tsx or root component
|
|
733
|
-
import { SonamuProvider } from "@sonamu-kit/react-components/contexts";
|
|
734
|
-
import type { SonamuFile } from "@sonamu-kit/react-components/contexts";
|
|
735
|
-
|
|
736
|
-
function App() {
|
|
737
|
-
// File uploader function (used by FileInput, useTypeForm)
|
|
738
|
-
const uploader = async (files: File[]): Promise<SonamuFile[]> => {
|
|
739
|
-
const formData = new FormData();
|
|
740
|
-
files.forEach(file => formData.append("files", file));
|
|
741
|
-
|
|
742
|
-
const response = await fetch("/api/upload", {
|
|
743
|
-
method: "POST",
|
|
744
|
-
body: formData,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
return response.json();
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
// Authentication state (optional)
|
|
751
|
-
const auth = {
|
|
752
|
-
user: currentUser,
|
|
753
|
-
loading: isLoading,
|
|
754
|
-
login: async (params) => { /* ... */ },
|
|
755
|
-
logout: async () => { /* ... */ },
|
|
756
|
-
refetch: async () => { /* ... */ },
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
// Internationalization function (optional)
|
|
760
|
-
const SD = (key: string) => dictionary[key] ?? key;
|
|
761
|
-
|
|
762
|
-
return (
|
|
763
|
-
<SonamuProvider uploader={uploader} auth={auth} SD={SD}>
|
|
764
|
-
{children}
|
|
765
|
-
</SonamuProvider>
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
**Required Props:**
|
|
771
|
-
|
|
772
|
-
- `uploader`: `(files: File[]) => Promise<SonamuFile[]>` - Used by FileInput
|
|
773
|
-
- `auth`: Authentication state and functions (optional)
|
|
774
|
-
- `SD`: Internationalization function (optional)
|
|
775
|
-
|
|
776
|
-
## Utility Functions
|
|
777
|
-
|
|
778
|
-
```typescript
|
|
779
|
-
import {
|
|
780
|
-
dateF,
|
|
781
|
-
datetimeF,
|
|
782
|
-
numF,
|
|
783
|
-
hidden,
|
|
784
|
-
arrayableToArray,
|
|
785
|
-
sqlDateToDateString,
|
|
786
|
-
} from "@sonamu-kit/react-components/lib";
|
|
787
|
-
|
|
788
|
-
// Date formatting
|
|
789
|
-
dateF(new Date()); // "2024-01-15"
|
|
790
|
-
dateF("2024-01-15T10:30:00"); // "2024-01-15"
|
|
791
|
-
datetimeF(new Date()); // "2024-01-15 10:30:00"
|
|
792
|
-
|
|
793
|
-
// Number formatting
|
|
794
|
-
numF(1234567); // "1,234,567"
|
|
795
|
-
|
|
796
|
-
// Conditional hidden class
|
|
797
|
-
<div className={hidden(isHidden)}>...</div>
|
|
798
|
-
|
|
799
|
-
// SQL date → date string
|
|
800
|
-
sqlDateToDateString("2024-01-15T10:30:00.000Z"); // "2024-01-15"
|
|
801
|
-
|
|
802
|
-
// Convert to array
|
|
803
|
-
arrayableToArray("single"); // ["single"]
|
|
804
|
-
arrayableToArray(["a", "b"]); // ["a", "b"]
|
|
805
|
-
arrayableToArray(undefined); // []
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
## Error Handling
|
|
809
|
-
|
|
810
|
-
```typescript
|
|
811
|
-
import { isSonamuError } from "@/lib/sonamu.shared";
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
await UserService.save([
|
|
815
|
-
{
|
|
816
|
-
/* ... */
|
|
817
|
-
},
|
|
818
|
-
]);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
if (isSonamuError(error)) {
|
|
821
|
-
console.log("Status:", error.code);
|
|
822
|
-
console.log("Message:", error.message);
|
|
823
|
-
error.issues.forEach((issue) => {
|
|
824
|
-
console.log(`${issue.path.join(".")}: ${issue.message}`);
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
## SSR
|
|
831
|
-
|
|
832
|
-
```typescript
|
|
833
|
-
// api/src/ssr/routes.ts
|
|
834
|
-
import { registerSSR } from "sonamu/ssr";
|
|
835
|
-
|
|
836
|
-
registerSSR({
|
|
837
|
-
path: "/companies/:companyId",
|
|
838
|
-
preload: (params) => [UserService.me(), CompanyService.findById("A", Number(params.companyId))],
|
|
839
|
-
});
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
## Initial Project Setup
|
|
843
|
-
|
|
844
|
-
**→ See the "Project Name Change" section in `create-sonamu.md`** (change 4 files: index.html, \_\_root.tsx, index.tsx, Sidebar.tsx)
|
|
845
|
-
|
|
846
|
-
## Rules
|
|
847
|
-
|
|
848
|
-
- NEVER manually modify `services.generated.ts`
|
|
849
|
-
- MUST specify Subset parameter when calling APIs
|
|
850
|
-
- Use `Promise.all([...])` for parallel requests
|
|
851
|
-
|
|
852
|
-
---
|
|
853
|
-
|
|
854
|
-
## Full Component Implementation Examples
|
|
855
|
-
|
|
856
|
-
### List Page
|
|
857
|
-
|
|
858
|
-
```typescript
|
|
859
|
-
function ConsultationListPage() {
|
|
860
|
-
const [params, setParams] = useState({ num: 20, page: 1 });
|
|
861
|
-
const { data, isLoading } = ConsultationService.useConsultations("P", params);
|
|
862
|
-
|
|
863
|
-
return (
|
|
864
|
-
<div>
|
|
865
|
-
{/* Manage selection with useSelection */}
|
|
866
|
-
{data?.rows.map((row) => (
|
|
867
|
-
<div key={row.id}>{row.title} - {row.status}</div>
|
|
868
|
-
))}
|
|
869
|
-
{/* Pagination: manipulate params.page */}
|
|
870
|
-
</div>
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
### Edit Page
|
|
876
|
-
|
|
877
|
-
```typescript
|
|
878
|
-
function ConsultationFormPage() {
|
|
879
|
-
const { id } = useParams();
|
|
880
|
-
const { form, setForm, register, submit } = useTypeForm(ConsultationSaveParams, {
|
|
881
|
-
title: "", content: "", status: "pending", user_id: 0,
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
// Edit mode: load data
|
|
885
|
-
useEffect(() => {
|
|
886
|
-
if (id) ConsultationService.getConsultation("A", Number(id)).then((row) => setForm((prev) => ({ ...prev, ...row })));
|
|
887
|
-
}, [id]);
|
|
888
|
-
|
|
889
|
-
const saveMutation = ConsultationService.useSaveMutation();
|
|
890
|
-
const handleSubmit = submit(async (form) => {
|
|
891
|
-
const [cId] = await saveMutation.mutateAsync({ spa: [form] });
|
|
892
|
-
navigate(`/consultations/${cId}`);
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
return (
|
|
896
|
-
<form>
|
|
897
|
-
<Input {...register("title")} />
|
|
898
|
-
<Textarea {...register("content")} />
|
|
899
|
-
<Select {...register("status")} items={[{value:"pending",label:"Pending"},{value:"completed",label:"Completed"}]} />
|
|
900
|
-
<button onClick={handleSubmit} disabled={saveMutation.isPending}>Save</button>
|
|
901
|
-
</form>
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
### Cache Invalidation
|
|
907
|
-
|
|
908
|
-
```typescript
|
|
909
|
-
const queryClient = useQueryClient();
|
|
910
|
-
await ConsultationService.changeStatus(id, newStatus, "Status change");
|
|
911
|
-
queryClient.invalidateQueries({
|
|
912
|
-
queryKey: ["Consultation", "findById", "A", id],
|
|
913
|
-
});
|
|
914
|
-
queryClient.invalidateQueries({ queryKey: ["Consultation", "findMany"] });
|
|
915
|
-
```
|