From ef31499bd9e492874b1ebd6bdd8184816347dd5f Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 4 May 2026 03:01:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(admin-ui):=20DatePicker=20+=20nested=20obje?= =?UTF-8?q?ct=20+=20raw=20record=20fetch=20=D0=B4=D0=BB=D1=8F=20Edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Несколько связанных багов в форме записей: DatePicker: - launch_date / decay_date (format=date) и validFrom/validTo рендерились как raw text input → теперь @nstart/ui DatePicker с RU локалью. - DatePickerProvider добавлен в main.tsx с DATE_PICKER_LOCALE_RU. - Far-future infinity dates (year>=9999) скрываются (пустое поле). - ISO date format на submit: yyyy-MM-dd. Nested object → JsonField fallback: - orbit (type=object с raan_deg/eccentricity/...) и antennas (array of object) показывали [object Object] в text input. Добавлен JsonField — TextArea с JSON.stringify/parse, локальный state для невалидного промежуточного ввода + inline parse error. Edit raw record fetch: - Read-api flatten'ит i18n в строку выбранной локали → form для edit получал data.name = 'Terra' вместо {ru-RU, en-US}. На Edit теперь fetch raw record через writer GET /api/v1/dictionaries/{name}/records/{businessKey}. - nginx regex location ~ ^/api/v1/dictionaries/ → writer (вместо узкого regex только для /dictionaries/{name}). GET schema, GET raw record, mutations — всё на writer. Read-api держит /api/v1/{name}/records/* (другой URL pattern). - LoadingBlock пока тащим raw запись + Alert на ошибке. --- ordinis-admin-ui/nginx.conf | 7 +- ordinis-admin-ui/src/api/queries.ts | 20 +++ .../src/components/form/SchemaDrivenForm.tsx | 147 +++++++++++++++++- ordinis-admin-ui/src/main.tsx | 6 +- .../src/routes/dictionaries.$name.tsx | 53 +++++-- 5 files changed, 205 insertions(+), 28 deletions(-) diff --git a/ordinis-admin-ui/nginx.conf b/ordinis-admin-ui/nginx.conf index 9fcaaed..506cc82 100644 --- a/ordinis-admin-ui/nginx.conf +++ b/ordinis-admin-ui/nginx.conf @@ -15,9 +15,10 @@ server { root /usr/share/nginx/html; index index.html; - # /api/v1/dictionaries/{name} (без /records) — admin endpoint, отдаёт schema_json. - # Только writer его реализует. Regex-location имеет приоритет над префиксом ниже. - location ~ ^/api/v1/dictionaries/[^/]+$ { + # /api/v1/dictionaries/* — все admin endpoints (schema, raw record, mutations) идут + # на writer. Read-api держит /api/v1/{name}/records/* (другой URL pattern, flatten i18n). + # Regex-location имеет приоритет над префиксом ниже. + location ~ ^/api/v1/dictionaries/ { set $backend "ordinis-app.${ORDINIS_NAMESPACE}.svc.${CLUSTER_DOMAIN}"; proxy_pass http://$backend$request_uri; proxy_http_version 1.1; diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts index 565d6be..930b79c 100644 --- a/ordinis-admin-ui/src/api/queries.ts +++ b/ordinis-admin-ui/src/api/queries.ts @@ -4,6 +4,7 @@ import { type DictionaryDefinition, type DictionaryDetail, type FlattenedRecord, + type RecordResponse, } from './client' export const dictionariesQuery = queryOptions({ @@ -35,7 +36,26 @@ export const recordsQuery = (dictionaryName: string, scopeCsv: string) => }, }) +export const recordRawQuery = (dictionaryName: string, businessKey: string) => + queryOptions({ + queryKey: ['record-raw', dictionaryName, businessKey] as const, + queryFn: async (): Promise => { + const { data } = await apiClient.get( + `/dictionaries/${dictionaryName}/records/${encodeURIComponent(businessKey)}`, + ) + return data + }, + }) + export const useDictionaries = () => useQuery(dictionariesQuery) export const useDictionaryDetail = (name: string) => useQuery(dictionaryDetailQuery(name)) export const useRecords = (dictionaryName: string, scopeCsv: string) => useQuery(recordsQuery(dictionaryName, scopeCsv)) +export const useRecordRaw = ( + dictionaryName: string, + businessKey: string | undefined, +) => + useQuery({ + ...recordRawQuery(dictionaryName, businessKey ?? ''), + enabled: Boolean(businessKey), + }) diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx index d2d354f..5cfef9a 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx @@ -8,6 +8,7 @@ import { Badge, Button, Checkbox, + DatePicker, FieldError, FieldHint, FieldLabel, @@ -48,6 +49,22 @@ const isLocalized = (s: JsonSchema): boolean => Boolean(s['x-localized']) const humanize = (key: string): string => key.replace(/[_-]+/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase()) +const parseFormDate = (s: unknown): Date | null => { + if (typeof s !== 'string' || !s) return null + const d = new Date(s) + if (Number.isNaN(d.getTime())) return null + if (d.getFullYear() >= 9999) return null + return d +} + +const formatIsoDate = (d: Date): string => { + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +} + +const isDateFormat = (s: JsonSchema): boolean => + s.type === 'string' && (s.format === 'date' || s.format === 'date-time') + type TabId = 'identity' | 'description' | 'extra' const bucketProperties = ( @@ -154,20 +171,33 @@ export const SchemaDrivenForm = ({ {...register('businessKey', { required: true, minLength: 1 })} /> - ( + field.onChange(d ? formatIsoDate(d) : '')} + /> + )} /> - ( + field.onChange(d ? formatIsoDate(d) : '')} + /> + )} /> {buckets.identity.map((key) => ( ( + field.onChange(d ? formatIsoDate(d) : undefined)} + /> + )} + /> + ) + } + if (schema.type === 'integer' || schema.type === 'number') { return ( ( + + )} + /> + ) + } + + if (schema.type === 'array' && schema.items?.type === 'object') { + return ( + ( + + )} + /> + ) + } + if (schema.type === 'array' && schema.items?.type === 'string') { return ( void +} + +const JsonField = ({ label, required, hint, error, value, onChange }: JsonFieldProps) => { + const [text, setText] = useState(() => + value === undefined || value === null ? '' : JSON.stringify(value, null, 2), + ) + const [parseError, setParseError] = useState(null) + + return ( +