Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.0 KiB
Pattern Declarative KIND_CONFIG Record — Single-page multi-kind CRUD reuse
Discovered: S29 Plan CA (Master/Catalogs/CatalogsPage.tsx 321 LOC) — first proven instance. Reinforced: S35 G-H2 FE Admin (Hrm/HrmConfigsPage.tsx ~470 LOC) — Pattern 2× cumulative cross-module.
Pattern
Single-page handle N kind of catalog/config via URL :kind param + declarative KIND_CONFIG: Record<Kind, ...>. KHÔNG split N page riêng (clutter routes + duplicate boilerplate).
Structure
type Kind = 'kind-a' | 'kind-b' | ... // union literal (TS6 erasableSyntaxOnly)
type FieldDef = {
key: string
label: string
type: 'text' | 'textarea' | 'checkbox' | 'number' | 'date' | 'time' | 'select' | 'multiselect-weekday' | ...
required?: boolean
placeholder?: string
step?: number
options?: Array<{value, label}>
}
const KIND_CONFIG: Record<Kind, {
label: string
icon: React.ComponentType<{className?: string}>
fields: FieldDef[]
columns: string[]
}> = {
'kind-a': { label, icon, fields: [...], columns: [...] },
'kind-b': { ... },
}
export function CatalogPage() {
const { kind } = useParams<{kind?: string}>()
const config = KIND_CONFIG[kind ?? 'default']
// Render sub-tab nav + table + Dialog form (declarative renderField per FieldDef.type)
}
Decision
Khi nào dùng Pattern Declarative KIND_CONFIG (vs split page):
- N kind ≤ 6, mỗi kind có fields shape DIFFERENT nhưng UX flow SAME (List + Create/Edit dialog + Delete)
- KHÔNG có per-kind business logic phức tạp (workflow / approval flow / codeGen)
- Mỗi kind chỉ khác fields list + columns
- Total page LOC ~300-500 (acceptable single-file maintain)
KHÔNG dùng khi:
- Per-kind business logic phức tạp → split per-page (vd Contract type 7 có 7 form khác + 9 phase workflow)
- N kind > 6 → file > 1000 LOC khó scan
- Fields shape differ rất nhiều (vd file upload field + signature picker mix vào)
Example proven cumulative
Plan CA Chunk B (S29) — Master/Catalogs
- 4 kind: units / materials / services / work-items
- File:
fe-admin/src/pages/master/CatalogsPage.tsx321 LOC + mirror fe-user - Field shape: code/name + variant per kind (defaultUnit/category/originCountry/etc.)
- BE:
/api/catalogs/{kind}× 4 verb mirror controller
S35 G-H2 (this session) — Hrm/HrmConfigs
- 4 kind: leave-types / holidays / shifts / ot-policies
- File:
fe-admin/src/pages/hrm/HrmConfigsPage.tsx~470 LOC + mirror fe-user - Field shape: complex (multiselect-weekday cho ShiftPattern WorkDays + TimeOnly cho startTime/endTime + 3 multiplier OtPolicy)
- BE:
/api/hrm-configs/{kind}× 4 verb mirror controller - NEW FieldDef.type extend:
number/date/time/select/multiselect-weekday
renderField helper extend pattern
Mỗi kind có thể introduce NEW field type → extend renderField(field, value, onChange) switch:
text/date/time→<Input type={field.type}>number→<Input type="number" step={field.step}>textarea→<Textarea>checkbox→<input type="checkbox">select→<select>vớifield.optionsmultiselect-weekday(NEW S35) → 7 checkbox Mon-Sun + comma-join string
Reusable cross-project
- VIPIX: Tags + Categories + Collections (3 catalog) — Pattern fit
- NAMGROUP: Loại NV + Phòng ban + Vị trí (3 catalog đơn giản) — Pattern fit
- ASHICO: 5-site config + 7-DB selector → có thể adapt
- DH_Y_DUOC: Loại sáng kiến + Cấp duyệt (2 catalog) — Pattern fit
- BVAAU: Loại bệnh nhân + Bảo hiểm (2-3 catalog) — Pattern fit
Pattern relationship
| Pattern | Scope | Token cost | Notes |
|---|---|---|---|
| Declarative KIND_CONFIG | Single-page multi-kind CRUD | ~25K Implementer spawn | 1 page + KIND_CONFIG Record |
| 12-bis | Cross-module mirror BE | ~25K Implementer spawn | BE mega 4 region |
| 12-ter | Within-module N-satellite | ~22K spawn | 1 mega features + N region |
| 16-bis | Cross-app FE 4-place mirror | ~15K Implementer spawn | types + page + Routes + Layout staticMap |
Declarative KIND_CONFIG + 12-bis + 16-bis = Trinity pattern cho master catalog feature S29 + S35 proven 2×.
Anti-pattern
❌ KHÔNG split N page per kind — clutter routes + duplicate boilerplate State/Mutation/Dialog
❌ KHÔNG hardcode per-kind switch trong render JSX — dùng KIND_CONFIG Record + renderField switch tách logic
❌ KHÔNG quên FieldDef.type extend khi NEW kind có shape khác — vd time picker / multiselect
Verify checklist
- 1 page với
:kindURL param +useParamsextract KIND_CONFIG: Record<Kind, {...}>declarative N kind config- 1 useQuery list + 1 useMutation save (Create + Update branch via isEdit) + 1 useMutation remove
- Sub-tab nav 4 kind (NavLink active state per kind)
- Dialog modal CRUD generic (KHÔNG per-kind dialog)
renderField()helper switch case per FieldDef.type- BE URL
/api/{prefix}/{kind}× 4 verb (List + Create + Update + Delete) mirror controller - Mirror SHA256 IDENTICAL × 2 app (Pattern 16-bis)
- Layout.tsx staticMap entry N leaf (Pattern 16-bis 4-place)