feat(schema): создание словаря по шаблону существующего

Для семейств похожих словарей (satellite_type / consumer_type /
processing_level — все 'code+name+description' с таким же scope)
admin копировал JSON руками или дублировал поля в Поля-табе. Это
~3 минуты на каждый.

Теперь в create-mode dictionary editor сверху появился блок 'Создать
по шаблону': SingleSelect списка существующих словарей. На выбор
fetch'ится detail и заполняются:
- description, scope, bundle
- supportedLocales / defaultLocale
- properties (через parseSchemaJson — round-trip preserved для FK,
  enum, localized, opaque kinds)
- idSource

Не трогаем:
- name (admin вводит уникальный имя — это валидация на 422)
- displayName (специфичен для нового словаря)
- schemaVersion (resets на 1.0.0)

Чисто client-side: queryClient.fetchQuery(dictionaryDetailQuery(name))
— TanStack Query сам кеширует результат и для users 'Изменить схему'
позже. Не требует backend изменений.

Edit-mode не показывает шаблон — там initial уже имеет schema
существующего словаря.
This commit is contained in:
Zimin A.N.
2026-05-06 21:41:21 +03:00
parent f2cf34e2fc
commit d8686c0352
2 changed files with 66 additions and 0 deletions
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import axios from 'axios' import axios from 'axios'
import { import {
Alert, Alert,
@@ -15,6 +16,7 @@ import {
type TabItem, type TabItem,
} from '@nstart/ui' } from '@nstart/ui'
import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations' import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations'
import { dictionaryDetailQuery, useDictionaries } from '@/api/queries'
import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client' import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client'
import { SchemaBuilder } from './SchemaBuilder' import { SchemaBuilder } from './SchemaBuilder'
import { import {
@@ -73,6 +75,44 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
const [idSource, setIdSource] = useState<string>(parsed.idSource ?? '') const [idSource, setIdSource] = useState<string>(parsed.idSource ?? '')
const [nameError, setNameError] = useState<string | null>(null) const [nameError, setNameError] = useState<string | null>(null)
// Template (только в create-mode): admin выбирает существующий dict,
// его schema + locale + scope копируются. Имя остаётся пустым (admin
// вводит уникальное), version reset на 1.0.0.
const queryClient = useQueryClient()
const dictsQuery = useDictionaries()
const [loadingTemplate, setLoadingTemplate] = useState(false)
const templateOptions: SelectOption[] = useMemo(() => {
if (isEdit) return []
const list = dictsQuery.data ?? []
return list
.map((d) => ({
id: d.name,
label: d.displayName ? `${d.name}${d.displayName}` : d.name,
}))
.sort((a, b) => a.label.localeCompare(b.label, 'ru'))
}, [dictsQuery.data, isEdit])
const handleApplyTemplate = async (templateName: string) => {
if (!templateName) return
setLoadingTemplate(true)
try {
const detail = await queryClient.fetchQuery(dictionaryDetailQuery(templateName))
const tplParsed = parseSchemaJson(detail.schemaJson)
// Не трогаем name — admin должен ввести уникальное.
setDisplayName('')
setDescription(detail.description ?? '')
setScope(detail.scope)
setBundle(detail.bundle ?? 'cuod')
setSchemaVersion('1.0.0')
setSupportedLocales(detail.supportedLocales ?? ['ru-RU'])
setDefaultLocale(detail.defaultLocale ?? 'ru-RU')
setProperties(tplParsed.properties)
setIdSource(tplParsed.idSource ?? '')
} finally {
setLoadingTemplate(false)
}
}
const schemaJson = useMemo( const schemaJson = useMemo(
() => buildSchemaJson(properties, idSource || undefined), () => buildSchemaJson(properties, idSource || undefined),
[properties, idSource], [properties, idSource],
@@ -147,6 +187,26 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
<Tabs items={tabs} value={activeTab} onValueChange={(id) => setActiveTab(id as EditorTab)} /> <Tabs items={tabs} value={activeTab} onValueChange={(id) => setActiveTab(id as EditorTab)} />
<div className={activeTab === 'metadata' ? 'block' : 'hidden'}> <div className={activeTab === 'metadata' ? 'block' : 'hidden'}>
{!isEdit && (
<div className="mb-3 p-3 rounded-md border border-ultramarain/20 bg-ultramarain/4">
<SingleSelect
label={t('schema.template.label')}
hint={t('schema.template.hint')}
options={templateOptions}
value=""
onChange={(id) => {
void handleApplyTemplate(id)
}}
placeholder={
dictsQuery.isLoading || loadingTemplate
? '…'
: t('schema.template.placeholder')
}
disabled={dictsQuery.isLoading || loadingTemplate}
/>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<TextInput <TextInput
label={t('schema.name')} label={t('schema.name')}
+6
View File
@@ -176,6 +176,9 @@ i18n
'schema.scope': 'Scope', 'schema.scope': 'Scope',
'schema.bundle': 'Bundle', 'schema.bundle': 'Bundle',
'schema.bundleHint': 'Обычно cuod', 'schema.bundleHint': 'Обычно cuod',
'schema.template.label': 'Создать по шаблону',
'schema.template.hint': 'Скопирует схему, scope и locale выбранного словаря. Имя оставьте уникальное; версия сбросится на 1.0.0.',
'schema.template.placeholder': '— без шаблона —',
'schema.version': 'Версия схемы', 'schema.version': 'Версия схемы',
'schema.supportedLocales': 'Поддерживаемые локали', 'schema.supportedLocales': 'Поддерживаемые локали',
'schema.defaultLocale': 'Локаль по умолчанию', 'schema.defaultLocale': 'Локаль по умолчанию',
@@ -432,6 +435,9 @@ i18n
'schema.scope': 'Scope', 'schema.scope': 'Scope',
'schema.bundle': 'Bundle', 'schema.bundle': 'Bundle',
'schema.bundleHint': 'Usually cuod', 'schema.bundleHint': 'Usually cuod',
'schema.template.label': 'Create from template',
'schema.template.hint': 'Copies schema, scope and locales from the selected dictionary. Use a unique name; version resets to 1.0.0.',
'schema.template.placeholder': '— no template —',
'schema.version': 'Schema version', 'schema.version': 'Schema version',
'schema.supportedLocales': 'Supported locales', 'schema.supportedLocales': 'Supported locales',
'schema.defaultLocale': 'Default locale', 'schema.defaultLocale': 'Default locale',