diff --git a/ordinis-admin-ui/src/api/client.ts b/ordinis-admin-ui/src/api/client.ts index f597337..38d924c 100644 --- a/ordinis-admin-ui/src/api/client.ts +++ b/ordinis-admin-ui/src/api/client.ts @@ -88,6 +88,18 @@ export type RecordResponse = { version: number } +export type CreateDictionaryRequest = { + name: string + displayName?: string + description?: string + scope: DataScope + schemaJson: JsonSchema + schemaVersion?: string + bundle?: string + supportedLocales?: string[] + defaultLocale?: string +} + export type CreateRecordRequest = { businessKey: string data: Record diff --git a/ordinis-admin-ui/src/api/mutations.ts b/ordinis-admin-ui/src/api/mutations.ts index 2616386..76b493c 100644 --- a/ordinis-admin-ui/src/api/mutations.ts +++ b/ordinis-admin-ui/src/api/mutations.ts @@ -1,7 +1,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { apiClient, + type CreateDictionaryRequest, type CreateRecordRequest, + type DictionaryDetail, type RecordResponse, } from './client' @@ -10,6 +12,39 @@ const idempotencyKey = () => ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}` +export const useCreateDictionary = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: async (req: CreateDictionaryRequest): Promise => { + const { data } = await apiClient.post('/dictionaries', req) + return data + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['dictionaries'] }) + }, + }) +} + +export const useUpdateDictionary = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: async (params: { + name: string + payload: CreateDictionaryRequest + }): Promise => { + const { data } = await apiClient.put( + `/dictionaries/${params.name}`, + params.payload, + ) + return data + }, + onSuccess: (_, params) => { + qc.invalidateQueries({ queryKey: ['dictionaries'] }) + qc.invalidateQueries({ queryKey: ['dictionary', params.name] }) + }, + }) +} + export const useCreateRecord = (dictionaryName: string) => { const qc = useQueryClient() return useMutation({ diff --git a/ordinis-admin-ui/src/components/schema/DictionaryEditorDialog.tsx b/ordinis-admin-ui/src/components/schema/DictionaryEditorDialog.tsx new file mode 100644 index 0000000..8a444d6 --- /dev/null +++ b/ordinis-admin-ui/src/components/schema/DictionaryEditorDialog.tsx @@ -0,0 +1,224 @@ +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import axios from 'axios' +import { + Alert, + Button, + FormActions, + Modal, + MultiSelect, + SingleSelect, + Tabs, + TextArea, + TextInput, + type SelectOption, + type TabItem, +} from '@nstart/ui' +import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations' +import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client' +import { SchemaBuilder } from './SchemaBuilder' +import { buildSchemaJson, parseSchemaJson, type PropertyDef } 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' + +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 [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 [properties, setProperties] = useState( + () => parseSchemaJson(initial?.schemaJson), + ) + const [nameError, setNameError] = useState(null) + + const schemaJson = useMemo(() => buildSchemaJson(properties), [properties]) + + 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) + + 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, + } + + 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') }, + ] + + return ( + +
+ setActiveTab(id as EditorTab)} /> + +
+
+ setName(e.target.value)} + /> + setDisplayName(e.target.value)} + /> +
+