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 ( -
-
- - - {formState.errors.businessKey && ( -

{t('form.error.required')}

- )} -
- -
-
- - + + + + + + -
-
- - -
-
+ + -
-

{t('form.dataFields')}

-
+ + {Object.entries(properties).map(([key, propSchema]) => ( ))} -
-
+ + - {serverError && ( -
- {serverError} -
- )} + {serverError && {serverError}} -
- - + -
+ +
) } @@ -194,12 +178,10 @@ const FieldRenderer = ({ if (isLocalized(schema)) { return (
- -
+ + {label} i18n + +
{supportedLocales.map((loc) => ( (
- + {loc === defaultLocale ? {loc} : 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 ( +