import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useQueryClient } from '@tanstack/react-query' import axios from 'axios' import { Alert, Button, FormActions, Modal, MultiSelect, SingleSelect, Tabs, TextArea, TextInput, type SelectOption, type TabItem, } from '@/ui' import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations' import { dictionaryDetailQuery, useDictionaries } from '@/api/queries' import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client' import { SchemaBuilder } from './SchemaBuilder' import { EventsPreviewTab } from './EventsPreviewTab' import { buildSchemaJson, diffSchemas, hasBreakingChanges, parseSchemaJson, suggestVersionBump, type PropertyDef, type SchemaChange, } from './types' type Mode = | { kind: 'create' } | { kind: 'edit'; existing: DictionaryDetail } type Props = { open: boolean mode: Mode onClose: () => void onSuccess: (name: string) => void } const DEFAULT_LOCALES = ['ru-RU', 'en-US', 'zh-CN'] const SCOPE_OPTIONS = (t: (k: string) => string): SelectOption[] => [ { id: 'PUBLIC', label: t('scope.PUBLIC') }, { id: 'INTERNAL', label: t('scope.INTERNAL') }, { id: 'RESTRICTED', label: t('scope.RESTRICTED') }, ] type EditorTab = 'metadata' | 'schema' | 'preview' | 'events' export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props) => { const { t } = useTranslation() const createMut = useCreateDictionary() const updateMut = useUpdateDictionary() const isEdit = mode.kind === 'edit' const initial = mode.kind === 'edit' ? mode.existing : null const parsed = useMemo(() => parseSchemaJson(initial?.schemaJson), [initial]) const [activeTab, setActiveTab] = useState('metadata') const [name, setName] = useState(initial?.name ?? '') const [displayName, setDisplayName] = useState(initial?.displayName ?? '') const [description, setDescription] = useState(initial?.description ?? '') const [scope, setScope] = useState(initial?.scope ?? 'PUBLIC') const [bundle, setBundle] = useState(initial?.bundle ?? 'cuod') const [schemaVersion, setSchemaVersion] = useState(initial?.schemaVersion ?? '1.0.0') const [supportedLocales, setSupportedLocales] = useState( initial?.supportedLocales ?? ['ru-RU'], ) const [defaultLocale, setDefaultLocale] = useState(initial?.defaultLocale ?? 'ru-RU') const [redisProjection, setRedisProjection] = useState( initial?.redisProjectionEnabled ?? false, ) const [approvalRequired, setApprovalRequired] = useState( (initial as { approvalRequired?: boolean } | null)?.approvalRequired ?? false, ) const [approvalMinRole, setApprovalMinRole] = useState( (initial as { approvalMinRole?: string } | null)?.approvalMinRole ?? '', ) const [properties, setProperties] = useState(parsed.properties) const [idSource, setIdSource] = useState(parsed.idSource ?? '') const [nameError, setNameError] = useState(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( () => buildSchemaJson(properties, idSource || undefined), [properties, idSource], ) const changes: SchemaChange[] = useMemo( () => (isEdit ? diffSchemas(initial?.schemaJson, schemaJson) : []), [isEdit, initial, schemaJson], ) const breaking = hasBreakingChanges(changes) const suggestedVersion = useMemo( () => (isEdit ? suggestVersionBump(initial?.schemaVersion, changes) : schemaVersion), [isEdit, initial, changes, schemaVersion], ) const activeMutation = isEdit ? updateMut : createMut const serverError = serverErrorMessage(activeMutation.error) const localeOptions: SelectOption[] = DEFAULT_LOCALES.map((l) => ({ id: l, label: l })) const defaultLocaleOptions: SelectOption[] = supportedLocales.map((l) => ({ id: l, label: l })) const handleSubmit = () => { if (!isEdit && !/^[a-z][a-z0-9_]{1,127}$/.test(name)) { setNameError(t('schema.nameInvalid')) setActiveTab('metadata') return } setNameError(null) if (isEdit && breaking) { setActiveTab('metadata') return } const payload: CreateDictionaryRequest = { name, displayName: displayName || undefined, description: description || undefined, scope, schemaJson, schemaVersion: schemaVersion || undefined, bundle: bundle || undefined, supportedLocales: supportedLocales.length > 0 ? supportedLocales : undefined, defaultLocale: defaultLocale || undefined, redisProjectionEnabled: redisProjection, approvalRequired, approvalMinRole: approvalMinRole.trim() || undefined, } if (isEdit) { updateMut.mutate( { name: initial!.name, payload }, { onSuccess: () => onSuccess(initial!.name) }, ) } else { createMut.mutate(payload, { onSuccess: (resp) => onSuccess(resp.name) }) } } const tabs: TabItem[] = [ { id: 'metadata', label: t('schema.tabs.metadata') }, { id: 'schema', label: t('schema.tabs.schema') }, { id: 'preview', label: t('schema.tabs.preview') }, { id: 'events', label: t('schema.tabs.events') }, ] return (
setActiveTab(id as EditorTab)} />
{!isEdit && (
{ void handleApplyTemplate(id) }} placeholder={ dictsQuery.isLoading || loadingTemplate ? '…' : t('schema.template.placeholder') } disabled={dictsQuery.isLoading || loadingTemplate} />
)}
setName(e.target.value)} /> setDisplayName(e.target.value)} />