form-craft-package 1.0.0

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.
Files changed (40) hide show
  1. package/.prettierrc +9 -0
  2. package/AJV_JSON_Schema_Guide.md +409 -0
  3. package/README.md +108 -0
  4. package/index.ts +8 -0
  5. package/package.json +40 -0
  6. package/src/ajv/form/form.schema.json +10 -0
  7. package/src/ajv/form/layout.schema.json +97 -0
  8. package/src/ajv/form/migration-rules.schema.json +59 -0
  9. package/src/ajv/master-portal-only/render-conditions/conditions.schema.json +24 -0
  10. package/src/ajv/master-portal-only/render-conditions/validate.ts +15 -0
  11. package/src/components/common/button.tsx +72 -0
  12. package/src/components/common/custom-hooks/use-find-dynamic-form.ts +33 -0
  13. package/src/components/common/custom-hooks/use-lazy-modal-opener.hook.ts +20 -0
  14. package/src/components/common/custom-hooks/use-notification.hook.tsx +157 -0
  15. package/src/components/common/disabled-field-indicator.tsx +20 -0
  16. package/src/components/common/warning-icon.tsx +10 -0
  17. package/src/components/form/layout-renderer/1-row/index.tsx +27 -0
  18. package/src/components/form/layout-renderer/2-col/index.tsx +32 -0
  19. package/src/components/form/layout-renderer/3-element/1-dynamic-button.tsx +277 -0
  20. package/src/components/form/layout-renderer/3-element/2-field-element.tsx +220 -0
  21. package/src/components/form/layout-renderer/3-element/index.tsx +73 -0
  22. package/src/components/index.tsx +2 -0
  23. package/src/components/modals/form-data-loading.modal.tsx +48 -0
  24. package/src/constants.ts +15 -0
  25. package/src/enums.ts +177 -0
  26. package/src/functions/axios-handler.ts +158 -0
  27. package/src/functions/data-list-functions.tsx +41 -0
  28. package/src/functions/form-schema-validator.ts +50 -0
  29. package/src/functions/get-element-props.ts +20 -0
  30. package/src/functions/index.ts +56 -0
  31. package/src/functions/json-handlers.ts +19 -0
  32. package/src/functions/validations.ts +120 -0
  33. package/src/types/form-data-list/index.ts +54 -0
  34. package/src/types/index.ts +124 -0
  35. package/src/types/layout-elements/element-data-render-logic.ts +56 -0
  36. package/src/types/layout-elements/field-option-source.ts +14 -0
  37. package/src/types/layout-elements/index.ts +224 -0
  38. package/src/types/layout-elements/style.ts +35 -0
  39. package/src/types/layout-elements/validation.ts +18 -0
  40. package/tsconfig.json +111 -0
