@bsol-oss/react-datatable5 12.0.0-beta.55 → 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 +54 -2
- package/dist/index.js +267 -7
- package/dist/index.mjs +264 -8
- package/dist/types/components/Form/SchemaFormContext.d.ts +4 -1
- package/dist/types/components/Form/components/core/FormRoot.d.ts +4 -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;
|
|
@@ -546,6 +548,8 @@ interface FormRootProps<TData extends FieldValues> {
|
|
|
546
548
|
rowNumber?: number | string;
|
|
547
549
|
requestOptions?: AxiosRequestConfig;
|
|
548
550
|
getUpdatedData?: () => TData | Promise<TData> | void;
|
|
551
|
+
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
552
|
+
validationLocale?: SupportedLocale$1;
|
|
549
553
|
}
|
|
550
554
|
interface CustomJSONSchema7Definition extends JSONSchema7 {
|
|
551
555
|
variant: string;
|
|
@@ -562,7 +566,7 @@ declare const idPickerSanityCheck: (column: string, foreign_key?: {
|
|
|
562
566
|
column?: string | undefined;
|
|
563
567
|
display_column?: string | undefined;
|
|
564
568
|
} | undefined) => void;
|
|
565
|
-
declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, }: 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;
|
|
566
570
|
|
|
567
571
|
interface DefaultFormProps<TData extends FieldValues> {
|
|
568
572
|
formConfig: Omit<FormRootProps<TData>, "children">;
|
|
@@ -637,6 +641,54 @@ interface RecordDisplayProps {
|
|
|
637
641
|
}
|
|
638
642
|
declare const RecordDisplay: ({ object, boxProps, translate, prefix, }: RecordDisplayProps) => react_jsx_runtime.JSX.Element;
|
|
639
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
|
+
|
|
640
692
|
declare module "@tanstack/react-table" {
|
|
641
693
|
interface ColumnMeta<TData extends RowData, TValue> {
|
|
642
694
|
/**
|
|
@@ -709,4 +761,4 @@ declare module "@tanstack/react-table" {
|
|
|
709
761
|
}
|
|
710
762
|
}
|
|
711
763
|
|
|
712
|
-
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');
|
|
@@ -3160,7 +3164,9 @@ const TableBody = ({ showSelector = false, canResize = true, }) => {
|
|
|
3160
3164
|
return (jsxRuntime.jsxs(react.Table.Row, { display: "flex", zIndex: 1, onMouseEnter: () => handleRowHover(index), onMouseLeave: () => handleRowHover(-1), ...getTrProps({ hoveredRow, index }), children: [showSelector && (jsxRuntime.jsx(TableRowSelector, { index: index, row: row, hoveredRow: hoveredRow })), row.getVisibleCells().map((cell, index) => {
|
|
3161
3165
|
return (jsxRuntime.jsx(react.Table.Cell, { padding: `${table.getDensityValue()}px`,
|
|
3162
3166
|
// styling resize and pinning start
|
|
3163
|
-
flex: `${canResize ? "0" : "1"} 0 ${cell.column.getSize()}px`,
|
|
3167
|
+
flex: `${canResize ? "0" : "1"} 0 ${cell.column.getSize()}px`,
|
|
3168
|
+
// this is to avoid the cell from being too wide
|
|
3169
|
+
minWidth: `0`, color: {
|
|
3164
3170
|
base: "colorPalette.900",
|
|
3165
3171
|
_dark: "colorPalette.100",
|
|
3166
3172
|
},
|
|
@@ -3686,6 +3692,7 @@ const SchemaFormContext = React.createContext({
|
|
|
3686
3692
|
onSubmit: async () => { },
|
|
3687
3693
|
rowNumber: 0,
|
|
3688
3694
|
requestOptions: {},
|
|
3695
|
+
validationLocale: 'en',
|
|
3689
3696
|
});
|
|
3690
3697
|
|
|
3691
3698
|
const useSchemaContext = () => {
|
|
@@ -3696,6 +3703,179 @@ const clearEmptyString = (object) => {
|
|
|
3696
3703
|
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ""));
|
|
3697
3704
|
};
|
|
3698
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
|
+
|
|
3699
3879
|
const idPickerSanityCheck = (column, foreign_key) => {
|
|
3700
3880
|
if (!!foreign_key == false) {
|
|
3701
3881
|
throw new Error(`The key foreign_key does not exist in properties of column ${column} when using id-picker.`);
|
|
@@ -3711,7 +3891,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
3711
3891
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
3712
3892
|
}
|
|
3713
3893
|
};
|
|
3714
|
-
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, }) => {
|
|
3894
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, validationLocale = 'en', }) => {
|
|
3715
3895
|
const [isSuccess, setIsSuccess] = React.useState(false);
|
|
3716
3896
|
const [isError, setIsError] = React.useState(false);
|
|
3717
3897
|
const [isSubmiting, setIsSubmiting] = React.useState(false);
|
|
@@ -3744,6 +3924,8 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
3744
3924
|
error,
|
|
3745
3925
|
setError,
|
|
3746
3926
|
getUpdatedData,
|
|
3927
|
+
customErrorRenderer,
|
|
3928
|
+
validationLocale,
|
|
3747
3929
|
}, children: jsxRuntime.jsx(reactHookForm.FormProvider, { ...form, children: children }) }));
|
|
3748
3930
|
};
|
|
3749
3931
|
|
|
@@ -4639,7 +4821,7 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
|
|
|
4639
4821
|
|
|
4640
4822
|
const NumberInputRoot = React__namespace.forwardRef(function NumberInput(props, ref) {
|
|
4641
4823
|
const { children, ...rest } = props;
|
|
4642
|
-
return (jsxRuntime.
|
|
4824
|
+
return (jsxRuntime.jsx(react.NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: children }));
|
|
4643
4825
|
});
|
|
4644
4826
|
const NumberInputField$1 = react.NumberInput.Input;
|
|
4645
4827
|
react.NumberInput.Scrubber;
|
|
@@ -5467,10 +5649,28 @@ const ColumnViewer = ({ column, properties, prefix, }) => {
|
|
|
5467
5649
|
};
|
|
5468
5650
|
|
|
5469
5651
|
const SubmitButton = () => {
|
|
5470
|
-
const { translate, setValidatedData, setIsError, setIsConfirming } = useSchemaContext();
|
|
5652
|
+
const { translate, setValidatedData, setIsError, setIsConfirming, setError, schema, validationLocale } = useSchemaContext();
|
|
5471
5653
|
const methods = reactHookForm.useFormContext();
|
|
5472
5654
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5473
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
|
|
5474
5674
|
setValidatedData(data);
|
|
5475
5675
|
setIsError(false);
|
|
5476
5676
|
setIsConfirming(true);
|
|
@@ -5481,7 +5681,7 @@ const SubmitButton = () => {
|
|
|
5481
5681
|
};
|
|
5482
5682
|
|
|
5483
5683
|
const FormBody = () => {
|
|
5484
|
-
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, } = 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();
|
|
5485
5685
|
const methods = reactHookForm.useFormContext();
|
|
5486
5686
|
const { properties } = schema;
|
|
5487
5687
|
const onBeforeSubmit = () => {
|
|
@@ -5497,6 +5697,27 @@ const FormBody = () => {
|
|
|
5497
5697
|
const onSubmitSuccess = () => {
|
|
5498
5698
|
setIsSuccess(true);
|
|
5499
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
|
+
};
|
|
5500
5721
|
const defaultOnSubmit = async (promise) => {
|
|
5501
5722
|
try {
|
|
5502
5723
|
onBeforeSubmit();
|
|
@@ -5521,12 +5742,47 @@ const FormBody = () => {
|
|
|
5521
5742
|
};
|
|
5522
5743
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5523
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
|
+
}
|
|
5524
5761
|
if (onSubmit === undefined) {
|
|
5525
5762
|
await defaultOnSubmit(defaultSubmitPromise(data));
|
|
5526
5763
|
return;
|
|
5527
5764
|
}
|
|
5528
5765
|
await defaultOnSubmit(onSubmit(data));
|
|
5529
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
|
+
};
|
|
5530
5786
|
const renderColumns = ({ order, keys, ignore, include, }) => {
|
|
5531
5787
|
const included = include.length > 0 ? include : keys;
|
|
5532
5788
|
const not_exist = included.filter((columnA) => !order.some((columnB) => columnA === columnB));
|
|
@@ -5562,7 +5818,7 @@ const FormBody = () => {
|
|
|
5562
5818
|
setIsConfirming(false);
|
|
5563
5819
|
}, variant: "subtle", children: translate.t("cancel") }), jsxRuntime.jsx(react.Button, { onClick: () => {
|
|
5564
5820
|
onFormSubmit(validatedData);
|
|
5565
|
-
}, 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: 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)}` })] }) }) }) })) })) }))] }));
|
|
5566
5822
|
}
|
|
5567
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) => {
|
|
5568
5824
|
return (jsxRuntime.jsx(ColumnRenderer
|
|
@@ -5572,7 +5828,7 @@ const FormBody = () => {
|
|
|
5572
5828
|
properties: properties, prefix: ``, column }, `form-input-${column}`));
|
|
5573
5829
|
}) }), jsxRuntime.jsxs(react.Flex, { justifyContent: "end", gap: "2", children: [jsxRuntime.jsx(react.Button, { onClick: () => {
|
|
5574
5830
|
methods.reset();
|
|
5575
|
-
}, 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) }))] }));
|
|
5576
5832
|
};
|
|
5577
5833
|
|
|
5578
5834
|
const FormTitle = () => {
|
|
@@ -5655,12 +5911,16 @@ exports.TableSorter = TableSorter;
|
|
|
5655
5911
|
exports.TableViewer = TableViewer;
|
|
5656
5912
|
exports.TextCell = TextCell;
|
|
5657
5913
|
exports.ViewDialog = ViewDialog;
|
|
5914
|
+
exports.createSchemaValidator = createSchemaValidator;
|
|
5658
5915
|
exports.getColumns = getColumns;
|
|
5659
5916
|
exports.getMultiDates = getMultiDates;
|
|
5660
5917
|
exports.getRangeDates = getRangeDates;
|
|
5918
|
+
exports.getSupportedLocales = getSupportedLocales;
|
|
5661
5919
|
exports.idPickerSanityCheck = idPickerSanityCheck;
|
|
5920
|
+
exports.isLocaleSupported = isLocaleSupported;
|
|
5662
5921
|
exports.useDataTable = useDataTable;
|
|
5663
5922
|
exports.useDataTableContext = useDataTableContext;
|
|
5664
5923
|
exports.useDataTableServer = useDataTableServer;
|
|
5665
5924
|
exports.useForm = useForm;
|
|
5925
|
+
exports.validateData = validateData;
|
|
5666
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';
|
|
@@ -3140,7 +3144,9 @@ const TableBody = ({ showSelector = false, canResize = true, }) => {
|
|
|
3140
3144
|
return (jsxs(Table$1.Row, { display: "flex", zIndex: 1, onMouseEnter: () => handleRowHover(index), onMouseLeave: () => handleRowHover(-1), ...getTrProps({ hoveredRow, index }), children: [showSelector && (jsx(TableRowSelector, { index: index, row: row, hoveredRow: hoveredRow })), row.getVisibleCells().map((cell, index) => {
|
|
3141
3145
|
return (jsx(Table$1.Cell, { padding: `${table.getDensityValue()}px`,
|
|
3142
3146
|
// styling resize and pinning start
|
|
3143
|
-
flex: `${canResize ? "0" : "1"} 0 ${cell.column.getSize()}px`,
|
|
3147
|
+
flex: `${canResize ? "0" : "1"} 0 ${cell.column.getSize()}px`,
|
|
3148
|
+
// this is to avoid the cell from being too wide
|
|
3149
|
+
minWidth: `0`, color: {
|
|
3144
3150
|
base: "colorPalette.900",
|
|
3145
3151
|
_dark: "colorPalette.100",
|
|
3146
3152
|
},
|
|
@@ -3666,6 +3672,7 @@ const SchemaFormContext = createContext({
|
|
|
3666
3672
|
onSubmit: async () => { },
|
|
3667
3673
|
rowNumber: 0,
|
|
3668
3674
|
requestOptions: {},
|
|
3675
|
+
validationLocale: 'en',
|
|
3669
3676
|
});
|
|
3670
3677
|
|
|
3671
3678
|
const useSchemaContext = () => {
|
|
@@ -3676,6 +3683,179 @@ const clearEmptyString = (object) => {
|
|
|
3676
3683
|
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ""));
|
|
3677
3684
|
};
|
|
3678
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
|
+
|
|
3679
3859
|
const idPickerSanityCheck = (column, foreign_key) => {
|
|
3680
3860
|
if (!!foreign_key == false) {
|
|
3681
3861
|
throw new Error(`The key foreign_key does not exist in properties of column ${column} when using id-picker.`);
|
|
@@ -3691,7 +3871,7 @@ const idPickerSanityCheck = (column, foreign_key) => {
|
|
|
3691
3871
|
throw new Error(`The key column does not exist in properties of column ${column} when using id-picker.`);
|
|
3692
3872
|
}
|
|
3693
3873
|
};
|
|
3694
|
-
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, }) => {
|
|
3874
|
+
const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, children, order = [], ignore = [], include = [], onSubmit = undefined, rowNumber = undefined, requestOptions = {}, getUpdatedData = () => { }, customErrorRenderer, validationLocale = 'en', }) => {
|
|
3695
3875
|
const [isSuccess, setIsSuccess] = useState(false);
|
|
3696
3876
|
const [isError, setIsError] = useState(false);
|
|
3697
3877
|
const [isSubmiting, setIsSubmiting] = useState(false);
|
|
@@ -3724,6 +3904,8 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
|
|
|
3724
3904
|
error,
|
|
3725
3905
|
setError,
|
|
3726
3906
|
getUpdatedData,
|
|
3907
|
+
customErrorRenderer,
|
|
3908
|
+
validationLocale,
|
|
3727
3909
|
}, children: jsx(FormProvider, { ...form, children: children }) }));
|
|
3728
3910
|
};
|
|
3729
3911
|
|
|
@@ -4619,7 +4801,7 @@ const IdPicker = ({ column, schema, prefix, isMultiple = false, }) => {
|
|
|
4619
4801
|
|
|
4620
4802
|
const NumberInputRoot = React.forwardRef(function NumberInput$1(props, ref) {
|
|
4621
4803
|
const { children, ...rest } = props;
|
|
4622
|
-
return (
|
|
4804
|
+
return (jsx(NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: children }));
|
|
4623
4805
|
});
|
|
4624
4806
|
const NumberInputField$1 = NumberInput.Input;
|
|
4625
4807
|
NumberInput.Scrubber;
|
|
@@ -5447,10 +5629,28 @@ const ColumnViewer = ({ column, properties, prefix, }) => {
|
|
|
5447
5629
|
};
|
|
5448
5630
|
|
|
5449
5631
|
const SubmitButton = () => {
|
|
5450
|
-
const { translate, setValidatedData, setIsError, setIsConfirming } = useSchemaContext();
|
|
5632
|
+
const { translate, setValidatedData, setIsError, setIsConfirming, setError, schema, validationLocale } = useSchemaContext();
|
|
5451
5633
|
const methods = useFormContext();
|
|
5452
5634
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5453
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
|
|
5454
5654
|
setValidatedData(data);
|
|
5455
5655
|
setIsError(false);
|
|
5456
5656
|
setIsConfirming(true);
|
|
@@ -5461,7 +5661,7 @@ const SubmitButton = () => {
|
|
|
5461
5661
|
};
|
|
5462
5662
|
|
|
5463
5663
|
const FormBody = () => {
|
|
5464
|
-
const { schema, requestUrl, order, ignore, include, onSubmit, rowNumber, translate, requestOptions, isSuccess, setIsSuccess, isError, setIsError, isSubmiting, setIsSubmiting, isConfirming, setIsConfirming, validatedData, setValidatedData, error, setError, getUpdatedData, } = 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();
|
|
5465
5665
|
const methods = useFormContext();
|
|
5466
5666
|
const { properties } = schema;
|
|
5467
5667
|
const onBeforeSubmit = () => {
|
|
@@ -5477,6 +5677,27 @@ const FormBody = () => {
|
|
|
5477
5677
|
const onSubmitSuccess = () => {
|
|
5478
5678
|
setIsSuccess(true);
|
|
5479
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
|
+
};
|
|
5480
5701
|
const defaultOnSubmit = async (promise) => {
|
|
5481
5702
|
try {
|
|
5482
5703
|
onBeforeSubmit();
|
|
@@ -5501,12 +5722,47 @@ const FormBody = () => {
|
|
|
5501
5722
|
};
|
|
5502
5723
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5503
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
|
+
}
|
|
5504
5741
|
if (onSubmit === undefined) {
|
|
5505
5742
|
await defaultOnSubmit(defaultSubmitPromise(data));
|
|
5506
5743
|
return;
|
|
5507
5744
|
}
|
|
5508
5745
|
await defaultOnSubmit(onSubmit(data));
|
|
5509
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
|
+
};
|
|
5510
5766
|
const renderColumns = ({ order, keys, ignore, include, }) => {
|
|
5511
5767
|
const included = include.length > 0 ? include : keys;
|
|
5512
5768
|
const not_exist = included.filter((columnA) => !order.some((columnB) => columnA === columnB));
|
|
@@ -5542,7 +5798,7 @@ const FormBody = () => {
|
|
|
5542
5798
|
setIsConfirming(false);
|
|
5543
5799
|
}, variant: "subtle", children: translate.t("cancel") }), jsx(Button$1, { onClick: () => {
|
|
5544
5800
|
onFormSubmit(validatedData);
|
|
5545
|
-
}, 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: 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)}` })] }) }) }) })) })) }))] }));
|
|
5546
5802
|
}
|
|
5547
5803
|
return (jsxs(Flex, { flexFlow: "column", gap: "2", children: [jsx(Grid, { gap: "4", gridTemplateColumns: "repeat(12, 1fr)", autoFlow: "row", children: ordered.map((column) => {
|
|
5548
5804
|
return (jsx(ColumnRenderer
|
|
@@ -5552,7 +5808,7 @@ const FormBody = () => {
|
|
|
5552
5808
|
properties: properties, prefix: ``, column }, `form-input-${column}`));
|
|
5553
5809
|
}) }), jsxs(Flex, { justifyContent: "end", gap: "2", children: [jsx(Button$1, { onClick: () => {
|
|
5554
5810
|
methods.reset();
|
|
5555
|
-
}, 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) }))] }));
|
|
5556
5812
|
};
|
|
5557
5813
|
|
|
5558
5814
|
const FormTitle = () => {
|
|
@@ -5594,4 +5850,4 @@ const getMultiDates = ({ selected, selectedDate, selectedDates, selectable, }) =
|
|
|
5594
5850
|
}
|
|
5595
5851
|
};
|
|
5596
5852
|
|
|
5597
|
-
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 };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AxiosRequestConfig } from "axios";
|
|
2
2
|
import { JSONSchema7 } from "json-schema";
|
|
3
|
-
import { Dispatch, SetStateAction } from "react";
|
|
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;
|
|
@@ -29,5 +30,7 @@ export interface SchemaFormContext<TData extends FieldValues> {
|
|
|
29
30
|
error: unknown;
|
|
30
31
|
setError: Dispatch<SetStateAction<unknown>>;
|
|
31
32
|
getUpdatedData: () => TData | Promise<TData>;
|
|
33
|
+
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
34
|
+
validationLocale?: SupportedLocale;
|
|
32
35
|
}
|
|
33
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";
|
|
@@ -21,6 +22,8 @@ export interface FormRootProps<TData extends FieldValues> {
|
|
|
21
22
|
rowNumber?: number | string;
|
|
22
23
|
requestOptions?: AxiosRequestConfig;
|
|
23
24
|
getUpdatedData?: () => TData | Promise<TData> | void;
|
|
25
|
+
customErrorRenderer?: (error: unknown) => ReactNode;
|
|
26
|
+
validationLocale?: SupportedLocale;
|
|
24
27
|
}
|
|
25
28
|
export interface CustomJSONSchema7Definition extends JSONSchema7 {
|
|
26
29
|
variant: string;
|
|
@@ -37,4 +40,4 @@ export declare const idPickerSanityCheck: (column: string, foreign_key?: {
|
|
|
37
40
|
column?: string | undefined;
|
|
38
41
|
display_column?: string | undefined;
|
|
39
42
|
} | undefined) => void;
|
|
40
|
-
export declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, }: 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",
|