@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 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`, minWidth: `0`, color: {
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.jsxs(react.NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: [children, jsxRuntime.jsxs(react.NumberInput.Control, { children: [jsxRuntime.jsx(react.NumberInput.IncrementTrigger, {}), jsxRuntime.jsx(react.NumberInput.DecrementTrigger, {})] })] }));
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`, minWidth: `0`, color: {
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 (jsxs(NumberInput.Root, { ref: ref, variant: "outline", ...rest, children: [children, jsxs(NumberInput.Control, { children: [jsx(NumberInput.IncrementTrigger, {}), jsx(NumberInput.DecrementTrigger, {})] })] }));
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 {};
@@ -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.55",
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",