diff --git a/ordinis-admin-ui/.npmrc b/ordinis-admin-ui/.npmrc
new file mode 100644
index 0000000..001e760
--- /dev/null
+++ b/ordinis-admin-ui/.npmrc
@@ -0,0 +1,2 @@
+@nstart:registry=https://repo.nstart.cloud/repository/npm-hosted/
+min-release-age=7
diff --git a/ordinis-admin-ui/package.json b/ordinis-admin-ui/package.json
index a50c9c9..ee8e8fb 100644
--- a/ordinis-admin-ui/package.json
+++ b/ordinis-admin-ui/package.json
@@ -10,17 +10,19 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
+ "@nstart/ui": "^0.1.3",
+ "@phosphor-icons/react": "^2.1.10",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-router": "^1.86.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"axios": "^1.7.9",
- "i18next": "^24.0.5",
+ "i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
- "react-i18next": "^15.4.0"
+ "react-i18next": "^17.0.2"
},
"devDependencies": {
"@tanstack/router-vite-plugin": "^1.86.0",
diff --git a/ordinis-admin-ui/pnpm-lock.yaml b/ordinis-admin-ui/pnpm-lock.yaml
index b2131e4..1622c67 100644
--- a/ordinis-admin-ui/pnpm-lock.yaml
+++ b/ordinis-admin-ui/pnpm-lock.yaml
@@ -11,6 +11,12 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.10.0(react-hook-form@7.75.0(react@19.2.5))
+ '@nstart/ui':
+ specifier: ^0.1.3
+ version: 0.1.3(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-i18next@17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(react@19.2.5)
+ '@phosphor-icons/react':
+ specifier: ^2.1.10
+ version: 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tanstack/react-query':
specifier: ^5.62.0
version: 5.100.9(react@19.2.5)
@@ -27,8 +33,8 @@ importers:
specifier: ^1.7.9
version: 1.16.0
i18next:
- specifier: ^24.0.5
- version: 24.2.3(typescript@5.9.3)
+ specifier: ^26.0.3
+ version: 26.0.8(typescript@5.9.3)
i18next-browser-languagedetector:
specifier: ^8.0.2
version: 8.2.1
@@ -42,8 +48,8 @@ importers:
specifier: ^7.54.2
version: 7.75.0(react@19.2.5)
react-i18next:
- specifier: ^15.4.0
- version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)
+ specifier: ^17.0.2
+ version: 17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)
devDependencies:
'@tailwindcss/vite':
specifier: ^4.0.0-beta.7
@@ -348,6 +354,22 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@nstart/ui@0.1.3':
+ resolution: {integrity: sha512-dLhM2Uzvok1UB+86c+WIYz/1uYr/wN3Ch1Ab3ioZbzxBEt0bZQs+QYkj3GXAEzffCjV4zTHen31yJUHzJmpyTQ==}
+ engines: {node: ^24.0.0 || ^22.0.0}
+ peerDependencies:
+ i18next: ^26.0.3
+ react: ^19.0.0
+ react-dom: ^19.0.0
+ react-i18next: ^17.0.2
+
+ '@phosphor-icons/react@2.1.10':
+ resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>= 16.8'
+ react-dom: '>= 16.8'
+
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -779,6 +801,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ echarts@6.0.0:
+ resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
electron-to-chromium@1.5.349:
resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==}
@@ -886,16 +911,19 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
+ hls.js@1.6.16:
+ resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
+
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
i18next-browser-languagedetector@8.2.1:
resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
- i18next@24.2.3:
- resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
+ i18next@26.0.8:
+ resolution: {integrity: sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==}
peerDependencies:
- typescript: ^5
+ typescript: ^5 || ^6
peerDependenciesMeta:
typescript:
optional: true
@@ -1014,6 +1042,15 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lottie-react@2.4.1:
+ resolution: {integrity: sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ lottie-web@5.13.0:
+ resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -1085,14 +1122,14 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
- react-i18next@15.7.4:
- resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==}
+ react-i18next@17.0.6:
+ resolution: {integrity: sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==}
peerDependencies:
- i18next: '>= 23.4.0'
+ i18next: '>= 26.0.1'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
- typescript: ^5
+ typescript: ^5 || ^6
peerDependenciesMeta:
react-dom:
optional: true
@@ -1158,6 +1195,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ tslib@2.3.0:
+ resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -1231,6 +1271,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+ zrender@6.0.0:
+ resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
+
snapshots:
'@babel/code-frame@7.29.0':
@@ -1458,6 +1501,22 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@nstart/ui@0.1.3(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-i18next@17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(react@19.2.5)':
+ dependencies:
+ '@phosphor-icons/react': 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ echarts: 6.0.0
+ hls.js: 1.6.16
+ i18next: 26.0.8(typescript@5.9.3)
+ lottie-react: 2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-i18next: 17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)
+
+ '@phosphor-icons/react@2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
+ dependencies:
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.60.2':
@@ -1839,6 +1898,11 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ echarts@6.0.0:
+ dependencies:
+ tslib: 2.3.0
+ zrender: 6.0.0
+
electron-to-chromium@1.5.349: {}
enhanced-resolve@5.21.0:
@@ -1957,6 +2021,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hls.js@1.6.16: {}
+
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
@@ -1965,9 +2031,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.29.2
- i18next@24.2.3(typescript@5.9.3):
- dependencies:
- '@babel/runtime': 7.29.2
+ i18next@26.0.8(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
@@ -2044,6 +2108,14 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ lottie-react@2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ dependencies:
+ lottie-web: 5.13.0
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+
+ lottie-web@5.13.0: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -2095,12 +2167,13 @@ snapshots:
dependencies:
react: 19.2.5
- react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3):
+ react-i18next@17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
- i18next: 24.2.3(typescript@5.9.3)
+ i18next: 26.0.8(typescript@5.9.3)
react: 19.2.5
+ use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
react-dom: 19.2.5(react@19.2.5)
typescript: 5.9.3
@@ -2171,6 +2244,8 @@ snapshots:
dependencies:
is-number: 7.0.0
+ tslib@2.3.0: {}
+
typescript@5.9.3: {}
unplugin@3.0.0:
@@ -2209,3 +2284,7 @@ snapshots:
yallist@3.1.1: {}
zod@3.25.76: {}
+
+ zrender@6.0.0:
+ dependencies:
+ tslib: 2.3.0
diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx
index 7dbaf5e..dd3427b 100644
--- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx
+++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx
@@ -3,6 +3,21 @@ import { useForm, Controller, type SubmitHandler } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import Ajv, { type ErrorObject } from 'ajv'
import addFormats from 'ajv-formats'
+import {
+ Alert,
+ Button,
+ Checkbox,
+ FieldError,
+ FieldHint,
+ FieldLabel,
+ FormActions,
+ FormGrid,
+ FormSection,
+ SingleSelect,
+ TextArea,
+ TextInput,
+ type SelectOption,
+} from '@nstart/ui'
import type { CreateRecordRequest, JsonSchema } from '@/api/client'
type FormValues = {
@@ -28,9 +43,7 @@ const ajv = new Ajv({ allErrors: true, strict: false })
addFormats(ajv)
const isLocalized = (s: JsonSchema): boolean => Boolean(s['x-localized'])
-
-const fieldLabel = (key: string, schema: JsonSchema): string =>
- schema.title ?? key
+const fieldLabel = (key: string, schema: JsonSchema): string => schema.title ?? key
export const SchemaDrivenForm = ({
schema,
@@ -74,59 +87,43 @@ export const SchemaDrivenForm = ({
}
}
- const req: CreateRecordRequest = {
+ onSubmit({
businessKey: values.businessKey.trim(),
data: values.data,
validFrom: values.validFrom || undefined,
validTo: values.validTo || undefined,
- }
- onSubmit(req)
+ })
}
return (
-
)
}
@@ -194,12 +178,10 @@ const FieldRenderer = ({
if (isLocalized(schema)) {
return (
-
-
+
+ {label} i18n
+
+
{supportedLocales.map((loc) => (
(
)}
/>
))}
- {schema.description && (
-
{schema.description}
- )}
- {errorMsg &&
{errorMsg}
}
+ {schema.description &&
{schema.description}}
+
{errorMsg}
)
}
if (schema.enum) {
+ const options: SelectOption[] = schema.enum.map((v) => ({
+ id: String(v),
+ label: String(v),
+ }))
return (
(
-
-
-
- {errorMsg &&
{errorMsg}
}
-
+
)}
/>
)
@@ -265,15 +237,13 @@ const FieldRenderer = ({
control={control}
name={`data.${fieldKey}` as `data.${string}`}
render={({ field }) => (
-
+ field.onChange(e.target.checked)}
+ />
)}
/>
)
@@ -285,22 +255,18 @@ const FieldRenderer = ({
control={control}
name={`data.${fieldKey}` as `data.${string}`}
render={({ field }) => (
-
-
-
- field.onChange(e.target.value === '' ? undefined : Number(e.target.value))
- }
- className="w-full px-3 py-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
- />
- {errorMsg &&
{errorMsg}
}
-
+
+ field.onChange(e.target.value === '' ? undefined : Number(e.target.value))
+ }
+ />
)}
/>
)
@@ -312,27 +278,21 @@ const FieldRenderer = ({
control={control}
name={`data.${fieldKey}` as `data.${string}`}
render={({ field }) => (
-
-
-
- field.onChange(
- e.target.value
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean),
- )
- }
- className="w-full px-3 py-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
- />
- {errorMsg &&
{errorMsg}
}
-
+
+ field.onChange(
+ e.target.value
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ />
)}
/>
)
@@ -344,33 +304,30 @@ const FieldRenderer = ({
name={`data.${fieldKey}` as `data.${string}`}
render={({ field }) => {
const isLong = (schema.maxLength ?? 0) > 200
+ if (isLong) {
+ return (
+
+ )
+ }
return (
-
-
- {isLong ? (
-
- ) : (
-
- )}
- {schema.description && (
-
{schema.description}
- )}
- {errorMsg &&
{errorMsg}
}
-
+
)
}}
/>
diff --git a/ordinis-admin-ui/src/components/ui/Modal.tsx b/ordinis-admin-ui/src/components/ui/Modal.tsx
deleted file mode 100644
index acb68ed..0000000
--- a/ordinis-admin-ui/src/components/ui/Modal.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useEffect, type ReactNode } from 'react'
-
-type Props = {
- open: boolean
- title: string
- onClose: () => void
- children: ReactNode
- widthClass?: string
-}
-
-export const Modal = ({ open, title, onClose, children, widthClass = 'max-w-2xl' }: Props) => {
- useEffect(() => {
- if (!open) return
- const handleKey = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
- }
- window.addEventListener('keydown', handleKey)
- return () => window.removeEventListener('keydown', handleKey)
- }, [open, onClose])
-
- if (!open) return null
-
- return (
-
-
e.stopPropagation()}
- >
-
-
{title}
-
-
-
{children}
-
-
- )
-}
diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts
index b21d9cf..fc36c87 100644
--- a/ordinis-admin-ui/src/i18n.ts
+++ b/ordinis-admin-ui/src/i18n.ts
@@ -15,6 +15,7 @@ i18n
'app.title': 'Ordinis НСИ ЦУОД',
'nav.dictionaries': 'Справочники',
'header.scope': 'Доступный scope',
+ 'dict.list.subtitle': 'Каталоги НСИ ЦУОД ОДХ',
'dict.empty': 'В этом справочнике пока нет записей',
'dict.col.businessKey': 'Бизнес-ключ',
'dict.col.scope': 'Scope',
@@ -30,6 +31,7 @@ i18n
'Запись «{{key}}» будет помечена как закрытая (valid_to = now). Действие обратимо через создание новой версии.',
'dict.confirmClose.reason': 'Причина (опционально)',
'dict.confirmClose.reasonPlaceholder': 'например, «дубликат» или «выведено из эксплуатации»',
+ 'form.identity': 'Идентификация',
'form.businessKey': 'Бизнес-ключ',
'form.validFrom': 'Действует с',
'form.validTo': 'Действует до',
@@ -47,6 +49,7 @@ i18n
'app.title': 'Ordinis MDM',
'nav.dictionaries': 'Dictionaries',
'header.scope': 'Allowed scope',
+ 'dict.list.subtitle': 'Reference data catalogues',
'dict.empty': 'No records in this dictionary yet',
'dict.col.businessKey': 'Business key',
'dict.col.scope': 'Scope',
@@ -62,6 +65,7 @@ i18n
'Record "{{key}}" will be marked as closed (valid_to = now). Reversible by creating a new version.',
'dict.confirmClose.reason': 'Reason (optional)',
'dict.confirmClose.reasonPlaceholder': 'e.g., "duplicate" or "decommissioned"',
+ 'form.identity': 'Identity',
'form.businessKey': 'Business key',
'form.validFrom': 'Valid from',
'form.validTo': 'Valid to',
diff --git a/ordinis-admin-ui/src/main.tsx b/ordinis-admin-ui/src/main.tsx
index 4f13445..392d204 100644
--- a/ordinis-admin-ui/src/main.tsx
+++ b/ordinis-admin-ui/src/main.tsx
@@ -2,6 +2,7 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RouterProvider, createRouter } from '@tanstack/react-router'
+import { NStartUiProvider } from '@nstart/ui'
import { routeTree } from './routeTree.gen'
import './i18n'
import './styles.css'
@@ -23,7 +24,9 @@ declare module '@tanstack/react-router' {
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
)
diff --git a/ordinis-admin-ui/src/routes/__root.tsx b/ordinis-admin-ui/src/routes/__root.tsx
index 99907bc..c27aaa4 100644
--- a/ordinis-admin-ui/src/routes/__root.tsx
+++ b/ordinis-admin-ui/src/routes/__root.tsx
@@ -1,42 +1,41 @@
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
+import { LanguageSwitch } from '@nstart/ui'
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
component: RootLayout,
})
+const LANG_OPTIONS = [
+ { id: 'ru-RU', label: 'RU' },
+ { id: 'en-US', label: 'EN' },
+]
+
function RootLayout() {
const { t, i18n } = useTranslation()
return (
-
-
+
+
-
+
{t('app.title')}
-
-
-
+
+ i18n.changeLanguage(id)}
+ />
diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
index 765313d..edcac0b 100644
--- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
+++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
@@ -2,10 +2,28 @@ import { useState } from 'react'
import { createFileRoute, Link } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import axios from 'axios'
+import {
+ Alert,
+ Badge,
+ Button,
+ EmptyState,
+ IconButton,
+ LoadingBlock,
+ Modal,
+ PageHeader,
+ Panel,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeaderCell,
+ TableRow,
+ TextInput,
+} from '@nstart/ui'
+import { PlusIcon, PencilSimpleIcon, XCircleIcon } from '@phosphor-icons/react'
import { useDictionaryDetail, useRecords } from '@/api/queries'
import { useCreateRecord, useUpdateRecord, useCloseRecord } from '@/api/mutations'
import type { CreateRecordRequest, FlattenedRecord } from '@/api/client'
-import { Modal } from '@/components/ui/Modal'
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
export const Route = createFileRoute('/dictionaries/$name')({
@@ -60,95 +78,106 @@ function DictionaryDetail() {
edit.kind === 'create' ? createMut : edit.kind === 'edit' ? updateMut : null
const serverError = serverErrorMessage(activeMutation?.error)
+ const totalRecords = recordsResult.data?.length ?? 0
+
return (
-
-
-
- ← {t('nav.dictionaries')}
-
-
+
+
+ ← {t('nav.dictionaries')}
+
+ }
+ title={detailQuery.data?.displayName ?? name}
+ description={
+ detailQuery.data?.description
+ ? `${detailQuery.data.description} · ${totalRecords} ${t('dict.list.records')}`
+ : `${totalRecords} ${t('dict.list.records')}`
+ }
+ actions={
+ }
+ disabled={!detailQuery.data}
+ onClick={() => setEdit({ kind: 'create' })}
+ >
+ {t('dict.action.create')}
+
+ }
+ />
-
-
-
- {detailQuery.data?.displayName ?? name}
-
- {detailQuery.data?.description && (
-
{detailQuery.data.description}
- )}
-
- {recordsResult.data?.length ?? 0} {t('dict.list.records')}
-
-
-
-
+ {recordsResult.isLoading && }
- {recordsResult.isLoading && {t('loading')}
}
- {recordsResult.error && {t('error.failed')}
}
+ {recordsResult.error && (
+
+ {String(recordsResult.error)}
+
+ )}
{recordsResult.data && recordsResult.data.length === 0 && (
- {t('dict.empty')}
+
)}
{recordsResult.data && recordsResult.data.length > 0 && (
-
-
-
-
- | {t('dict.col.businessKey')} |
- name / code |
- {t('dict.col.scope')} |
- {t('dict.col.locale')} |
- {t('dict.col.validFrom')} |
- {t('dict.col.actions')} |
-
-
-
+
+
+
+
+ {t('dict.col.businessKey')}
+ name / code
+ {t('dict.col.scope')}
+ {t('dict.col.locale')}
+ {t('dict.col.validFrom')}
+ {t('dict.col.actions')}
+
+
+
{recordsResult.data.map((r) => (
-
- | {r.businessKey} |
-
+
+
+ {r.businessKey}
+
+
{String(r.data.name ?? r.data.code ?? '—')}
- |
-
- {r.dataScope}
- |
- {r._meta?.locale ?? '—'} |
-
- {new Date(r.validFrom).toLocaleDateString()}
- |
-
-
-
- |
-
+
+
+ {r.dataScope}
+
+
+ {r._meta?.locale ?? '—'}
+
+
+
+ {new Date(r.validFrom).toLocaleDateString()}
+
+
+
+
+
setEdit({ kind: 'edit', record: r })}
+ >
+
+
+
setEdit({ kind: 'close-confirm', record: r })}
+ >
+
+
+
+
+
))}
-
-
-
+
+
+
)}
setEdit({ kind: 'closed' })}
title={
edit.kind === 'create'
@@ -157,6 +186,7 @@ function DictionaryDetail() {
? `${t('dict.action.edit')}: ${edit.record.businessKey}`
: ''
}
+ maxWidth="max-w-2xl"
>
{detailQuery.data && (edit.kind === 'create' || edit.kind === 'edit') && (
setEdit({ kind: 'closed' })}
+ isOpen={edit.kind === 'close-confirm'}
+ onClose={() => {
+ setEdit({ kind: 'closed' })
+ setCloseReason('')
+ }}
title={t('dict.confirmClose.title')}
- widthClass="max-w-md"
+ maxWidth="max-w-md"
>
{edit.kind === 'close-confirm' && (
-
+
{t('dict.confirmClose.body', { key: edit.record.businessKey })}
-
-
- setCloseReason(e.target.value)}
- className="w-full px-3 py-2 border border-zinc-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-brand-500"
- placeholder={t('dict.confirmClose.reasonPlaceholder')}
- />
-
+
setCloseReason(e.target.value)}
+ />
{serverErrorMessage(closeMut.error) && (
-
- {serverErrorMessage(closeMut.error)}
-
+ {serverErrorMessage(closeMut.error)}
)}
-
-
)}
diff --git a/ordinis-admin-ui/src/routes/dictionaries.index.tsx b/ordinis-admin-ui/src/routes/dictionaries.index.tsx
index f61247e..07b0c9c 100644
--- a/ordinis-admin-ui/src/routes/dictionaries.index.tsx
+++ b/ordinis-admin-ui/src/routes/dictionaries.index.tsx
@@ -1,5 +1,6 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
+import { Alert, Badge, EmptyState, LoadingBlock, PageHeader } from '@nstart/ui'
import { useDictionaries } from '@/api/queries'
export const Route = createFileRoute('/dictionaries/')({
@@ -10,34 +11,53 @@ function DictionariesPage() {
const { t } = useTranslation()
const { data, isLoading, error } = useDictionaries()
- if (isLoading) return {t('loading')}
- if (error) return {t('error.failed')}: {String(error)}
+ if (isLoading) {
+ return
+ }
+
+ if (error) {
+ return (
+
+ {String(error)}
+
+ )
+ }
+
+ if (!data || data.length === 0) {
+ return
+ }
return (
-
- {(data ?? []).map((d) => (
-
-
-
{d.displayName ?? d.name}
-
- {d.scope}
-
-
-
{d.description}
-
- v{d.schemaVersion}
- ·
- {d.bundle}
- ·
- {d.supportedLocales.join(', ')}
-
-
- ))}
+
+
+
+
+ {data.map((d) => (
+
+
+
+ {d.displayName ?? d.name}
+
+ {d.scope}
+
+ {d.description && (
+
{d.description}
+ )}
+
+ v{d.schemaVersion}
+ ·
+ {d.bundle}
+ ·
+ {d.supportedLocales.join(', ')}
+
+
+ ))}
+
)
}
diff --git a/ordinis-admin-ui/src/styles.css b/ordinis-admin-ui/src/styles.css
index 1aaf448..26685ea 100644
--- a/ordinis-admin-ui/src/styles.css
+++ b/ordinis-admin-ui/src/styles.css
@@ -1,26 +1,12 @@
-@import 'tailwindcss';
+@import "tailwindcss";
+@import "@nstart/ui/styles/fonts.css";
+@import "@nstart/ui/styles/theme.css";
-@theme {
- --color-brand-50: oklch(0.97 0.02 250);
- --color-brand-100: oklch(0.93 0.04 250);
- --color-brand-200: oklch(0.86 0.07 250);
- --color-brand-300: oklch(0.77 0.10 250);
- --color-brand-400: oklch(0.66 0.13 250);
- --color-brand-500: oklch(0.55 0.16 250);
- --color-brand-600: oklch(0.48 0.17 250);
- --color-brand-700: oklch(0.42 0.18 250);
- --color-brand-800: oklch(0.34 0.16 250);
- --color-brand-900: oklch(0.26 0.13 250);
- --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
-}
+/* Без @source Tailwind v4 не сгенерирует CSS для классов внутри dist/@nstart/ui */
+@source "../node_modules/@nstart/ui/dist";
html,
body,
#root {
height: 100%;
}
-
-body {
- font-family: var(--font-sans);
- -webkit-font-smoothing: antialiased;
-}