package/src/enums.ts ADDED
@@ -0,0 +1,177 @@
1
+ export enum ElementTypeEnum {
2
+ ShortInput = 'Short Input',
3
+ LongInput = 'Long Input',
4
+ Search = 'Search',
5
+ NumberInput = 'Number Input',
6
+ Select = 'Select',
7
+ Radio = 'Radio',
8
+ Checkbox = 'Checkbox',
9
+ DatePicker = 'Date Picker',
10
+ TimePicker = 'Time Picker',
11
+ FileUpload = 'File Upload',
12
+ Switch = 'Switch',
13
+ Slider = 'Slider',
14
+ Password = 'Password',
15
+ Container = 'Container',
16
+ RichTextEditor = 'Rich Text Editor',
17
+ Table = 'Table',
18
+ ReadOnly = 'Read Only',
19
+ Divider = 'Divider',
20
+ Button = 'Button',
21
+ TitlePlacement = 'Title Placement',
22
+ Signature = 'Signature',
23
+ Title = 'Title',
24
+ ReCaptcha = 'reCAPTCHA',
25
+ }
26
+ export enum MigrationReasonOperationEnum {
27
+ RenameField = 'rename_field',
28
+ CreateRepeatingSection = 'create_repeating_section',
29
+ // RemoveField = 'remove_field',
30
+ // ConvertDataType = 'convert_data_type',
31
+ // MakeRequired = 'make_required',
32
+ // AddRequiredField = 'add_required_field',
33
+ // ValidationRuleChange = 'validation_rule_change',
34
+ // UpdateDefaultValue = 'update_default_value',
35
+ // ModifyStructure = 'modify_structure',
36
+ // ConditionalLogicUpdate = 'conditional_logic_update',
37
+ }
38
+ export enum FieldValidationEnum {
39
+ Required = 'Required',
40
+ DataLength = 'DataLength',
41
+ MaxDataLength = 'MaxDataLength',
42
+ MinDataLength = 'MinDataLength',
43
+ MinValue = 'MinValue',
44
+ MaxValue = 'MaxValue',
45
+ Regex = 'Regex',
46
+ Email = 'Email',
47
+ PhoneNumber_US = 'PhoneNumber_US',
48
+ PhoneNumber_MN = 'PhoneNumber_MN',
49
+ }
50
+ export enum FormLayoutTypeEnum {
51
+ Main = 'main',
52
+ Sidebar = 'sidebar',
53
+ }
54
+ export enum FormLayoutNodeEnum { // when saved, everything in the layout will have either of these node types
55
+ Element = 'Element', // it's the actual display of the form (can be a field, a divider etc.)
56
+ Row = 'Row',
57
+ Col = 'Col',
58
+ }
59
+ export enum FormLayoutAddableNodeEnum { // used on SuperAdmin portal only during the form crafting
60
+ Template = 'Template',
61
+ NewElement = 'NewElement',
62
+ Container = 'Container',
63
+ }
64
+ export enum TooltipPlacementEnum {
65
+ Top = 'top',
66
+ Left = 'left',
67
+ Right = 'right',
68
+ Bottom = 'bottom',
69
+ TopLeft = 'topLeft',
70
+ TopRight = 'topRight',
71
+ BottomLeft = 'bottomLeft',
72
+ BottomRight = 'bottomRight',
73
+ LeftTop = 'leftTop',
74
+ LeftBottom = 'leftBottom',
75
+ RightTop = 'rightTop',
76
+ RightBottom = 'rightBottom',
77
+ }
78
+ export enum TooltipTriggerEnum {
79
+ Hover = 'hover',
80
+ Click = 'click',
81
+ }
82
+ export enum AlignTypeEnum {
83
+ Left = 'left',
84
+ Center = 'center',
85
+ Right = 'right',
86
+ }
87
+ export enum DataRenderTypeEnum {
88
+ // string enum for readability
89
+ Default = 'default',
90
+ Date = 'date',
91
+ Conditional = 'conditional',
92
+ Buttons = 'buttons',
93
+ }
94
+ export enum ButtonElementTypeEnum {
95
+ Primary = 'primary',
96
+ PrimaryOutlined = 'primary-outline',
97
+ Secondary = 'secondary',
98
+ SecondaryOutlined = 'secondary-outline',
99
+ Danger = 'danger',
100
+ DangerOutlined = 'danger-outline',
101
+ Link = 'link',
102
+ }
103
+ export enum ButtonElementSizeEnum {
104
+ Default = 'default',
105
+ Small = 'small',
106
+ Large = 'large',
107
+ }
108
+ export enum FieldElementOptionSourceEnum {
109
+ ManualCreation = 'ManualCreation',
110
+ FromOtherForms = 'FromOtherForms',
111
+ }
112
+ export enum FormDataListViewTypeEnum {
113
+ Table = 'table',
114
+ }
115
+ export enum ButtonActionCategoryEnum {
116
+ // those are predefined actions that can be completed with formId and formDataId
117
+ CreateNewData = 'create-new-data',
118
+ ViewDataDetails = 'view-data-details',
119
+ DeleteData = 'delete-data',
120
+ DuplicateData = 'duplicate-data',
121
+ SaveDataChanges = 'save-data-changes',
122
+ PublishDataChanges = 'publish-data-changes',
123
+ ReturnToDataList = 'return-to-data-list',
124
+ ViewDataVersions = 'view-data-versions',
125
+ CustomFunction = 'custom-function',
126
+ }
127
+ export enum CSSLayoutType {
128
+ Flex = 'flex',
129
+ }
130
+ export enum FlexDirection {
131
+ Row = 'row',
132
+ Col = 'column',
133
+ RowReverse = 'row-reverse',
134
+ ColReverse = 'column-reverse',
135
+ }
136
+ export enum FlexWrap {
137
+ Wrap = 'wrap',
138
+ NoWrap = 'nowrap',
139
+ }
140
+ export enum FlexJustifyContent {
141
+ Center = 'center',
142
+ Start = 'flex-start',
143
+ End = 'flex-end',
144
+ SpaceAround = 'space-around',
145
+ SpaceBetween = 'space-between',
146
+ Stretch = 'stretch',
147
+ }
148
+ export enum FlexAlignItems {
149
+ Center = 'center',
150
+ Start = 'flex-start',
151
+ End = 'flex-end',
152
+ Stretch = 'stretch',
153
+ Baseline = 'baseline',
154
+ }
155
+ export enum FilterConfigTypeEnum {
156
+ NoFilter = 'NoFilter',
157
+ ByMe = 'ByMe',
158
+ Custom = 'Custom',
159
+ }
160
+ export enum TitleElementTypeEnum {
161
+ Plain,
162
+ Rich,
163
+ }
164
+ export enum ResponsivenessDeviceEnum {
165
+ Default = 'default',
166
+ Laptop = 'lg',
167
+ Tablet = 'md',
168
+ Mobile = 'xs',
169
+ }
170
+ export enum FormLoadingModalTypeEnum {
171
+ SavingChanges,
172
+ ErrorOccured,
173
+ }
174
+ export enum TOKEN_COOKIE_KEYS {
175
+ AccessToken = 'd9508def54ed42b258db56c92155662f',
176
+ RefreshToken = 'd293a508f5409447b3bb63171e53e8c4',
177
+ }
@@ -0,0 +1,158 @@
1
+ import Cookies from 'js-cookie'
2
+ import axios from 'axios'
3
+
4
+ import { notification } from 'antd'
5
+ import { TOKEN_COOKIE_KEYS } from '../enums'
6
+
7
+ let baseURL = 'https://bark-backend-form.azurewebsites.net'
8
+
9
+ const getCookieDeadline = (expiresIn: number): Date => {
10
+ const now = new Date()
11
+ return new Date(now.getTime() + expiresIn * 1000)
12
+ }
13
+
14
+ const authResHandler = (resData: any) => {
15
+ const expires = getCookieDeadline(resData.expires_in)
16
+ const cookieSettings: { expires?: Date; path: string; domain?: string } = {
17
+ expires,
18
+ path: '/',
19
+ }
20
+
21
+ Cookies.set(TOKEN_COOKIE_KEYS.AccessToken, resData.access_token, cookieSettings)
22
+ Cookies.set(TOKEN_COOKIE_KEYS.RefreshToken, resData.refresh_token)
23
+ }
24
+
25
+ /** --------------- Access Token Interceptor --------------- */
26
+
27
+ let accessToken = Cookies.get(TOKEN_COOKIE_KEYS.AccessToken)
28
+ const client = axios.create({
29
+ baseURL,
30
+ headers: { Authorization: `Bearer ${accessToken}` },
31
+ })
32
+
33
+ client.interceptors.request.use((req) => {
34
+ accessToken = Cookies.get(TOKEN_COOKIE_KEYS.AccessToken)
35
+ if (req.headers) req.headers.Authorization = `Bearer ${accessToken}`
36
+ return req
37
+ })
38
+
39
+ /** --------------- Refresh Token --------------- */
40
+
41
+ const is401 = (error: any) => {
42
+ try {
43
+ return error.response.status === 401
44
+ } catch (e) {
45
+ return false
46
+ }
47
+ }
48
+
49
+ const handleTokenRefresh = () => {
50
+ const refreshToken = Cookies.get(TOKEN_COOKIE_KEYS.RefreshToken)
51
+ if (!refreshToken) {
52
+ notification.destroy()
53
+ window.location.href = '/login'
54
+ }
55
+ return new Promise((resolve, reject) => {
56
+ axios
57
+ .post(`${baseURL}/connect/token/refresh`, { refreshToken })
58
+ .then(({ data }) => {
59
+ resolve({
60
+ accessToken: data.accessToken,
61
+ refreshToken: data.refreshToken,
62
+ expiresIn: data.expiresIn,
63
+ })
64
+ })
65
+ .catch((err) => {
66
+ reject(err)
67
+ })
68
+ })
69
+ }
70
+
71
+ const customAxiosInterceptor = (axiosClient: any) => {
72
+ let isRefreshing = false
73
+ let failedQueue: any = []
74
+
75
+ const helperFunctions = {
76
+ handleTokenRefresh,
77
+ is401,
78
+ }
79
+
80
+ const processQueue = (error: any, token = null) => {
81
+ failedQueue.forEach((req: any) => {
82
+ if (error) req.reject(error)
83
+ else req.resolve(token)
84
+ })
85
+
86
+ failedQueue = []
87
+ }
88
+
89
+ const interceptor = (error: any) => {
90
+ if (!helperFunctions.is401(error)) return Promise.reject(error)
91
+
92
+ if (error.config._retry || error.config._queued) return Promise.reject(error)
93
+
94
+ const originalRequest = error.config
95
+ if (isRefreshing)
96
+ return new Promise((resolve, reject) => failedQueue.push({ resolve, reject }))
97
+ .then((accessToken) => {
98
+ originalRequest._queued = true
99
+ originalRequest.headers['Authorization'] = `Bearer ${accessToken}`
100
+ return axiosClient.request(originalRequest)
101
+ })
102
+ .catch(() => Promise.reject(error))
103
+
104
+ originalRequest._retry = true
105
+ isRefreshing = true
106
+
107
+ return new Promise((resolve, reject) => {
108
+ helperFunctions.handleTokenRefresh
109
+ .call(helperFunctions.handleTokenRefresh)
110
+ .then((newTokenResData: any) => {
111
+ authResHandler(newTokenResData)
112
+ originalRequest.headers['Authorization'] = `Bearer ${newTokenResData.accessToken}`
113
+
114
+ processQueue(null, newTokenResData.accessToken)
115
+ resolve(axiosClient.request(originalRequest))
116
+ })
117
+ .catch((err) => {
118
+ processQueue(err, null)
119
+ reject(err)
120
+ })
121
+ .finally(() => (isRefreshing = false))
122
+ })
123
+ }
124
+
125
+ axiosClient.interceptors.response.use(undefined, interceptor)
126
+ }
127
+
128
+ client.interceptors.response.use(
129
+ (res) => res,
130
+ (error) => {
131
+ if (error.response?.status === 401) customAxiosInterceptor(client)
132
+ else displayNotification(error)
133
+ return Promise.reject(error)
134
+ },
135
+ )
136
+
137
+ const displayNotification = (error: any) => {
138
+ if (error && error.response) {
139
+ let description = `${error.response.path} - ${error.response.status}`
140
+ let message = error.response.data
141
+ if (error.response.status === 503) message = 'Deployment is in progress!'
142
+ else if (error.response.status === 500) message = 'Server or BE error!'
143
+
144
+ console.error(description)
145
+
146
+ const baseNotif = { message, duration: 4, onClick: () => notification.destroy() }
147
+ notification.destroy()
148
+
149
+ notification.error({ ...baseNotif })
150
+
151
+ console.error(
152
+ `Request failed: ${error.response.status} ${error.response.statusText} ${error.response.data} ${error.response.config?.url}`,
153
+ )
154
+ }
155
+ }
156
+
157
+ export default client
158
+ export { authResHandler }
@@ -0,0 +1,41 @@
1
+ import dayjs from 'dayjs'
2
+ import { AlignTypeEnum, DataRenderTypeEnum } from '../enums'
3
+ import { IDataRender_Buttons, IDataRender_Date, IFormData, IFormDataListElement, ITableColumn } from '../types'
4
+ import { DynamicFormButtonRender } from '../components'
5
+
6
+ export const generateTableColumns = (elements: IFormDataListElement[]) => {
7
+ return elements.map((el) => {
8
+ const col: ITableColumn = {
9
+ ...el,
10
+ title: el.label,
11
+ dataIndex: el.key ?? '',
12
+ sorter: el.sortable,
13
+ }
14
+ if (el.renderConfig) {
15
+ if (el.renderConfig.type === DataRenderTypeEnum.Date) {
16
+ const { format = '' } = (el.renderConfig as IDataRender_Date) ?? {}
17
+ col.render = (data: string) => dayjs(data).format(format)
18
+ } else if (el.renderConfig.type === DataRenderTypeEnum.Buttons) {
19
+ col.render = (rowData: IFormData) => {
20
+ return (
21
+ <div
22
+ className={`flex items-center gap-2 ${
23
+ el.align === AlignTypeEnum.Center
24
+ ? 'justify-center'
25
+ : el.align === AlignTypeEnum.Right
26
+ ? 'justify-end'
27
+ : ''
28
+ }`}
29
+ >
30
+ {(el.renderConfig as IDataRender_Buttons).buttons.map((btnProps) => (
31
+ <DynamicFormButtonRender btnProps={btnProps} formDataId={rowData.id} />
32
+ ))}
33
+ </div>
34
+ )
35
+ }
36
+ }
37
+ }
38
+
39
+ return col
40
+ })
41
+ }
@@ -0,0 +1,50 @@
1
+ import { IFormSchema } from '../types'
2
+ import Ajv, { ErrorObject, Options } from 'ajv'
3
+
4
+ import formSchema from '../ajv/form/form.schema.json'
5
+ import layoutSchema from '../ajv/form/layout.schema.json'
6
+ import migrationRulesSchema from '../ajv/form/migration-rules.schema.json'
7
+
8
+ export const validateFormJsonSchema = (data: IFormSchema) => {
9
+ const ajvOptions: Options = {
10
+ allErrors: true,
11
+ logger: false,
12
+ verbose: true,
13
+ }
14
+
15
+ const ajv = new Ajv(ajvOptions)
16
+
17
+ ajv.addSchema(layoutSchema, layoutSchema.$id)
18
+ ajv.addSchema(migrationRulesSchema, migrationRulesSchema.$id)
19
+ ajv.addSchema(formSchema, formSchema.$id)
20
+
21
+ const validate = ajv.compile(formSchema)
22
+
23
+ // const isSchemaValid = ajv.validateSchema(formSchema)
24
+
25
+ const isValid = validate(data)
26
+
27
+ let sortedErrors: ErrorObject[] = []
28
+ if (!isValid) {
29
+ const errors: ErrorObject[] = validate.errors || []
30
+ sortedErrors = errors.sort((a: ErrorObject, b: ErrorObject) => {
31
+ const getErrorPriority = (error: ErrorObject): number => {
32
+ const standardKeywords = ['minLength', 'required', 'type', 'minimum', 'maximum', 'pattern', 'minItems']
33
+ if (standardKeywords.includes(error.keyword)) return 1
34
+
35
+ const schemaKeywords = ['oneOf', 'allOf', 'anyOf', 'not']
36
+ if (schemaKeywords.includes(error.keyword)) return 2
37
+
38
+ return 3
39
+ }
40
+
41
+ const priorityDiff = getErrorPriority(a) - getErrorPriority(b)
42
+ if (priorityDiff !== 0) return priorityDiff
43
+
44
+ // If in the same group, sort by schemaPath length (descending)
45
+ return b.schemaPath.length - a.schemaPath.length
46
+ })
47
+ }
48
+
49
+ return { isValid, errors: sortedErrors }
50
+ }
@@ -0,0 +1,20 @@
1
+ import { ButtonElementSizeEnum, ButtonElementTypeEnum } from '../enums'
2
+ import { IDataRender_ButtonProps } from '../types'
3
+
4
+ // since all the field elements have different type, this function is generalizing the types
5
+ export const getElementGeneralizedProps = (props: Record<string, any> | undefined): Record<string, any> => ({
6
+ ...props,
7
+ })
8
+
9
+ export const getButtonRenderProps = (btnProps: IDataRender_ButtonProps) => ({
10
+ primary: [ButtonElementTypeEnum.Primary, ButtonElementTypeEnum.PrimaryOutlined].includes(btnProps.buttonType!),
11
+ secondary: [ButtonElementTypeEnum.Secondary, ButtonElementTypeEnum.SecondaryOutlined].includes(btnProps.buttonType!),
12
+ danger: [ButtonElementTypeEnum.Danger, ButtonElementTypeEnum.DangerOutlined].includes(btnProps.buttonType!),
13
+ short: btnProps.size === ButtonElementSizeEnum.Small,
14
+ link: btnProps.buttonType === ButtonElementTypeEnum.Link,
15
+ outline: [
16
+ ButtonElementTypeEnum.PrimaryOutlined,
17
+ ButtonElementTypeEnum.SecondaryOutlined,
18
+ ButtonElementTypeEnum.DangerOutlined,
19
+ ].includes(btnProps.buttonType!),
20
+ })
@@ -0,0 +1,56 @@
1
+ import { DEFAULT_FLEX_CONFIG } from '../constants'
2
+ import { ElementTypeEnum, FieldElementOptionSourceEnum, FormLayoutNodeEnum, ResponsivenessDeviceEnum } from '../enums'
3
+ import { IFilterNested, IFormLayoutCol, IFormLayoutElement, IFormLayoutRow, IFlexConfig } from '../types'
4
+ export * from './validations'
5
+ export * from './data-list-functions'
6
+
7
+ export const extractFiltersFromLayout = (layout: IFormLayoutRow[]) => {
8
+ const filters: IFilterNested = {}
9
+
10
+ const findFilters = (
11
+ node: IFormLayoutRow | IFormLayoutCol | IFormLayoutElement,
12
+ ): IFormLayoutRow | IFormLayoutCol | IFormLayoutElement => {
13
+ if (node.nodeType === FormLayoutNodeEnum.Row) node.children.forEach((child) => findFilters(child) as IFormLayoutCol)
14
+ else if (node.nodeType === FormLayoutNodeEnum.Col)
15
+ node.children.forEach((child) => findFilters(child) as IFormLayoutRow | IFormLayoutElement)
16
+
17
+ if (node.nodeType === FormLayoutNodeEnum.Element) {
18
+ if (
19
+ node.elementType === ElementTypeEnum.Radio &&
20
+ node.props.optionSource?.type === FieldElementOptionSourceEnum.ManualCreation
21
+ ) {
22
+ if (node.key)
23
+ node.props.optionSource.options.forEach((op) => {
24
+ if (op.filter)
25
+ filters[node.key] = filters[node.key]
26
+ ? { ...filters[node.key], [op.value]: op.filter }
27
+ : { [op.value]: op.filter }
28
+ })
29
+ else console.warn('Element key was not found!')
30
+ } else if (node.props && 'filter' in node.props && node.props.filter) {
31
+ if (node.key) filters[node.key] = node.props.filter
32
+ else console.warn('Element key was not found!')
33
+ }
34
+ }
35
+ return { ...node, nodeType: FormLayoutNodeEnum.Element } as IFormLayoutElement
36
+ }
37
+
38
+ layout.forEach((row) => findFilters(row) as IFormLayoutRow)
39
+ return filters
40
+ }
41
+
42
+ /** --------------------------------------------------------------------------------------------------------- */
43
+
44
+ export const getFlexContainerStyle = (device: ResponsivenessDeviceEnum, responsiveness?: IFlexConfig) =>
45
+ responsiveness ? responsiveness[device]?.container ?? DEFAULT_FLEX_CONFIG.Container : DEFAULT_FLEX_CONFIG.Container
46
+
47
+ export const getFlexItemStyle = (device: ResponsivenessDeviceEnum, itemIdx: number, responsiveness?: IFlexConfig) =>
48
+ responsiveness ? responsiveness[device]?.items[itemIdx] ?? DEFAULT_FLEX_CONFIG.Item : DEFAULT_FLEX_CONFIG.Item
49
+
50
+ /** --------------------------------------------------------------------------------------------------------- */
51
+
52
+ export const constructDynamicFormHref = (name: string) => '/d/' + name.split(' ').join('-').toLowerCase()
53
+
54
+ /** --------------------------------------------------------------------------------------------------------- */
55
+
56
+ /** --------------------------------------------------------------------------------------------------------- */
@@ -0,0 +1,19 @@
1
+ // TODO: add json validator
2
+ // TODO: pass the T and validate with it?
3
+
4
+ export const parseJSON = <T extends any>(jsonString: string): T | null => {
5
+ try {
6
+ return JSON.parse(jsonString) as T
7
+ } catch (error) {
8
+ console.error('Invalid JSON string:', error)
9
+ return null
10
+ }
11
+ }
12
+ export const stringifyJSON = (jsonObject: any): string => {
13
+ try {
14
+ return JSON.stringify(jsonObject)
15
+ } catch (error) {
16
+ console.error('Unable to stringify JSON:', error)
17
+ return ''
18
+ }
19
+ }
@@ -0,0 +1,120 @@
1
+ import { FieldValidationEnum } from '../enums'
2
+ export * from '../types/layout-elements/validation'
3
+ import { FormItemRule, IValidationRule } from '../types/layout-elements/validation'
4
+
5
+ /**
6
+ * Maps an array of validation rules into Form.Item-compatible rules.
7
+ *
8
+ * Each validation rule in the input array is transformed based on its `key` and `value`.
9
+ * Default messages are used if no message is provided for a rule.
10
+ *
11
+ * @param rules - Array of validation rules to be transformed.
12
+ * @returns Array of structured Form.Item-compatible rules.
13
+ */
14
+ export const mapToFormItemRules = (rules: IValidationRule[]): FormItemRule[] => {
15
+ const errors: string[] = []
16
+
17
+ const formItemRules = rules.reduce<FormItemRule[]>((acc, rule) => {
18
+ if ([FieldValidationEnum.MaxValue, FieldValidationEnum.MinValue].includes(rule.key)) return acc
19
+
20
+ const defaultMessages: Partial<Record<FieldValidationEnum, string>> = {
21
+ [FieldValidationEnum.Required]: 'This field is required',
22
+ [FieldValidationEnum.DataLength]: 'Length does not match',
23
+ [FieldValidationEnum.MaxDataLength]: 'Max length exceeded',
24
+ [FieldValidationEnum.MinDataLength]: 'Does not meet min length',
25
+ [FieldValidationEnum.Regex]: 'Invalid format',
26
+ [FieldValidationEnum.Email]: 'Invalid email address',
27
+ [FieldValidationEnum.PhoneNumber_US]: 'Invalid phone number',
28
+ [FieldValidationEnum.PhoneNumber_MN]: 'Invalid phone number',
29
+ }
30
+
31
+ const message = rule.message || defaultMessages[rule.key] || ''
32
+ let formItemRule: FormItemRule | null = null
33
+
34
+ switch (rule.key) {
35
+ case FieldValidationEnum.Required:
36
+ formItemRule = handleRequiredRule(message)
37
+ break
38
+ case FieldValidationEnum.MaxDataLength:
39
+ formItemRule = handleMaxLengthRule(rule.value, message)
40
+ break
41
+ case FieldValidationEnum.MinDataLength:
42
+ formItemRule = handleMinLengthRule(rule.value, message)
43
+ break
44
+ case FieldValidationEnum.Regex:
45
+ formItemRule = handleRegexRule(rule.value, message)
46
+ break
47
+ case FieldValidationEnum.Email:
48
+ formItemRule = handleEmailRule(message)
49
+ break
50
+ case FieldValidationEnum.PhoneNumber_US:
51
+ formItemRule = handlePhoneNumberRule(message, PhoneNumberCountry.US)
52
+ case FieldValidationEnum.PhoneNumber_MN:
53
+ formItemRule = handlePhoneNumberRule(message, PhoneNumberCountry.MN)
54
+ break
55
+ default:
56
+ errors.push(`Unsupported validation type: ${rule.key}`)
57
+ break
58
+ }
59
+
60
+ if (formItemRule) acc.push(formItemRule)
61
+
62
+ return acc
63
+ }, [])
64
+
65
+ if (errors.length > 0) {
66
+ console.error('Validation Errors:', errors)
67
+ }
68
+
69
+ return formItemRules
70
+ }
71
+
72
+ export const handleRequiredRule = (message: string): FormItemRule => ({
73
+ required: true,
74
+ message,
75
+ })
76
+
77
+ const handleDataLengthRule = (value: any, message: string): FormItemRule | null => {
78
+ if (value) return { len: parseFloat(value), message }
79
+
80
+ console.error('Invalid or missing value for ExactDataLength rule')
81
+ return null
82
+ }
83
+
84
+ const handleMinLengthRule = (value: any, message: string): FormItemRule | null => {
85
+ if (value != null && value !== '') return { min: parseFloat(value), message }
86
+
87
+ console.error('Invalid or missing value for MinLength rule')
88
+ return null
89
+ }
90
+
91
+ const handleMaxLengthRule = (value: any, message: string): FormItemRule | null => {
92
+ if (value != null && value !== '') return { max: parseFloat(value), message }
93
+
94
+ console.error('Invalid or missing value for MaxLength rule')
95
+ return null
96
+ }
97
+
98
+ const handleRegexRule = (value: any, message: string): FormItemRule | null => {
99
+ if (value instanceof RegExp) return { pattern: value, message }
100
+
101
+ console.error('Invalid or missing value for Regex rule')
102
+ return null
103
+ }
104
+
105
+ const handleEmailRule = (message: string): FormItemRule => ({ type: 'email', message })
106
+
107
+ const handlePhoneNumberRule = (message: string, country: PhoneNumberCountry): FormItemRule => ({
108
+ pattern: country === PhoneNumberCountry.US ? /^\(\d{3}\) \d{3}-\d{4}$/ : /^\d{4} \d{4}$/,
109
+ message,
110
+ })
111
+
112
+ enum PhoneNumberCountry {
113
+ US,
114
+ MN,
115
+ }
116
+
117
+ export const findMaxAndMinValues = (validations: IValidationRule[] = []) => ({
118
+ maxValue: validations.find((v) => v.key === FieldValidationEnum.MaxValue)?.value,
119
+ minValue: validations.find((v) => v.key === FieldValidationEnum.MinValue)?.value,
120
+ })