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.
- package/.prettierrc +9 -0
- package/AJV_JSON_Schema_Guide.md +409 -0
- package/README.md +108 -0
- package/index.ts +8 -0
- package/package.json +40 -0
- package/src/ajv/form/form.schema.json +10 -0
- package/src/ajv/form/layout.schema.json +97 -0
- package/src/ajv/form/migration-rules.schema.json +59 -0
- package/src/ajv/master-portal-only/render-conditions/conditions.schema.json +24 -0
- package/src/ajv/master-portal-only/render-conditions/validate.ts +15 -0
- package/src/components/common/button.tsx +72 -0
- package/src/components/common/custom-hooks/use-find-dynamic-form.ts +33 -0
- package/src/components/common/custom-hooks/use-lazy-modal-opener.hook.ts +20 -0
- package/src/components/common/custom-hooks/use-notification.hook.tsx +157 -0
- package/src/components/common/disabled-field-indicator.tsx +20 -0
- package/src/components/common/warning-icon.tsx +10 -0
- package/src/components/form/layout-renderer/1-row/index.tsx +27 -0
- package/src/components/form/layout-renderer/2-col/index.tsx +32 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button.tsx +277 -0
- package/src/components/form/layout-renderer/3-element/2-field-element.tsx +220 -0
- package/src/components/form/layout-renderer/3-element/index.tsx +73 -0
- package/src/components/index.tsx +2 -0
- package/src/components/modals/form-data-loading.modal.tsx +48 -0
- package/src/constants.ts +15 -0
- package/src/enums.ts +177 -0
- package/src/functions/axios-handler.ts +158 -0
- package/src/functions/data-list-functions.tsx +41 -0
- package/src/functions/form-schema-validator.ts +50 -0
- package/src/functions/get-element-props.ts +20 -0
- package/src/functions/index.ts +56 -0
- package/src/functions/json-handlers.ts +19 -0
- package/src/functions/validations.ts +120 -0
- package/src/types/form-data-list/index.ts +54 -0
- package/src/types/index.ts +124 -0
- package/src/types/layout-elements/element-data-render-logic.ts +56 -0
- package/src/types/layout-elements/field-option-source.ts +14 -0
- package/src/types/layout-elements/index.ts +224 -0
- package/src/types/layout-elements/style.ts +35 -0
- package/src/types/layout-elements/validation.ts +18 -0
- 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
|
+
})
|