feat(admin-ui): visual schema builder для создания/редактирования справочников

Новый функционал — заводить свои справочники через UI без code deploy:

API:
- CreateDictionaryRequest type в client.ts
- useCreateDictionary / useUpdateDictionary mutations с invalidate
  ['dictionaries'] и ['dictionary', name]

Компоненты (src/components/schema/):
- types.ts: PropertyDef + buildSchemaJson() (Draft-07) + parseSchemaJson()
  для двусторонней конвертации между UI state и schema_json
- PropertyEditor: name + kind select + required + description + type-specific
  опции (minLength/maxLength/pattern для string; minimum/maximum для number;
  enum values; uniqueItems для array). Up/Down/Trash icon buttons
- SchemaBuilder: список property cards + кнопка «добавить» + EmptyState
- DictionaryEditorDialog: модалка с 3 табами:
  * Метаданные (name/displayName/description/scope/bundle/version/locales)
  * Поля (SchemaBuilder)
  * JSON (readonly preview сгенерированной schema_json)

Поддерживаемые типы полей: string, integer, number, boolean, enum,
localized (i18n object с patternProperties), date (format=date),
datetime (format=date-time), array_string (с опц. enum + uniqueItems).

Wiring:
- /dictionaries: PageHeader actions = «+ Создать справочник» → Dialog в режиме create
  → onSuccess navigate на /dictionaries/{name}
- /dictionaries/$name: PageHeader actions расширен «Схема» (secondary) +
  «Создать запись» (primary). Dialog в edit-режиме prefilled из
  detailQuery.data + parseSchemaJson()

i18n: 30+ новых ключей в schema.* / scope.* (RU/EN)
This commit is contained in:
Zimin A.N.
2026-05-04 03:12:56 +03:00
parent ef31499bd9
commit 987559a6ab
9 changed files with 860 additions and 13 deletions
@@ -20,11 +20,12 @@ import {
TableRow,
TextInput,
} from '@nstart/ui'
import { PlusIcon, PencilSimpleIcon, XCircleIcon } from '@phosphor-icons/react'
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon } from '@phosphor-icons/react'
import { useDictionaryDetail, useRecordRaw, useRecords } from '@/api/queries'
import { useCreateRecord, useUpdateRecord, useCloseRecord } from '@/api/mutations'
import type { CreateRecordRequest, FlattenedRecord } from '@/api/client'
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
export const Route = createFileRoute('/dictionaries/$name')({
component: DictionaryDetail,
@@ -47,6 +48,7 @@ function DictionaryDetail() {
const [edit, setEdit] = useState<EditState>({ kind: 'closed' })
const [closeReason, setCloseReason] = useState('')
const [schemaEditOpen, setSchemaEditOpen] = useState(false)
const editingKey = edit.kind === 'edit' ? edit.record.businessKey : undefined
const rawRecordQuery = useRecordRaw(name, editingKey)
@@ -98,14 +100,24 @@ function DictionaryDetail() {
: `${totalRecords} ${t('dict.list.records')}`
}
actions={
<Button
variant="primary"
leftIcon={<PlusIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setEdit({ kind: 'create' })}
>
{t('dict.action.create')}
</Button>
<div className="flex items-center gap-2">
<Button
variant="secondary"
leftIcon={<GearIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setSchemaEditOpen(true)}
>
{t('schema.action.editSchema')}
</Button>
<Button
variant="primary"
leftIcon={<PlusIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setEdit({ kind: 'create' })}
>
{t('dict.action.create')}
</Button>
</div>
}
/>
@@ -278,6 +290,15 @@ function DictionaryDetail() {
</div>
)}
</Modal>
{detailQuery.data && (
<DictionaryEditorDialog
open={schemaEditOpen}
mode={{ kind: 'edit', existing: detailQuery.data }}
onClose={() => setSchemaEditOpen(false)}
onSuccess={() => setSchemaEditOpen(false)}
/>
)}
</div>
)
}