fix(admin-ui): translate field labels + responsive header + modal max-h
Issues:
- Field labels (ORBIT/STATUS/COUNTRY/MASS_KG/...) показывались raw'ом потому
что backend schema не присылает title. Добавил humanize() (snake_case → 'Snake Case')
+ i18n fallback t('field.$key') для известных полей spacecraft/satellite_type/
ground_station: name, designator, norad_id, status, country, operator, mass_kg,
mission, orbit, launch_date, decay_date, spectrum_bands, satellite_type_code, etc.
- Modal panel рос в высоту бесконечно (nstart-ui Modal не клампит max-h),
на iPad портрете форма уходила за viewport. panelClassName=max-h-[calc(100vh-2rem)]
flex flex-col + bodyClassName=overflow-y-auto flex-1.
- Header в __root: padding/gap responsive (px-4 sm:px-6, py-3 sm:py-4, gap-3 sm:gap-6),
flex-wrap чтобы LanguageSwitch на узких экранах переносился под навигацию.
- Main container: px-4 sm:px-6 + py-4 sm:py-8.
This commit is contained in:
@@ -43,7 +43,11 @@ const ajv = new Ajv({ allErrors: true, strict: false })
|
|||||||
addFormats(ajv)
|
addFormats(ajv)
|
||||||
|
|
||||||
const isLocalized = (s: JsonSchema): boolean => Boolean(s['x-localized'])
|
const isLocalized = (s: JsonSchema): boolean => Boolean(s['x-localized'])
|
||||||
const fieldLabel = (key: string, schema: JsonSchema): string => schema.title ?? key
|
|
||||||
|
const humanize = (key: string): string =>
|
||||||
|
key
|
||||||
|
.replace(/[_-]+/g, ' ')
|
||||||
|
.replace(/\b\w/g, (ch) => ch.toUpperCase())
|
||||||
|
|
||||||
export const SchemaDrivenForm = ({
|
export const SchemaDrivenForm = ({
|
||||||
schema,
|
schema,
|
||||||
@@ -134,6 +138,7 @@ export const SchemaDrivenForm = ({
|
|||||||
supportedLocales={supportedLocales}
|
supportedLocales={supportedLocales}
|
||||||
defaultLocale={defaultLocale}
|
defaultLocale={defaultLocale}
|
||||||
fieldError={formState.errors.data?.[key as keyof FormValues['data']]}
|
fieldError={formState.errors.data?.[key as keyof FormValues['data']]}
|
||||||
|
labelOf={(k, s) => s.title ?? t(`field.${k}`, { defaultValue: humanize(k) })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +174,7 @@ type FieldRendererProps = {
|
|||||||
supportedLocales: string[]
|
supportedLocales: string[]
|
||||||
defaultLocale: string
|
defaultLocale: string
|
||||||
fieldError?: unknown
|
fieldError?: unknown
|
||||||
|
labelOf: (key: string, schema: JsonSchema) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FieldRenderer = (props: FieldRendererProps) => {
|
const FieldRenderer = (props: FieldRendererProps) => {
|
||||||
@@ -188,8 +194,9 @@ const FieldBody = ({
|
|||||||
supportedLocales,
|
supportedLocales,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
fieldError,
|
fieldError,
|
||||||
|
labelOf,
|
||||||
}: FieldRendererProps) => {
|
}: FieldRendererProps) => {
|
||||||
const label = fieldLabel(fieldKey, schema)
|
const label = labelOf(fieldKey, schema)
|
||||||
const errorMsg = (fieldError as { message?: string } | undefined)?.message
|
const errorMsg = (fieldError as { message?: string } | undefined)?.message
|
||||||
|
|
||||||
if (isLocalized(schema)) {
|
if (isLocalized(schema)) {
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ i18n
|
|||||||
'form.save': 'Сохранить',
|
'form.save': 'Сохранить',
|
||||||
'form.saving': 'Сохранение...',
|
'form.saving': 'Сохранение...',
|
||||||
'form.error.required': 'Обязательное поле',
|
'form.error.required': 'Обязательное поле',
|
||||||
|
'field.name': 'Наименование',
|
||||||
|
'field.designator': 'Designator (COSPAR)',
|
||||||
|
'field.norad_id': 'NORAD ID',
|
||||||
|
'field.status': 'Статус',
|
||||||
|
'field.country': 'Страна',
|
||||||
|
'field.operator': 'Оператор',
|
||||||
|
'field.mass_kg': 'Масса (кг)',
|
||||||
|
'field.mission': 'Миссия',
|
||||||
|
'field.orbit': 'Орбита',
|
||||||
|
'field.launch_date': 'Дата запуска',
|
||||||
|
'field.decay_date': 'Дата схода',
|
||||||
|
'field.spectrum_bands': 'Спектральные диапазоны',
|
||||||
|
'field.satellite_type_code': 'Тип КА (код)',
|
||||||
|
'field.code': 'Код',
|
||||||
|
'field.description': 'Описание',
|
||||||
|
'field.location': 'Местоположение',
|
||||||
|
'field.coordinates': 'Координаты',
|
||||||
|
'field.frequency_bands': 'Частотные диапазоны',
|
||||||
'loading': 'Загрузка...',
|
'loading': 'Загрузка...',
|
||||||
'error.failed': 'Не удалось загрузить данные',
|
'error.failed': 'Не удалось загрузить данные',
|
||||||
},
|
},
|
||||||
@@ -74,6 +92,24 @@ i18n
|
|||||||
'form.save': 'Save',
|
'form.save': 'Save',
|
||||||
'form.saving': 'Saving...',
|
'form.saving': 'Saving...',
|
||||||
'form.error.required': 'Required field',
|
'form.error.required': 'Required field',
|
||||||
|
'field.name': 'Name',
|
||||||
|
'field.designator': 'Designator (COSPAR)',
|
||||||
|
'field.norad_id': 'NORAD ID',
|
||||||
|
'field.status': 'Status',
|
||||||
|
'field.country': 'Country',
|
||||||
|
'field.operator': 'Operator',
|
||||||
|
'field.mass_kg': 'Mass (kg)',
|
||||||
|
'field.mission': 'Mission',
|
||||||
|
'field.orbit': 'Orbit',
|
||||||
|
'field.launch_date': 'Launch date',
|
||||||
|
'field.decay_date': 'Decay date',
|
||||||
|
'field.spectrum_bands': 'Spectrum bands',
|
||||||
|
'field.satellite_type_code': 'Satellite type code',
|
||||||
|
'field.code': 'Code',
|
||||||
|
'field.description': 'Description',
|
||||||
|
'field.location': 'Location',
|
||||||
|
'field.coordinates': 'Coordinates',
|
||||||
|
'field.frequency_bands': 'Frequency bands',
|
||||||
'loading': 'Loading...',
|
'loading': 'Loading...',
|
||||||
'error.failed': 'Failed to load data',
|
'error.failed': 'Failed to load data',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-full flex flex-col bg-white">
|
<div className="min-h-full flex flex-col bg-white">
|
||||||
<header className="border-b border-regolith bg-white">
|
<header className="border-b border-regolith bg-white">
|
||||||
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center gap-6">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center flex-wrap gap-3 sm:gap-6">
|
||||||
<Link to="/" className="font-primary text-lg text-ultramarain">
|
<Link to="/" className="font-primary text-base sm:text-lg text-ultramarain">
|
||||||
{t('app.title')}
|
{t('app.title')}
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex gap-4 text-sm">
|
<nav className="flex gap-4 text-sm">
|
||||||
@@ -39,7 +39,7 @@ function RootLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 max-w-6xl mx-auto w-full px-6 py-8">
|
<main className="flex-1 max-w-6xl mx-auto w-full px-4 sm:px-6 py-4 sm:py-8">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ function DictionaryDetail() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
maxWidth="max-w-4xl"
|
maxWidth="max-w-4xl"
|
||||||
|
panelClassName="max-h-[calc(100vh-2rem)] my-4 flex flex-col"
|
||||||
|
bodyClassName="overflow-y-auto flex-1"
|
||||||
>
|
>
|
||||||
{detailQuery.data && (edit.kind === 'create' || edit.kind === 'edit') && (
|
{detailQuery.data && (edit.kind === 'create' || edit.kind === 'edit') && (
|
||||||
<SchemaDrivenForm
|
<SchemaDrivenForm
|
||||||
|
|||||||
Reference in New Issue
Block a user