@bsol-oss/react-datatable5 12.0.0-beta.56 → 12.0.0-beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/index.d.ts +53 -2
- package/dist/index.js +263 -6
- package/dist/index.mjs +260 -7
- package/dist/types/components/Form/SchemaFormContext.d.ts +2 -0
- package/dist/types/components/Form/components/core/FormRoot.d.ts +3 -1
- package/dist/types/components/Form/utils/validation.d.ts +49 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@ The datetable package is built on top of `@tanstack/react-table` and `chakra-ui`
|
|
|
8
8
|
npm install @tanstack/react-table @chakra-ui/react @emotion/react @bsol-oss/react-datatable5
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
For form validation features with internationalization support, also install:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install ajv ajv-formats ajv-i18n
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## Usage
|
|
12
18
|
|
|
13
19
|
### Hook
|
|
@@ -82,6 +88,192 @@ GET http://localhost:8081/api/g/core_people?offset=0&limit=10&sorting[0][id]=id&
|
|
|
82
88
|
</DataTable>
|
|
83
89
|
```
|
|
84
90
|
|
|
91
|
+
### Form Validation with AJV and Internationalization
|
|
92
|
+
|
|
93
|
+
The package now includes built-in JSON Schema validation using AJV (Another JSON Schema Validator) with format support and comprehensive internationalization (i18n) capabilities, including full Traditional Chinese (Hong Kong/Taiwan) support.
|
|
94
|
+
|
|
95
|
+
#### Features
|
|
96
|
+
|
|
97
|
+
- **JSON Schema Validation**: Full JSON Schema Draft 7 support
|
|
98
|
+
- **Format Validation**: Built-in support for date, time, email, UUID, and other formats via `ajv-formats`
|
|
99
|
+
- **Multi-language Support**: Complete i18n support with Traditional Chinese (Hong Kong/Taiwan) and Simplified Chinese
|
|
100
|
+
- **Enhanced Error Messages**: Culturally appropriate error messages in multiple languages
|
|
101
|
+
- **Automatic Validation**: Validation occurs before form submission and confirmation
|
|
102
|
+
- **Custom Error Display**: Collapsible error panels with localized validation feedback
|
|
103
|
+
|
|
104
|
+
#### Supported Languages
|
|
105
|
+
|
|
106
|
+
- **🇺🇸 English (en)** - Default language
|
|
107
|
+
- **🇭🇰 Traditional Chinese - Hong Kong (zh-HK)** - 繁體中文(香港)
|
|
108
|
+
- **🇹🇼 Traditional Chinese - Taiwan (zh-TW)** - 繁體中文(台灣)
|
|
109
|
+
- **🇨🇳 Simplified Chinese (zh-CN, zh)** - 简体中文
|
|
110
|
+
|
|
111
|
+
#### Basic Usage with Internationalization
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { FormRoot, FormBody, validateData, SupportedLocale } from "@bsol-oss/react-datatable5";
|
|
115
|
+
|
|
116
|
+
const schema = {
|
|
117
|
+
type: "object",
|
|
118
|
+
required: ["email", "age"],
|
|
119
|
+
properties: {
|
|
120
|
+
email: {
|
|
121
|
+
type: "string",
|
|
122
|
+
format: "email"
|
|
123
|
+
},
|
|
124
|
+
age: {
|
|
125
|
+
type: "integer",
|
|
126
|
+
minimum: 18,
|
|
127
|
+
maximum: 120
|
|
128
|
+
},
|
|
129
|
+
name: {
|
|
130
|
+
type: "string",
|
|
131
|
+
minLength: 2,
|
|
132
|
+
maxLength: 50
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Use Traditional Chinese (Hong Kong) for validation errors
|
|
138
|
+
<FormRoot
|
|
139
|
+
schema={schema}
|
|
140
|
+
validationLocale="zh-HK"
|
|
141
|
+
/* other props */
|
|
142
|
+
>
|
|
143
|
+
<FormBody />
|
|
144
|
+
</FormRoot>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Language-specific Examples
|
|
148
|
+
|
|
149
|
+
**Traditional Chinese (Hong Kong):**
|
|
150
|
+
```tsx
|
|
151
|
+
<FormRoot
|
|
152
|
+
schema={schema}
|
|
153
|
+
validationLocale="zh-HK"
|
|
154
|
+
/* other props */
|
|
155
|
+
>
|
|
156
|
+
<FormBody />
|
|
157
|
+
</FormRoot>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Traditional Chinese (Taiwan):**
|
|
161
|
+
```tsx
|
|
162
|
+
<FormRoot
|
|
163
|
+
schema={schema}
|
|
164
|
+
validationLocale="zh-TW"
|
|
165
|
+
/* other props */
|
|
166
|
+
>
|
|
167
|
+
<FormBody />
|
|
168
|
+
</FormRoot>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Simplified Chinese:**
|
|
172
|
+
```tsx
|
|
173
|
+
<FormRoot
|
|
174
|
+
schema={schema}
|
|
175
|
+
validationLocale="zh-CN"
|
|
176
|
+
/* other props */
|
|
177
|
+
>
|
|
178
|
+
<FormBody />
|
|
179
|
+
</FormRoot>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### Manual Validation with Internationalization
|
|
183
|
+
|
|
184
|
+
You can also use the validation utilities directly with language support:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { validateData, ValidationResult, SupportedLocale } from "@bsol-oss/react-datatable5";
|
|
188
|
+
|
|
189
|
+
// Validate with Traditional Chinese (Hong Kong) error messages
|
|
190
|
+
const result: ValidationResult = validateData(formData, schema, {
|
|
191
|
+
locale: 'zh-HK'
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!result.isValid) {
|
|
195
|
+
console.log("驗證錯誤:", result.errors);
|
|
196
|
+
// Error messages will be in Traditional Chinese
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check supported locales
|
|
200
|
+
import { getSupportedLocales, isLocaleSupported } from "@bsol-oss/react-datatable5";
|
|
201
|
+
|
|
202
|
+
const supportedLocales = getSupportedLocales(); // ['en', 'zh-HK', 'zh-TW', 'zh-CN', 'zh']
|
|
203
|
+
const isSupported = isLocaleSupported('zh-HK'); // true
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Validation Error Structure
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
interface ValidationError {
|
|
210
|
+
field: string; // The field that failed validation
|
|
211
|
+
message: string; // User-friendly error message (localized)
|
|
212
|
+
value?: unknown; // The current value that failed
|
|
213
|
+
schemaPath?: string; // JSON Schema path for debugging
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Dynamic Language Switching
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import { SupportedLocale } from "@bsol-oss/react-datatable5";
|
|
221
|
+
|
|
222
|
+
const MyForm = () => {
|
|
223
|
+
const [locale, setLocale] = useState<SupportedLocale>('zh-HK');
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div>
|
|
227
|
+
{/* Language selector */}
|
|
228
|
+
<select value={locale} onChange={(e) => setLocale(e.target.value as SupportedLocale)}>
|
|
229
|
+
<option value="en">English</option>
|
|
230
|
+
<option value="zh-HK">繁體中文(香港)</option>
|
|
231
|
+
<option value="zh-TW">繁體中文(台灣)</option>
|
|
232
|
+
<option value="zh-CN">简体中文</option>
|
|
233
|
+
</select>
|
|
234
|
+
|
|
235
|
+
{/* Form with dynamic locale */}
|
|
236
|
+
<FormRoot
|
|
237
|
+
schema={schema}
|
|
238
|
+
validationLocale={locale}
|
|
239
|
+
/* other props */
|
|
240
|
+
>
|
|
241
|
+
<FormBody />
|
|
242
|
+
</FormRoot>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Example Validation Messages
|
|
249
|
+
|
|
250
|
+
**English:**
|
|
251
|
+
- "email is required"
|
|
252
|
+
- "Invalid email format"
|
|
253
|
+
- "Must be at least 18"
|
|
254
|
+
|
|
255
|
+
**Traditional Chinese (Hong Kong/Taiwan):**
|
|
256
|
+
- "email 為必填"
|
|
257
|
+
- "無效的 email 格式"
|
|
258
|
+
- "必須至少為 18"
|
|
259
|
+
|
|
260
|
+
**Simplified Chinese:**
|
|
261
|
+
- "email 为必填"
|
|
262
|
+
- "无效的 email 格式"
|
|
263
|
+
- "必须至少为 18"
|
|
264
|
+
|
|
265
|
+
#### Supported Validation Types
|
|
266
|
+
|
|
267
|
+
- **Type validation**: string, number, integer, boolean, object, array
|
|
268
|
+
- **Format validation**: email, date, time, date-time, uuid, uri, etc.
|
|
269
|
+
- **String constraints**: minLength, maxLength, pattern (regex)
|
|
270
|
+
- **Numeric constraints**: minimum, maximum, multipleOf
|
|
271
|
+
- **Array constraints**: minItems, maxItems, uniqueItems
|
|
272
|
+
- **Object constraints**: required properties, additionalProperties
|
|
273
|
+
- **Enum validation**: restricted value sets
|
|
274
|
+
|
|
275
|
+
All validation types are fully supported across all languages with culturally appropriate translations.
|
|
276
|
+
|
|
85
277
|
For more details of props and examples, please review the stories in storybook platform.
|
|
86
278
|
|
|
87
279
|
## Development
|
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,12 @@ import { RankingInfo } from '@tanstack/match-sorter-utils';
|
|
|
11
11
|
import { UseQueryResult } from '@tanstack/react-query';
|
|
12
12
|
import { JSONSchema7 } from 'json-schema';
|
|
13
13
|
import { ForeignKeyProps as ForeignKeyProps$1 } from '@/components/Form/components/fields/StringInputField';
|
|
14
|
+
import { SupportedLocale as SupportedLocale$1 } from '@/components/Form/utils/validation';
|
|
14
15
|
import { AxiosRequestConfig } from 'axios';
|
|
15
16
|
import * as react_hook_form from 'react-hook-form';
|
|
16
17
|
import { UseFormReturn, FieldValues, SubmitHandler } from 'react-hook-form';
|
|
17
18
|
import { RenderProps, Props } from '@bsol-oss/dayzed-react19';
|
|
19
|
+
import * as ajv_i18n_localize_types from 'ajv-i18n/localize/types';
|
|
18
20
|
|
|
19
21
|
interface DensityToggleButtonProps {
|
|
20
22
|
icon?: React__default.ReactElement;
|
|
@@ -547,6 +549,7 @@ interface FormRootProps<TData extends FieldValues> {
|
|
|
547
549
|
requestOptions?: AxiosRequestConfig;
|
|
548
550
|
getUpdatedData?: () => TData | Promise<TData> | void;
|
|
549
551
|
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
552
|
+
validationLocale?: SupportedLocale$1;
|
|
550
553
|
}
|
|
551
554
|
interface CustomJSONSchema7Definition extends JSONSchema7 {
|
|
552
555
|
variant: string;
|
|
@@ -563,7 +566,7 @@ declare const idPickerSanityCheck: (column: string, foreign_key?: {
|
|
|
563
566
|
column?: string | undefined;
|
|
564
567
|
display_column?: string | undefined;
|
|
565
568
|
} | undefined) => void;
|
|
566
|
-
declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, }: FormRootProps<TData>) => react_jsx_runtime.JSX.Element;
|
|
569
|
+
declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, validationLocale, }: FormRootProps<TData>) => react_jsx_runtime.JSX.Element;
|
|
567
570
|
|
|
568
571
|
interface DefaultFormProps<TData extends FieldValues> {
|
|
569
572
|
formConfig: Omit<FormRootProps<TData>, "children">;
|
|
@@ -638,6 +641,54 @@ interface RecordDisplayProps {
|
|
|
638
641
|
}
|
|
639
642
|
declare const RecordDisplay: ({ object, boxProps, translate, prefix, }: RecordDisplayProps) => react_jsx_runtime.JSX.Element;
|
|
640
643
|
|
|
644
|
+
declare const localize: {
|
|
645
|
+
en: () => void;
|
|
646
|
+
'zh-HK': ajv_i18n_localize_types.Localize;
|
|
647
|
+
'zh-TW': ajv_i18n_localize_types.Localize;
|
|
648
|
+
'zh-CN': ajv_i18n_localize_types.Localize;
|
|
649
|
+
zh: ajv_i18n_localize_types.Localize;
|
|
650
|
+
};
|
|
651
|
+
type SupportedLocale = keyof typeof localize;
|
|
652
|
+
interface ValidationError {
|
|
653
|
+
field: string;
|
|
654
|
+
message: string;
|
|
655
|
+
value?: unknown;
|
|
656
|
+
schemaPath?: string;
|
|
657
|
+
}
|
|
658
|
+
interface ValidationResult {
|
|
659
|
+
isValid: boolean;
|
|
660
|
+
errors: ValidationError[];
|
|
661
|
+
}
|
|
662
|
+
interface ValidationOptions {
|
|
663
|
+
locale?: SupportedLocale;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Validates data against a JSON Schema using AJV with i18n support
|
|
667
|
+
* @param data - The data to validate
|
|
668
|
+
* @param schema - The JSON Schema to validate against
|
|
669
|
+
* @param options - Validation options including locale
|
|
670
|
+
* @returns ValidationResult containing validation status and errors
|
|
671
|
+
*/
|
|
672
|
+
declare const validateData: (data: unknown, schema: JSONSchema7, options?: ValidationOptions) => ValidationResult;
|
|
673
|
+
/**
|
|
674
|
+
* Creates a reusable validator function for a specific schema with i18n support
|
|
675
|
+
* @param schema - The JSON Schema to create validator for
|
|
676
|
+
* @param locale - The locale to use for error messages
|
|
677
|
+
* @returns A function that validates data against the schema
|
|
678
|
+
*/
|
|
679
|
+
declare const createSchemaValidator: (schema: JSONSchema7, locale?: SupportedLocale) => (data: unknown) => ValidationResult;
|
|
680
|
+
/**
|
|
681
|
+
* Get available locales for validation error messages
|
|
682
|
+
* @returns Array of supported locale codes
|
|
683
|
+
*/
|
|
684
|
+
declare const getSupportedLocales: () => SupportedLocale[];
|
|
685
|
+
/**
|
|
686
|
+
* Check if a locale is supported
|
|
687
|
+
* @param locale - The locale to check
|
|
688
|
+
* @returns Boolean indicating if the locale is supported
|
|
689
|
+
*/
|
|
690
|
+
declare const isLocaleSupported: (locale: string) => locale is "en" | "zh-HK" | "zh-TW" | "zh-CN" | "zh";
|
|
691
|
+
|
|
641
692
|
declare module "@tanstack/react-table" {
|
|
642
693
|
interface ColumnMeta<TData extends RowData, TValue> {
|
|
643
694
|
/**
|
|
@@ -710,4 +761,4 @@ declare module "@tanstack/react-table" {
|
|
|
710
761
|
}
|
|
711
762
|
}
|
|
712
763
|
|
|
713
|
-
export { type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7Definition, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, type DatePickerProps, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, ErrorAlert, type ErrorAlertProps, FilterDialog, FormBody, FormRoot, type FormRootProps, FormTitle, type GetColumnsConfigs, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, PageSizeControl, type PageSizeControlProps, Pagination, type RangeCalendarProps, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, TextCell, type TextCellProps, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, ViewDialog, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
|
|
764
|
+
export { type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7Definition, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, type DatePickerProps, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, ErrorAlert, type ErrorAlertProps, FilterDialog, FormBody, FormRoot, type FormRootProps, FormTitle, type GetColumnsConfigs, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, PageSizeControl, type PageSizeControlProps, Pagination, type RangeCalendarProps, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, type SupportedLocale, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, TextCell, type TextCellProps, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, type ValidationError, type ValidationOptions, type ValidationResult, ViewDialog, createSchemaValidator, getColumns, getMultiDates, getRangeDates, getSupportedLocales, idPickerSanityCheck, isLocaleSupported, useDataTable, useDataTableContext, useDataTableServer, useForm, validateData, widthSanityCheck };
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,10 @@ var gr = require('react-icons/gr');
|
|
|
29
29
|
var reactI18next = require('react-i18next');
|
|
30
30
|
var axios = require('axios');
|
|
31
31
|
var reactHookForm = require('react-hook-form');
|
|
32
|
+
var Ajv = require('ajv');
|
|
33
|
+
var addFormats = require('ajv-formats');
|
|
34
|
+
var zh_TW = require('ajv-i18n/localize/zh-TW');
|
|
35
|
+
var zh_CN = require('ajv-i18n/localize/zh');
|
|
32
36
|
var dayjs = require('dayjs');
|
|
33
37
|
var utc = require('dayjs/plugin/utc');
|
|
34
38
|
var ti = require('react-icons/ti');
|
|
@@ -3688,6 +3692,7 @@ const SchemaFormContext = React.createContext({
|
|
|
3688
3692
|
onSubmit: async () => { },
|
|
3689
3693
|
rowNumber: 0,
|
|
3690
3694
|
requestOptions: {},
|
|
3695
|
+
validationLocale: 'en',
|
|
3691
3696
|
});
|
|
3692
3697
|
|
|
3693
3698
|
const useSchemaContext = () => {
|
|
@@ -3698,6 +3703,179 @@ const clearEmptyString = (object) => {
|
|
|
3698
3703
|
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ""));
|
|
3699
3704
|
};
|
|
3700
3705
|
|
|
3706
|
+
// AJV i18n support
|
|
3707
|
+
const localize = {
|
|
3708
|
+
en: () => { }, // English is default, no localization needed
|
|
3709
|
+
'zh-HK': zh_TW, // Use zh-TW for Hong Kong Traditional Chinese
|
|
3710
|
+
'zh-TW': zh_TW, // Traditional Chinese (Taiwan)
|
|
3711
|
+
'zh-CN': zh_CN, // Simplified Chinese
|
|
3712
|
+
'zh': zh_CN, // Simplified Chinese (short form)
|
|
3713
|
+
};
|
|
3714
|
+
// Create AJV instance with format support
|
|
3715
|
+
const createValidator = () => {
|
|
3716
|
+
const ajv = new Ajv({
|
|
3717
|
+
allErrors: true,
|
|
3718
|
+
verbose: true,
|
|
3719
|
+
removeAdditional: false,
|
|
3720
|
+
strict: false,
|
|
3721
|
+
messages: false, // Disable default messages for i18n
|
|
3722
|
+
});
|
|
3723
|
+
// Add format validation support (date, time, email, etc.)
|
|
3724
|
+
addFormats(ajv);
|
|
3725
|
+
return ajv;
|
|
3726
|
+
};
|
|
3727
|
+
/**
|
|
3728
|
+
* Validates data against a JSON Schema using AJV with i18n support
|
|
3729
|
+
* @param data - The data to validate
|
|
3730
|
+
* @param schema - The JSON Schema to validate against
|
|
3731
|
+
* @param options - Validation options including locale
|
|
3732
|
+
* @returns ValidationResult containing validation status and errors
|
|
3733
|
+
*/
|
|
3734
|
+
const validateData = (data, schema, options = {}) => {
|
|
3735
|
+
const { locale = 'en' } = options;
|
|
3736
|
+
const ajv = createValidator();
|
|
3737
|
+
try {
|
|
3738
|
+
const validate = ajv.compile(schema);
|
|
3739
|
+
const isValid = validate(data);
|
|
3740
|
+
if (isValid) {
|
|
3741
|
+
return {
|
|
3742
|
+
isValid: true,
|
|
3743
|
+
errors: [],
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
// Apply localization if not English
|
|
3747
|
+
if (locale !== 'en' && validate.errors && localize[locale]) {
|
|
3748
|
+
try {
|
|
3749
|
+
localize[locale](validate.errors);
|
|
3750
|
+
}
|
|
3751
|
+
catch (error) {
|
|
3752
|
+
console.warn(`Failed to localize validation errors to ${locale}:`, error);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
const errors = (validate.errors || []).map((error) => {
|
|
3756
|
+
const field = error.instancePath?.replace(/^\//, '') || error.schemaPath?.split('/').pop() || 'root';
|
|
3757
|
+
let message = error.message || 'Validation error';
|
|
3758
|
+
// Enhanced error messages for better UX (only if using English or localization failed)
|
|
3759
|
+
if (locale === 'en' || !error.message) {
|
|
3760
|
+
switch (error.keyword) {
|
|
3761
|
+
case 'required':
|
|
3762
|
+
message = `${error.params?.missingProperty || 'Field'} is required`;
|
|
3763
|
+
break;
|
|
3764
|
+
case 'format':
|
|
3765
|
+
message = `Invalid ${error.params?.format} format`;
|
|
3766
|
+
break;
|
|
3767
|
+
case 'type':
|
|
3768
|
+
message = `Expected ${error.params?.type}, got ${typeof error.data}`;
|
|
3769
|
+
break;
|
|
3770
|
+
case 'minLength':
|
|
3771
|
+
message = `Must be at least ${error.params?.limit} characters`;
|
|
3772
|
+
break;
|
|
3773
|
+
case 'maxLength':
|
|
3774
|
+
message = `Must be no more than ${error.params?.limit} characters`;
|
|
3775
|
+
break;
|
|
3776
|
+
case 'minimum':
|
|
3777
|
+
message = `Must be at least ${error.params?.limit}`;
|
|
3778
|
+
break;
|
|
3779
|
+
case 'maximum':
|
|
3780
|
+
message = `Must be no more than ${error.params?.limit}`;
|
|
3781
|
+
break;
|
|
3782
|
+
case 'pattern':
|
|
3783
|
+
message = `Does not match required pattern`;
|
|
3784
|
+
break;
|
|
3785
|
+
case 'enum':
|
|
3786
|
+
message = `Must be one of: ${error.params?.allowedValues?.join(', ')}`;
|
|
3787
|
+
break;
|
|
3788
|
+
default:
|
|
3789
|
+
message = error.message || 'Validation error';
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
field: field || error.instancePath || 'unknown',
|
|
3794
|
+
message,
|
|
3795
|
+
value: error.data,
|
|
3796
|
+
schemaPath: error.schemaPath,
|
|
3797
|
+
};
|
|
3798
|
+
});
|
|
3799
|
+
return {
|
|
3800
|
+
isValid: false,
|
|
3801
|
+
errors,
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
catch (error) {
|
|
3805
|
+
// Handle AJV compilation errors
|
|
3806
|
+
const errorMessage = locale === 'zh-HK' || locale === 'zh-TW'
|
|
3807
|
+
? `架構驗證錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`
|
|
3808
|
+
: locale === 'zh-CN' || locale === 'zh'
|
|
3809
|
+
? `模式验证错误: ${error instanceof Error ? error.message : '未知错误'}`
|
|
3810
|
+
: `Schema validation error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
3811
|
+
return {
|
|
3812
|
+
isValid: false,
|
|
3813
|
+
errors: [
|
|
3814
|
+
{
|
|
3815
|
+
field: 'schema',
|
|
3816
|
+
message: errorMessage,
|
|
3817
|
+
},
|
|
3818
|
+
],
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
/**
|
|
3823
|
+
* Creates a reusable validator function for a specific schema with i18n support
|
|
3824
|
+
* @param schema - The JSON Schema to create validator for
|
|
3825
|
+
* @param locale - The locale to use for error messages
|
|
3826
|
+
* @returns A function that validates data against the schema
|
|
3827
|
+
*/
|
|
3828
|
+
const createSchemaValidator = (schema, locale = 'en') => {
|
|
3829
|
+
const ajv = createValidator();
|
|
3830
|
+
const validate = ajv.compile(schema);
|
|
3831
|
+
return (data) => {
|
|
3832
|
+
const isValid = validate(data);
|
|
3833
|
+
if (isValid) {
|
|
3834
|
+
return {
|
|
3835
|
+
isValid: true,
|
|
3836
|
+
errors: [],
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
// Apply localization if not English
|
|
3840
|
+
if (locale !== 'en' && validate.errors && localize[locale]) {
|
|
3841
|
+
try {
|
|
3842
|
+
localize[locale](validate.errors);
|
|
3843
|
+
}
|
|
3844
|
+
catch (error) {
|
|
3845
|
+
console.warn(`Failed to localize validation errors to ${locale}:`, error);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
const errors = (validate.errors || []).map((error) => {
|
|
3849
|
+
const field = error.instancePath?.replace(/^\//, '') || 'root';
|
|
3850
|
+
return {
|
|
3851
|
+
field,
|
|
3852
|
+
message: error.message || 'Validation error',
|
|
3853
|
+
value: error.data,
|
|
3854
|
+
schemaPath: error.schemaPath,
|
|
3855
|
+
};
|
|
3856
|
+
});
|
|
3857
|
+
return {
|
|
3858
|
+
isValid: false,
|
|
3859
|
+
errors,
|
|
3860
|
+
};
|
|
3861
|
+
};
|
|
3862
|
+
};
|
|
3863
|
+
/**
|
|
3864
|
+
* Get available locales for validation error messages
|
|
3865
|
+
* @returns Array of supported locale codes
|
|
3866
|
+
*/
|
|
3867
|
+
const getSupportedLocales = () => {
|
|
3868
|
+
return Object.keys(localize);
|
|
3869
|
+
};
|
|
3870
|
+
/**
|
|
3871
|
+
* Check if a locale is supported
|
|
3872
|
+
* @param locale - The locale to check
|
|
3873
|
+
* @returns Boolean indicating if the locale is supported
|
|
3874
|
+
*/
|
|
3875
|
+
const isLocaleSupported = (locale) => {
|
|
3876
|
+
return locale in localize;
|
|
3877
|
+
};
|
|
3878
|
+
|
|
3701
3879
|
const idPickerSanityCheck = (column, foreign_key) => {
|
|
3702
3880
|
if (!!foreign_key == false) {
|
|
3703
3881
|
throw new Error(`The key foreign_key does not exist in properties of column ${column} when using id-picker.`);
|
|
@@ -3713,7 +3891,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
3713
3891
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
3714
3892
|
}
|
|
3715
3893
|
};
|
|
3716
|
-
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, }) => {
|
|
3894
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, validationLocale = 'en', }) => {
|
|
3717
3895
|
const [isSuccess, setIsSuccess] = React.useState(false);
|
|
3718
3896
|
const [isError, setIsError] = React.useState(false);
|
|
3719
3897
|
const [isSubmiting, setIsSubmiting] = React.useState(false);
|
|
@@ -3747,6 +3925,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
3747
3925
|
setError,
|
|
3748
3926
|
getUpdatedData,
|
|
3749
3927
|
customErrorRenderer,
|
|
3928
|
+
validationLocale,
|
|
3750
3929
|
}, children: jsxRuntime.jsx(reactHookForm.FormProvider, { ...form, children: children }) }));
|
|
3751
3930
|
};
|
|
3752
3931
|
|
|
@@ -4642,7 +4821,7 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
|
|
|
4642
4821
|
|
|
4643
4822
|
const NumberInputRoot = React__namespace.forwardRef(function NumberInput(props, ref) {
|
|
4644
4823
|
const { children, ...rest } = props;
|
|
4645
|
-
return (jsxRuntime.
|
|
4824
|
+
return (jsxRuntime.jsx(react.NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: children }));
|
|
4646
4825
|
});
|
|
4647
4826
|
const NumberInputField$1 = react.NumberInput.Input;
|
|
4648
4827
|
react.NumberInput.Scrubber;
|
|
@@ -5470,10 +5649,28 @@ const ColumnViewer = ({ column, properties, prefix, }) => {
|
|
|
5470
5649
|
};
|
|
5471
5650
|
|
|
5472
5651
|
const SubmitButton = () => {
|
|
5473
|
-
const { translate, setValidatedData, setIsError, setIsConfirming } = useSchemaContext();
|
|
5652
|
+
const { translate, setValidatedData, setIsError, setIsConfirming, setError, schema, validationLocale } = useSchemaContext();
|
|
5474
5653
|
const methods = reactHookForm.useFormContext();
|
|
5475
5654
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5476
5655
|
const onValid = (data) => {
|
|
5656
|
+
// Validate data using AJV before proceeding to confirmation
|
|
5657
|
+
const validationResult = validateData(data, schema, { locale: validationLocale });
|
|
5658
|
+
if (!validationResult.isValid) {
|
|
5659
|
+
// Set validation errors with i18n support
|
|
5660
|
+
const validationErrorMessage = {
|
|
5661
|
+
type: 'validation',
|
|
5662
|
+
errors: validationResult.errors,
|
|
5663
|
+
message: validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5664
|
+
? '表單驗證失敗'
|
|
5665
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5666
|
+
? '表单验证失败'
|
|
5667
|
+
: 'Form validation failed'
|
|
5668
|
+
};
|
|
5669
|
+
setError(validationErrorMessage);
|
|
5670
|
+
setIsError(true);
|
|
5671
|
+
return;
|
|
5672
|
+
}
|
|
5673
|
+
// If validation passes, proceed to confirmation
|
|
5477
5674
|
setValidatedData(data);
|
|
5478
5675
|
setIsError(false);
|
|
5479
5676
|
setIsConfirming(true);
|
|
@@ -5484,7 +5681,7 @@ const SubmitButton = () => {
|
|
|
5484
5681
|
};
|
|
5485
5682
|
|
|
5486
5683
|
const FormBody = () => {
|
|
5487
|
-
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, customErrorRenderer, } = useSchemaContext();
|
|
5684
|
+
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, customErrorRenderer, validationLocale, } = useSchemaContext();
|
|
5488
5685
|
const methods = reactHookForm.useFormContext();
|
|
5489
5686
|
const { properties } = schema;
|
|
5490
5687
|
const onBeforeSubmit = () => {
|
|
@@ -5500,6 +5697,27 @@ const FormBody = () => {
|
|
|
5500
5697
|
const onSubmitSuccess = () => {
|
|
5501
5698
|
setIsSuccess(true);
|
|
5502
5699
|
};
|
|
5700
|
+
// Enhanced validation function using AJV with i18n support
|
|
5701
|
+
const validateFormData = (data) => {
|
|
5702
|
+
try {
|
|
5703
|
+
const validationResult = validateData(data, schema, { locale: validationLocale });
|
|
5704
|
+
return validationResult;
|
|
5705
|
+
}
|
|
5706
|
+
catch (error) {
|
|
5707
|
+
const errorMessage = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5708
|
+
? `驗證錯誤: ${error instanceof Error ? error.message : '未知驗證錯誤'}`
|
|
5709
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5710
|
+
? `验证错误: ${error instanceof Error ? error.message : '未知验证错误'}`
|
|
5711
|
+
: `Validation error: ${error instanceof Error ? error.message : 'Unknown validation error'}`;
|
|
5712
|
+
return {
|
|
5713
|
+
isValid: false,
|
|
5714
|
+
errors: [{
|
|
5715
|
+
field: 'validation',
|
|
5716
|
+
message: errorMessage
|
|
5717
|
+
}]
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5720
|
+
};
|
|
5503
5721
|
const defaultOnSubmit = async (promise) => {
|
|
5504
5722
|
try {
|
|
5505
5723
|
onBeforeSubmit();
|
|
@@ -5524,12 +5742,47 @@ const FormBody = () => {
|
|
|
5524
5742
|
};
|
|
5525
5743
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5526
5744
|
const onFormSubmit = async (data) => {
|
|
5745
|
+
// Validate data using AJV before submission
|
|
5746
|
+
const validationResult = validateFormData(data);
|
|
5747
|
+
if (!validationResult.isValid) {
|
|
5748
|
+
// Set validation errors
|
|
5749
|
+
const validationErrorMessage = {
|
|
5750
|
+
type: 'validation',
|
|
5751
|
+
errors: validationResult.errors,
|
|
5752
|
+
message: validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5753
|
+
? '表單驗證失敗'
|
|
5754
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5755
|
+
? '表单验证失败'
|
|
5756
|
+
: 'Form validation failed'
|
|
5757
|
+
};
|
|
5758
|
+
onSubmitError(validationErrorMessage);
|
|
5759
|
+
return;
|
|
5760
|
+
}
|
|
5527
5761
|
if (onSubmit === undefined) {
|
|
5528
5762
|
await defaultOnSubmit(defaultSubmitPromise(data));
|
|
5529
5763
|
return;
|
|
5530
5764
|
}
|
|
5531
5765
|
await defaultOnSubmit(onSubmit(data));
|
|
5532
5766
|
};
|
|
5767
|
+
// Custom error renderer for validation errors with i18n support
|
|
5768
|
+
const renderValidationErrors = (validationErrors) => {
|
|
5769
|
+
const title = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5770
|
+
? `表單驗證失敗 (${validationErrors.length} 個錯誤${validationErrors.length > 1 ? '' : ''})`
|
|
5771
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5772
|
+
? `表单验证失败 (${validationErrors.length} 个错误${validationErrors.length > 1 ? '' : ''})`
|
|
5773
|
+
: `Form Validation Failed (${validationErrors.length} error${validationErrors.length > 1 ? 's' : ''})`;
|
|
5774
|
+
const formLabel = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5775
|
+
? '表單'
|
|
5776
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5777
|
+
? '表单'
|
|
5778
|
+
: 'Form';
|
|
5779
|
+
const currentValueLabel = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5780
|
+
? '目前值:'
|
|
5781
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5782
|
+
? '当前值:'
|
|
5783
|
+
: 'Current value:';
|
|
5784
|
+
return (jsxRuntime.jsxs(react.Alert.Root, { status: "error", children: [jsxRuntime.jsx(react.Alert.Indicator, {}), jsxRuntime.jsx(react.Alert.Title, { children: jsxRuntime.jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxRuntime.jsxs(AccordionItem, { value: "validation-errors", children: [jsxRuntime.jsx(AccordionItemTrigger, { children: title }), jsxRuntime.jsx(AccordionItemContent, { children: jsxRuntime.jsx(react.Box, { mt: 2, children: validationErrors.map((err, index) => (jsxRuntime.jsxs(react.Box, { mb: 2, p: 2, bg: "red.50", borderLeft: "4px solid", borderColor: "red.500", children: [jsxRuntime.jsxs(react.Text, { fontWeight: "bold", color: "red.700", children: [err.field === 'root' ? formLabel : err.field, ":"] }), jsxRuntime.jsx(react.Text, { color: "red.600", children: err.message }), err.value !== undefined && (jsxRuntime.jsxs(react.Text, { fontSize: "sm", color: "red.500", mt: 1, children: [currentValueLabel, " ", JSON.stringify(err.value)] }))] }, index))) }) })] }) }) })] }));
|
|
5785
|
+
};
|
|
5533
5786
|
const renderColumns = ({ order, keys, ignore, include, }) => {
|
|
5534
5787
|
const included = include.length > 0 ? include : keys;
|
|
5535
5788
|
const not_exist = included.filter((columnA) => !order.some((columnB) => columnA === columnB));
|
|
@@ -5565,7 +5818,7 @@ const FormBody = () => {
|
|
|
5565
5818
|
setIsConfirming(false);
|
|
5566
5819
|
}, variant: "subtle", children: translate.t("cancel") }), jsxRuntime.jsx(react.Button, { onClick: () => {
|
|
5567
5820
|
onFormSubmit(validatedData);
|
|
5568
|
-
}, children: translate.t("confirm") })] }), isSubmiting && (jsxRuntime.jsx(react.Box, { pos: "absolute", inset: "0", bg: "bg/80", children: jsxRuntime.jsx(react.Center, { h: "full", children: jsxRuntime.jsx(react.Spinner, { color: "teal.500" }) }) })), isError && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: customErrorRenderer ? (customErrorRenderer(error)) : (jsxRuntime.jsx(react.Alert.Root, { status: "error", children: jsxRuntime.jsx(react.Alert.Title, { children: jsxRuntime.jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxRuntime.jsxs(AccordionItem, { value: "b", children: [jsxRuntime.jsxs(AccordionItemTrigger, { children: [jsxRuntime.jsx(react.Alert.Indicator, {}), `${error}`] }), jsxRuntime.jsx(AccordionItemContent, { children: `${JSON.stringify(error)}` })] }) }) }) })) }))] }));
|
|
5821
|
+
}, children: translate.t("confirm") })] }), isSubmiting && (jsxRuntime.jsx(react.Box, { pos: "absolute", inset: "0", bg: "bg/80", children: jsxRuntime.jsx(react.Center, { h: "full", children: jsxRuntime.jsx(react.Spinner, { color: "teal.500" }) }) })), isError && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: customErrorRenderer ? (customErrorRenderer(error)) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: error?.type === 'validation' && error?.errors ? (renderValidationErrors(error.errors)) : (jsxRuntime.jsx(react.Alert.Root, { status: "error", children: jsxRuntime.jsx(react.Alert.Title, { children: jsxRuntime.jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxRuntime.jsxs(AccordionItem, { value: "b", children: [jsxRuntime.jsxs(AccordionItemTrigger, { children: [jsxRuntime.jsx(react.Alert.Indicator, {}), `${error}`] }), jsxRuntime.jsx(AccordionItemContent, { children: `${JSON.stringify(error)}` })] }) }) }) })) })) }))] }));
|
|
5569
5822
|
}
|
|
5570
5823
|
return (jsxRuntime.jsxs(react.Flex, { flexFlow: "column", gap: "2", children: [jsxRuntime.jsx(react.Grid, { gap: "4", gridTemplateColumns: "repeat(12, 1fr)", autoFlow: "row", children: ordered.map((column) => {
|
|
5571
5824
|
return (jsxRuntime.jsx(ColumnRenderer
|
|
@@ -5575,7 +5828,7 @@ const FormBody = () => {
|
|
|
5575
5828
|
properties: properties, prefix: ``, column }, `form-input-${column}`));
|
|
5576
5829
|
}) }), jsxRuntime.jsxs(react.Flex, { justifyContent: "end", gap: "2", children: [jsxRuntime.jsx(react.Button, { onClick: () => {
|
|
5577
5830
|
methods.reset();
|
|
5578
|
-
}, variant: "subtle", children: translate.t("reset") }), jsxRuntime.jsx(SubmitButton, {})] })] }));
|
|
5831
|
+
}, variant: "subtle", children: translate.t("reset") }), jsxRuntime.jsx(SubmitButton, {})] }), isError && error?.type === 'validation' && (jsxRuntime.jsx(react.Box, { mt: 4, children: error?.errors && renderValidationErrors(error.errors) }))] }));
|
|
5579
5832
|
};
|
|
5580
5833
|
|
|
5581
5834
|
const FormTitle = () => {
|
|
@@ -5658,12 +5911,16 @@ exports.TableSorter = TableSorter;
|
|
|
5658
5911
|
exports.TableViewer = TableViewer;
|
|
5659
5912
|
exports.TextCell = TextCell;
|
|
5660
5913
|
exports.ViewDialog = ViewDialog;
|
|
5914
|
+
exports.createSchemaValidator = createSchemaValidator;
|
|
5661
5915
|
exports.getColumns = getColumns;
|
|
5662
5916
|
exports.getMultiDates = getMultiDates;
|
|
5663
5917
|
exports.getRangeDates = getRangeDates;
|
|
5918
|
+
exports.getSupportedLocales = getSupportedLocales;
|
|
5664
5919
|
exports.idPickerSanityCheck = idPickerSanityCheck;
|
|
5920
|
+
exports.isLocaleSupported = isLocaleSupported;
|
|
5665
5921
|
exports.useDataTable = useDataTable;
|
|
5666
5922
|
exports.useDataTableContext = useDataTableContext;
|
|
5667
5923
|
exports.useDataTableServer = useDataTableServer;
|
|
5668
5924
|
exports.useForm = useForm;
|
|
5925
|
+
exports.validateData = validateData;
|
|
5669
5926
|
exports.widthSanityCheck = widthSanityCheck;
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,10 @@ import { GrAscend, GrDescend } from 'react-icons/gr';
|
|
|
28
28
|
import { useTranslation } from 'react-i18next';
|
|
29
29
|
import axios from 'axios';
|
|
30
30
|
import { FormProvider, useFormContext, useForm as useForm$1 } from 'react-hook-form';
|
|
31
|
+
import Ajv from 'ajv';
|
|
32
|
+
import addFormats from 'ajv-formats';
|
|
33
|
+
import zh_TW from 'ajv-i18n/localize/zh-TW';
|
|
34
|
+
import zh_CN from 'ajv-i18n/localize/zh';
|
|
31
35
|
import dayjs from 'dayjs';
|
|
32
36
|
import utc from 'dayjs/plugin/utc';
|
|
33
37
|
import { TiDeleteOutline } from 'react-icons/ti';
|
|
@@ -3668,6 +3672,7 @@ const SchemaFormContext = createContext({
|
|
|
3668
3672
|
onSubmit: async () => { },
|
|
3669
3673
|
rowNumber: 0,
|
|
3670
3674
|
requestOptions: {},
|
|
3675
|
+
validationLocale: 'en',
|
|
3671
3676
|
});
|
|
3672
3677
|
|
|
3673
3678
|
const useSchemaContext = () => {
|
|
@@ -3678,6 +3683,179 @@ const clearEmptyString = (object) => {
|
|
|
3678
3683
|
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ""));
|
|
3679
3684
|
};
|
|
3680
3685
|
|
|
3686
|
+
// AJV i18n support
|
|
3687
|
+
const localize = {
|
|
3688
|
+
en: () => { }, // English is default, no localization needed
|
|
3689
|
+
'zh-HK': zh_TW, // Use zh-TW for Hong Kong Traditional Chinese
|
|
3690
|
+
'zh-TW': zh_TW, // Traditional Chinese (Taiwan)
|
|
3691
|
+
'zh-CN': zh_CN, // Simplified Chinese
|
|
3692
|
+
'zh': zh_CN, // Simplified Chinese (short form)
|
|
3693
|
+
};
|
|
3694
|
+
// Create AJV instance with format support
|
|
3695
|
+
const createValidator = () => {
|
|
3696
|
+
const ajv = new Ajv({
|
|
3697
|
+
allErrors: true,
|
|
3698
|
+
verbose: true,
|
|
3699
|
+
removeAdditional: false,
|
|
3700
|
+
strict: false,
|
|
3701
|
+
messages: false, // Disable default messages for i18n
|
|
3702
|
+
});
|
|
3703
|
+
// Add format validation support (date, time, email, etc.)
|
|
3704
|
+
addFormats(ajv);
|
|
3705
|
+
return ajv;
|
|
3706
|
+
};
|
|
3707
|
+
/**
|
|
3708
|
+
* Validates data against a JSON Schema using AJV with i18n support
|
|
3709
|
+
* @param data - The data to validate
|
|
3710
|
+
* @param schema - The JSON Schema to validate against
|
|
3711
|
+
* @param options - Validation options including locale
|
|
3712
|
+
* @returns ValidationResult containing validation status and errors
|
|
3713
|
+
*/
|
|
3714
|
+
const validateData = (data, schema, options = {}) => {
|
|
3715
|
+
const { locale = 'en' } = options;
|
|
3716
|
+
const ajv = createValidator();
|
|
3717
|
+
try {
|
|
3718
|
+
const validate = ajv.compile(schema);
|
|
3719
|
+
const isValid = validate(data);
|
|
3720
|
+
if (isValid) {
|
|
3721
|
+
return {
|
|
3722
|
+
isValid: true,
|
|
3723
|
+
errors: [],
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
// Apply localization if not English
|
|
3727
|
+
if (locale !== 'en' && validate.errors && localize[locale]) {
|
|
3728
|
+
try {
|
|
3729
|
+
localize[locale](validate.errors);
|
|
3730
|
+
}
|
|
3731
|
+
catch (error) {
|
|
3732
|
+
console.warn(`Failed to localize validation errors to ${locale}:`, error);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
const errors = (validate.errors || []).map((error) => {
|
|
3736
|
+
const field = error.instancePath?.replace(/^\//, '') || error.schemaPath?.split('/').pop() || 'root';
|
|
3737
|
+
let message = error.message || 'Validation error';
|
|
3738
|
+
// Enhanced error messages for better UX (only if using English or localization failed)
|
|
3739
|
+
if (locale === 'en' || !error.message) {
|
|
3740
|
+
switch (error.keyword) {
|
|
3741
|
+
case 'required':
|
|
3742
|
+
message = `${error.params?.missingProperty || 'Field'} is required`;
|
|
3743
|
+
break;
|
|
3744
|
+
case 'format':
|
|
3745
|
+
message = `Invalid ${error.params?.format} format`;
|
|
3746
|
+
break;
|
|
3747
|
+
case 'type':
|
|
3748
|
+
message = `Expected ${error.params?.type}, got ${typeof error.data}`;
|
|
3749
|
+
break;
|
|
3750
|
+
case 'minLength':
|
|
3751
|
+
message = `Must be at least ${error.params?.limit} characters`;
|
|
3752
|
+
break;
|
|
3753
|
+
case 'maxLength':
|
|
3754
|
+
message = `Must be no more than ${error.params?.limit} characters`;
|
|
3755
|
+
break;
|
|
3756
|
+
case 'minimum':
|
|
3757
|
+
message = `Must be at least ${error.params?.limit}`;
|
|
3758
|
+
break;
|
|
3759
|
+
case 'maximum':
|
|
3760
|
+
message = `Must be no more than ${error.params?.limit}`;
|
|
3761
|
+
break;
|
|
3762
|
+
case 'pattern':
|
|
3763
|
+
message = `Does not match required pattern`;
|
|
3764
|
+
break;
|
|
3765
|
+
case 'enum':
|
|
3766
|
+
message = `Must be one of: ${error.params?.allowedValues?.join(', ')}`;
|
|
3767
|
+
break;
|
|
3768
|
+
default:
|
|
3769
|
+
message = error.message || 'Validation error';
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
return {
|
|
3773
|
+
field: field || error.instancePath || 'unknown',
|
|
3774
|
+
message,
|
|
3775
|
+
value: error.data,
|
|
3776
|
+
schemaPath: error.schemaPath,
|
|
3777
|
+
};
|
|
3778
|
+
});
|
|
3779
|
+
return {
|
|
3780
|
+
isValid: false,
|
|
3781
|
+
errors,
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
catch (error) {
|
|
3785
|
+
// Handle AJV compilation errors
|
|
3786
|
+
const errorMessage = locale === 'zh-HK' || locale === 'zh-TW'
|
|
3787
|
+
? `架構驗證錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`
|
|
3788
|
+
: locale === 'zh-CN' || locale === 'zh'
|
|
3789
|
+
? `模式验证错误: ${error instanceof Error ? error.message : '未知错误'}`
|
|
3790
|
+
: `Schema validation error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
3791
|
+
return {
|
|
3792
|
+
isValid: false,
|
|
3793
|
+
errors: [
|
|
3794
|
+
{
|
|
3795
|
+
field: 'schema',
|
|
3796
|
+
message: errorMessage,
|
|
3797
|
+
},
|
|
3798
|
+
],
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
};
|
|
3802
|
+
/**
|
|
3803
|
+
* Creates a reusable validator function for a specific schema with i18n support
|
|
3804
|
+
* @param schema - The JSON Schema to create validator for
|
|
3805
|
+
* @param locale - The locale to use for error messages
|
|
3806
|
+
* @returns A function that validates data against the schema
|
|
3807
|
+
*/
|
|
3808
|
+
const createSchemaValidator = (schema, locale = 'en') => {
|
|
3809
|
+
const ajv = createValidator();
|
|
3810
|
+
const validate = ajv.compile(schema);
|
|
3811
|
+
return (data) => {
|
|
3812
|
+
const isValid = validate(data);
|
|
3813
|
+
if (isValid) {
|
|
3814
|
+
return {
|
|
3815
|
+
isValid: true,
|
|
3816
|
+
errors: [],
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
// Apply localization if not English
|
|
3820
|
+
if (locale !== 'en' && validate.errors && localize[locale]) {
|
|
3821
|
+
try {
|
|
3822
|
+
localize[locale](validate.errors);
|
|
3823
|
+
}
|
|
3824
|
+
catch (error) {
|
|
3825
|
+
console.warn(`Failed to localize validation errors to ${locale}:`, error);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
const errors = (validate.errors || []).map((error) => {
|
|
3829
|
+
const field = error.instancePath?.replace(/^\//, '') || 'root';
|
|
3830
|
+
return {
|
|
3831
|
+
field,
|
|
3832
|
+
message: error.message || 'Validation error',
|
|
3833
|
+
value: error.data,
|
|
3834
|
+
schemaPath: error.schemaPath,
|
|
3835
|
+
};
|
|
3836
|
+
});
|
|
3837
|
+
return {
|
|
3838
|
+
isValid: false,
|
|
3839
|
+
errors,
|
|
3840
|
+
};
|
|
3841
|
+
};
|
|
3842
|
+
};
|
|
3843
|
+
/**
|
|
3844
|
+
* Get available locales for validation error messages
|
|
3845
|
+
* @returns Array of supported locale codes
|
|
3846
|
+
*/
|
|
3847
|
+
const getSupportedLocales = () => {
|
|
3848
|
+
return Object.keys(localize);
|
|
3849
|
+
};
|
|
3850
|
+
/**
|
|
3851
|
+
* Check if a locale is supported
|
|
3852
|
+
* @param locale - The locale to check
|
|
3853
|
+
* @returns Boolean indicating if the locale is supported
|
|
3854
|
+
*/
|
|
3855
|
+
const isLocaleSupported = (locale) => {
|
|
3856
|
+
return locale in localize;
|
|
3857
|
+
};
|
|
3858
|
+
|
|
3681
3859
|
const idPickerSanityCheck = (column, foreign_key) => {
|
|
3682
3860
|
if (!!foreign_key == false) {
|
|
3683
3861
|
throw new Error(`The key foreign_key does not exist in properties of column ${column} when using id-picker.`);
|
|
@@ -3693,7 +3871,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
3693
3871
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
3694
3872
|
}
|
|
3695
3873
|
};
|
|
3696
|
-
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, }) => {
|
|
3874
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, validationLocale = 'en', }) => {
|
|
3697
3875
|
const [isSuccess, setIsSuccess] = useState(false);
|
|
3698
3876
|
const [isError, setIsError] = useState(false);
|
|
3699
3877
|
const [isSubmiting, setIsSubmiting] = useState(false);
|
|
@@ -3727,6 +3905,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
3727
3905
|
setError,
|
|
3728
3906
|
getUpdatedData,
|
|
3729
3907
|
customErrorRenderer,
|
|
3908
|
+
validationLocale,
|
|
3730
3909
|
}, children: jsx(FormProvider, { ...form, children: children }) }));
|
|
3731
3910
|
};
|
|
3732
3911
|
|
|
@@ -4622,7 +4801,7 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
|
|
|
4622
4801
|
|
|
4623
4802
|
const NumberInputRoot = React.forwardRef(function NumberInput$1(props, ref) {
|
|
4624
4803
|
const { children, ...rest } = props;
|
|
4625
|
-
return (
|
|
4804
|
+
return (jsx(NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: children }));
|
|
4626
4805
|
});
|
|
4627
4806
|
const NumberInputField$1 = NumberInput.Input;
|
|
4628
4807
|
NumberInput.Scrubber;
|
|
@@ -5450,10 +5629,28 @@ const ColumnViewer = ({ column, properties, prefix, }) => {
|
|
|
5450
5629
|
};
|
|
5451
5630
|
|
|
5452
5631
|
const SubmitButton = () => {
|
|
5453
|
-
const { translate, setValidatedData, setIsError, setIsConfirming } = useSchemaContext();
|
|
5632
|
+
const { translate, setValidatedData, setIsError, setIsConfirming, setError, schema, validationLocale } = useSchemaContext();
|
|
5454
5633
|
const methods = useFormContext();
|
|
5455
5634
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5456
5635
|
const onValid = (data) => {
|
|
5636
|
+
// Validate data using AJV before proceeding to confirmation
|
|
5637
|
+
const validationResult = validateData(data, schema, { locale: validationLocale });
|
|
5638
|
+
if (!validationResult.isValid) {
|
|
5639
|
+
// Set validation errors with i18n support
|
|
5640
|
+
const validationErrorMessage = {
|
|
5641
|
+
type: 'validation',
|
|
5642
|
+
errors: validationResult.errors,
|
|
5643
|
+
message: validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5644
|
+
? '表單驗證失敗'
|
|
5645
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5646
|
+
? '表单验证失败'
|
|
5647
|
+
: 'Form validation failed'
|
|
5648
|
+
};
|
|
5649
|
+
setError(validationErrorMessage);
|
|
5650
|
+
setIsError(true);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
// If validation passes, proceed to confirmation
|
|
5457
5654
|
setValidatedData(data);
|
|
5458
5655
|
setIsError(false);
|
|
5459
5656
|
setIsConfirming(true);
|
|
@@ -5464,7 +5661,7 @@ const SubmitButton = () => {
|
|
|
5464
5661
|
};
|
|
5465
5662
|
|
|
5466
5663
|
const FormBody = () => {
|
|
5467
|
-
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, customErrorRenderer, } = useSchemaContext();
|
|
5664
|
+
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, customErrorRenderer, validationLocale, } = useSchemaContext();
|
|
5468
5665
|
const methods = useFormContext();
|
|
5469
5666
|
const { properties } = schema;
|
|
5470
5667
|
const onBeforeSubmit = () => {
|
|
@@ -5480,6 +5677,27 @@ const FormBody = () => {
|
|
|
5480
5677
|
const onSubmitSuccess = () => {
|
|
5481
5678
|
setIsSuccess(true);
|
|
5482
5679
|
};
|
|
5680
|
+
// Enhanced validation function using AJV with i18n support
|
|
5681
|
+
const validateFormData = (data) => {
|
|
5682
|
+
try {
|
|
5683
|
+
const validationResult = validateData(data, schema, { locale: validationLocale });
|
|
5684
|
+
return validationResult;
|
|
5685
|
+
}
|
|
5686
|
+
catch (error) {
|
|
5687
|
+
const errorMessage = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5688
|
+
? `驗證錯誤: ${error instanceof Error ? error.message : '未知驗證錯誤'}`
|
|
5689
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5690
|
+
? `验证错误: ${error instanceof Error ? error.message : '未知验证错误'}`
|
|
5691
|
+
: `Validation error: ${error instanceof Error ? error.message : 'Unknown validation error'}`;
|
|
5692
|
+
return {
|
|
5693
|
+
isValid: false,
|
|
5694
|
+
errors: [{
|
|
5695
|
+
field: 'validation',
|
|
5696
|
+
message: errorMessage
|
|
5697
|
+
}]
|
|
5698
|
+
};
|
|
5699
|
+
}
|
|
5700
|
+
};
|
|
5483
5701
|
const defaultOnSubmit = async (promise) => {
|
|
5484
5702
|
try {
|
|
5485
5703
|
onBeforeSubmit();
|
|
@@ -5504,12 +5722,47 @@ const FormBody = () => {
|
|
|
5504
5722
|
};
|
|
5505
5723
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5506
5724
|
const onFormSubmit = async (data) => {
|
|
5725
|
+
// Validate data using AJV before submission
|
|
5726
|
+
const validationResult = validateFormData(data);
|
|
5727
|
+
if (!validationResult.isValid) {
|
|
5728
|
+
// Set validation errors
|
|
5729
|
+
const validationErrorMessage = {
|
|
5730
|
+
type: 'validation',
|
|
5731
|
+
errors: validationResult.errors,
|
|
5732
|
+
message: validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5733
|
+
? '表單驗證失敗'
|
|
5734
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5735
|
+
? '表单验证失败'
|
|
5736
|
+
: 'Form validation failed'
|
|
5737
|
+
};
|
|
5738
|
+
onSubmitError(validationErrorMessage);
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5507
5741
|
if (onSubmit === undefined) {
|
|
5508
5742
|
await defaultOnSubmit(defaultSubmitPromise(data));
|
|
5509
5743
|
return;
|
|
5510
5744
|
}
|
|
5511
5745
|
await defaultOnSubmit(onSubmit(data));
|
|
5512
5746
|
};
|
|
5747
|
+
// Custom error renderer for validation errors with i18n support
|
|
5748
|
+
const renderValidationErrors = (validationErrors) => {
|
|
5749
|
+
const title = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5750
|
+
? `表單驗證失敗 (${validationErrors.length} 個錯誤${validationErrors.length > 1 ? '' : ''})`
|
|
5751
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5752
|
+
? `表单验证失败 (${validationErrors.length} 个错误${validationErrors.length > 1 ? '' : ''})`
|
|
5753
|
+
: `Form Validation Failed (${validationErrors.length} error${validationErrors.length > 1 ? 's' : ''})`;
|
|
5754
|
+
const formLabel = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5755
|
+
? '表單'
|
|
5756
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5757
|
+
? '表单'
|
|
5758
|
+
: 'Form';
|
|
5759
|
+
const currentValueLabel = validationLocale === 'zh-HK' || validationLocale === 'zh-TW'
|
|
5760
|
+
? '目前值:'
|
|
5761
|
+
: validationLocale === 'zh-CN' || validationLocale === 'zh'
|
|
5762
|
+
? '当前值:'
|
|
5763
|
+
: 'Current value:';
|
|
5764
|
+
return (jsxs(Alert.Root, { status: "error", children: [jsx(Alert.Indicator, {}), jsx(Alert.Title, { children: jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxs(AccordionItem, { value: "validation-errors", children: [jsx(AccordionItemTrigger, { children: title }), jsx(AccordionItemContent, { children: jsx(Box, { mt: 2, children: validationErrors.map((err, index) => (jsxs(Box, { mb: 2, p: 2, bg: "red.50", borderLeft: "4px solid", borderColor: "red.500", children: [jsxs(Text, { fontWeight: "bold", color: "red.700", children: [err.field === 'root' ? formLabel : err.field, ":"] }), jsx(Text, { color: "red.600", children: err.message }), err.value !== undefined && (jsxs(Text, { fontSize: "sm", color: "red.500", mt: 1, children: [currentValueLabel, " ", JSON.stringify(err.value)] }))] }, index))) }) })] }) }) })] }));
|
|
5765
|
+
};
|
|
5513
5766
|
const renderColumns = ({ order, keys, ignore, include, }) => {
|
|
5514
5767
|
const included = include.length > 0 ? include : keys;
|
|
5515
5768
|
const not_exist = included.filter((columnA) => !order.some((columnB) => columnA === columnB));
|
|
@@ -5545,7 +5798,7 @@ const FormBody = () => {
|
|
|
5545
5798
|
setIsConfirming(false);
|
|
5546
5799
|
}, variant: "subtle", children: translate.t("cancel") }), jsx(Button$1, { onClick: () => {
|
|
5547
5800
|
onFormSubmit(validatedData);
|
|
5548
|
-
}, children: translate.t("confirm") })] }), isSubmiting && (jsx(Box, { pos: "absolute", inset: "0", bg: "bg/80", children: jsx(Center, { h: "full", children: jsx(Spinner, { color: "teal.500" }) }) })), isError && (jsx(Fragment, { children: customErrorRenderer ? (customErrorRenderer(error)) : (jsx(Alert.Root, { status: "error", children: jsx(Alert.Title, { children: jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxs(AccordionItem, { value: "b", children: [jsxs(AccordionItemTrigger, { children: [jsx(Alert.Indicator, {}), `${error}`] }), jsx(AccordionItemContent, { children: `${JSON.stringify(error)}` })] }) }) }) })) }))] }));
|
|
5801
|
+
}, children: translate.t("confirm") })] }), isSubmiting && (jsx(Box, { pos: "absolute", inset: "0", bg: "bg/80", children: jsx(Center, { h: "full", children: jsx(Spinner, { color: "teal.500" }) }) })), isError && (jsx(Fragment, { children: customErrorRenderer ? (customErrorRenderer(error)) : (jsx(Fragment, { children: error?.type === 'validation' && error?.errors ? (renderValidationErrors(error.errors)) : (jsx(Alert.Root, { status: "error", children: jsx(Alert.Title, { children: jsx(AccordionRoot, { collapsible: true, defaultValue: [], children: jsxs(AccordionItem, { value: "b", children: [jsxs(AccordionItemTrigger, { children: [jsx(Alert.Indicator, {}), `${error}`] }), jsx(AccordionItemContent, { children: `${JSON.stringify(error)}` })] }) }) }) })) })) }))] }));
|
|
5549
5802
|
}
|
|
5550
5803
|
return (jsxs(Flex, { flexFlow: "column", gap: "2", children: [jsx(Grid, { gap: "4", gridTemplateColumns: "repeat(12, 1fr)", autoFlow: "row", children: ordered.map((column) => {
|
|
5551
5804
|
return (jsx(ColumnRenderer
|
|
@@ -5555,7 +5808,7 @@ const FormBody = () => {
|
|
|
5555
5808
|
properties: properties, prefix: ``, column }, `form-input-${column}`));
|
|
5556
5809
|
}) }), jsxs(Flex, { justifyContent: "end", gap: "2", children: [jsx(Button$1, { onClick: () => {
|
|
5557
5810
|
methods.reset();
|
|
5558
|
-
}, variant: "subtle", children: translate.t("reset") }), jsx(SubmitButton, {})] })] }));
|
|
5811
|
+
}, variant: "subtle", children: translate.t("reset") }), jsx(SubmitButton, {})] }), isError && error?.type === 'validation' && (jsx(Box, { mt: 4, children: error?.errors && renderValidationErrors(error.errors) }))] }));
|
|
5559
5812
|
};
|
|
5560
5813
|
|
|
5561
5814
|
const FormTitle = () => {
|
|
@@ -5597,4 +5850,4 @@ const getMultiDates = ({ selected, selectedDate, selectedDates, selectable, }) =
|
|
|
5597
5850
|
}
|
|
5598
5851
|
};
|
|
5599
5852
|
|
|
5600
|
-
export { CardHeader, DataDisplay, DataTable, DataTableServer, DefaultCardTitle, DefaultForm, DefaultTable, DensityToggleButton, EditSortingButton, EmptyState$1 as EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, ViewDialog, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
|
|
5853
|
+
export { CardHeader, DataDisplay, DataTable, DataTableServer, DefaultCardTitle, DefaultForm, DefaultTable, DensityToggleButton, EditSortingButton, EmptyState$1 as EmptyState, ErrorAlert, FilterDialog, FormBody, FormRoot, FormTitle, GlobalFilter, PageSizeControl, Pagination, RecordDisplay, ReloadButton, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, RowCountText, Table, TableBody, TableCardContainer, TableCards, TableComponent, TableControls, TableDataDisplay, TableFilter, TableFilterTags, TableFooter, TableHeader, TableLoadingComponent, TableSelector, TableSorter, TableViewer, TextCell, ViewDialog, createSchemaValidator, getColumns, getMultiDates, getRangeDates, getSupportedLocales, idPickerSanityCheck, isLocaleSupported, useDataTable, useDataTableContext, useDataTableServer, useForm, validateData, widthSanityCheck };
|
|
@@ -3,6 +3,7 @@ import { JSONSchema7 } from "json-schema";
|
|
|
3
3
|
import { Dispatch, ReactNode, SetStateAction } from "react";
|
|
4
4
|
import { FieldValues } from "react-hook-form";
|
|
5
5
|
import { UseTranslationResponse } from "react-i18next";
|
|
6
|
+
import { SupportedLocale } from "./utils/validation";
|
|
6
7
|
export interface SchemaFormContext<TData extends FieldValues> {
|
|
7
8
|
schema: JSONSchema7;
|
|
8
9
|
serverUrl: string;
|
|
@@ -30,5 +31,6 @@ export interface SchemaFormContext<TData extends FieldValues> {
|
|
|
30
31
|
setError: Dispatch<SetStateAction<unknown>>;
|
|
31
32
|
getUpdatedData: () => TData | Promise<TData>;
|
|
32
33
|
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
34
|
+
validationLocale?: SupportedLocale;
|
|
33
35
|
}
|
|
34
36
|
export declare const SchemaFormContext: import("react").Context<SchemaFormContext<unknown>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ForeignKeyProps } from "@/components/Form/components/fields/StringInputField";
|
|
2
|
+
import { SupportedLocale } from "@/components/Form/utils/validation";
|
|
2
3
|
import { AxiosRequestConfig } from "axios";
|
|
3
4
|
import { JSONSchema7 } from "json-schema";
|
|
4
5
|
import { Dispatch, ReactNode, SetStateAction } from "react";
|
|
@@ -22,6 +23,7 @@ export interface FormRootProps<TData extends FieldValues> {
|
|
|
22
23
|
requestOptions?: AxiosRequestConfig;
|
|
23
24
|
getUpdatedData?: () => TData | Promise<TData> | void;
|
|
24
25
|
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
26
|
+
validationLocale?: SupportedLocale;
|
|
25
27
|
}
|
|
26
28
|
export interface CustomJSONSchema7Definition extends JSONSchema7 {
|
|
27
29
|
variant: string;
|
|
@@ -38,4 +40,4 @@ export declare const idPickerSanityCheck: (column: string, foreign_key?: {
|
|
|
38
40
|
column?: string | undefined;
|
|
39
41
|
display_column?: string | undefined;
|
|
40
42
|
} | undefined) => void;
|
|
41
|
-
export declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, }: FormRootProps<TData>) => import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, validationLocale, }: FormRootProps<TData>) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { JSONSchema7 } from "json-schema";
|
|
2
|
+
declare const localize: {
|
|
3
|
+
en: () => void;
|
|
4
|
+
'zh-HK': import("ajv-i18n/localize/types").Localize;
|
|
5
|
+
'zh-TW': import("ajv-i18n/localize/types").Localize;
|
|
6
|
+
'zh-CN': import("ajv-i18n/localize/types").Localize;
|
|
7
|
+
zh: import("ajv-i18n/localize/types").Localize;
|
|
8
|
+
};
|
|
9
|
+
export type SupportedLocale = keyof typeof localize;
|
|
10
|
+
export interface ValidationError {
|
|
11
|
+
field: string;
|
|
12
|
+
message: string;
|
|
13
|
+
value?: unknown;
|
|
14
|
+
schemaPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
isValid: boolean;
|
|
18
|
+
errors: ValidationError[];
|
|
19
|
+
}
|
|
20
|
+
export interface ValidationOptions {
|
|
21
|
+
locale?: SupportedLocale;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validates data against a JSON Schema using AJV with i18n support
|
|
25
|
+
* @param data - The data to validate
|
|
26
|
+
* @param schema - The JSON Schema to validate against
|
|
27
|
+
* @param options - Validation options including locale
|
|
28
|
+
* @returns ValidationResult containing validation status and errors
|
|
29
|
+
*/
|
|
30
|
+
export declare const validateData: (data: unknown, schema: JSONSchema7, options?: ValidationOptions) => ValidationResult;
|
|
31
|
+
/**
|
|
32
|
+
* Creates a reusable validator function for a specific schema with i18n support
|
|
33
|
+
* @param schema - The JSON Schema to create validator for
|
|
34
|
+
* @param locale - The locale to use for error messages
|
|
35
|
+
* @returns A function that validates data against the schema
|
|
36
|
+
*/
|
|
37
|
+
export declare const createSchemaValidator: (schema: JSONSchema7, locale?: SupportedLocale) => (data: unknown) => ValidationResult;
|
|
38
|
+
/**
|
|
39
|
+
* Get available locales for validation error messages
|
|
40
|
+
* @returns Array of supported locale codes
|
|
41
|
+
*/
|
|
42
|
+
export declare const getSupportedLocales: () => SupportedLocale[];
|
|
43
|
+
/**
|
|
44
|
+
* Check if a locale is supported
|
|
45
|
+
* @param locale - The locale to check
|
|
46
|
+
* @returns Boolean indicating if the locale is supported
|
|
47
|
+
*/
|
|
48
|
+
export declare const isLocaleSupported: (locale: string) => locale is "en" | "zh-HK" | "zh-TW" | "zh-CN" | "zh";
|
|
49
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -121,3 +121,5 @@ export * from "./components/DatePicker/getMultiDates";
|
|
|
121
121
|
export * from "./components/DatePicker/getRangeDates";
|
|
122
122
|
export * from "./components/DatePicker/RangeDatePicker";
|
|
123
123
|
export * from "./components/DataTable/display/RecordDisplay";
|
|
124
|
+
export * from "./components/Form/utils/validation";
|
|
125
|
+
export type { SupportedLocale, ValidationOptions } from "./components/Form/utils/validation";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsol-oss/react-datatable5",
|
|
3
|
-
"version": "12.0.0-beta.
|
|
3
|
+
"version": "12.0.0-beta.57",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"@tanstack/react-query": "^5.66.9",
|
|
44
44
|
"@tanstack/react-table": "^8.21.2",
|
|
45
45
|
"@uidotdev/usehooks": "^2.4.1",
|
|
46
|
+
"ajv": "^8.12.0",
|
|
47
|
+
"ajv-formats": "^3.0.1",
|
|
48
|
+
"ajv-i18n": "^4.2.0",
|
|
46
49
|
"axios": "^1.7.9",
|
|
47
50
|
"dayjs": "^1.11.13",
|
|
48
51
|
"next-themes": "^0.4.4",
|
|
@@ -70,6 +73,9 @@
|
|
|
70
73
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
71
74
|
"@typescript-eslint/parser": "^7.2.0",
|
|
72
75
|
"@vitejs/plugin-react": "^4.2.1",
|
|
76
|
+
"ajv": "^8.12.0",
|
|
77
|
+
"ajv-formats": "^3.0.1",
|
|
78
|
+
"ajv-i18n": "^4.2.0",
|
|
73
79
|
"eslint": "^8.57.0",
|
|
74
80
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
75
81
|
"eslint-plugin-react-refresh": "^0.4.6",
|