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:
@@ -0,0 +1,63 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, EmptyState } from '@nstart/ui'
|
||||
import { PlusIcon } from '@phosphor-icons/react'
|
||||
import { PropertyEditor } from './PropertyEditor'
|
||||
import { newPropertyDef, type PropertyDef } from './types'
|
||||
|
||||
type Props = {
|
||||
properties: PropertyDef[]
|
||||
onChange: (next: PropertyDef[]) => void
|
||||
}
|
||||
|
||||
export const SchemaBuilder = ({ properties, onChange }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const updateAt = (index: number, next: PropertyDef) => {
|
||||
const copy = properties.slice()
|
||||
copy[index] = next
|
||||
onChange(copy)
|
||||
}
|
||||
|
||||
const removeAt = (index: number) => {
|
||||
onChange(properties.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const moveBy = (index: number, delta: number) => {
|
||||
const target = index + delta
|
||||
if (target < 0 || target >= properties.length) return
|
||||
const copy = properties.slice()
|
||||
const [item] = copy.splice(index, 1)
|
||||
copy.splice(target, 0, item)
|
||||
onChange(copy)
|
||||
}
|
||||
|
||||
const add = () => onChange([...properties, newPropertyDef()])
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{properties.length === 0 && (
|
||||
<EmptyState title={t('schema.empty')} />
|
||||
)}
|
||||
{properties.map((p, i) => (
|
||||
<PropertyEditor
|
||||
key={p.id}
|
||||
prop={p}
|
||||
index={i}
|
||||
total={properties.length}
|
||||
onChange={(next) => updateAt(i, next)}
|
||||
onRemove={() => removeAt(i)}
|
||||
onMoveUp={() => moveBy(i, -1)}
|
||||
onMoveDown={() => moveBy(i, 1)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
leftIcon={<PlusIcon weight="bold" size={16} />}
|
||||
onClick={add}
|
||||
>
|
||||
{t('schema.addProperty')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user