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:
@@ -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')}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user