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
@@ -1,7 +1,10 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useState } from 'react'
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Alert, Badge, EmptyState, LoadingBlock, PageHeader } from '@nstart/ui'
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader } from '@nstart/ui'
import { PlusIcon } from '@phosphor-icons/react'
import { useDictionaries } from '@/api/queries'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
export const Route = createFileRoute('/dictionaries/')({
component: DictionariesPage,
@@ -9,7 +12,20 @@ export const Route = createFileRoute('/dictionaries/')({
function DictionariesPage() {
const { t } = useTranslation()
const navigate = useNavigate()
const { data, isLoading, error } = useDictionaries()
const [createOpen, setCreateOpen] = useState(false)
const createButton = (
<Button
type="button"
variant="primary"
leftIcon={<PlusIcon weight="bold" size={16} />}
onClick={() => setCreateOpen(true)}
>
{t('schema.action.create')}
</Button>
)
if (isLoading) {
return <LoadingBlock size="md" label={t('loading')} />
@@ -24,12 +40,34 @@ function DictionariesPage() {
}
if (!data || data.length === 0) {
return <EmptyState title={t('dict.empty')} />
return (
<div className="space-y-6">
<PageHeader
title={t('nav.dictionaries')}
description={t('dict.list.subtitle')}
actions={createButton}
/>
<EmptyState title={t('dict.empty')} />
<DictionaryEditorDialog
open={createOpen}
mode={{ kind: 'create' }}
onClose={() => setCreateOpen(false)}
onSuccess={(name) => {
setCreateOpen(false)
navigate({ to: '/dictionaries/$name', params: { name } })
}}
/>
</div>
)
}
return (
<div className="space-y-6">
<PageHeader title={t('nav.dictionaries')} description={t('dict.list.subtitle')} />
<PageHeader
title={t('nav.dictionaries')}
description={t('dict.list.subtitle')}
actions={createButton}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.map((d) => (
@@ -58,6 +96,16 @@ function DictionariesPage() {
</Link>
))}
</div>
<DictionaryEditorDialog
open={createOpen}
mode={{ kind: 'create' }}
onClose={() => setCreateOpen(false)}
onSuccess={(name) => {
setCreateOpen(false)
navigate({ to: '/dictionaries/$name', params: { name } })
}}
/>
</div>
)
}