form-craft-package 1.7.9-dev.2 → 1.7.10-dev.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/index.ts +4 -1
- package/package.json +4 -4
- package/src/api/client.ts +10 -0
- package/src/components/common/countdown.tsx +44 -0
- package/src/components/common/custom-hooks/index.ts +2 -0
- package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +18 -0
- package/src/components/common/custom-hooks/use-check-element-conditions.hook.ts +3 -2
- package/src/components/common/custom-hooks/use-dayjs-extender.hook.ts +8 -0
- package/src/components/common/custom-hooks/use-many-to-many-connector.hook.ts +2 -3
- package/src/components/common/custom-hooks/use-notification.hook.tsx +1 -1
- package/src/components/common/custom-hooks/use-window-width.hook.ts +6 -4
- package/src/components/common/loading-skeletons/details.tsx +61 -6
- package/src/components/common/loading-skeletons/index.tsx +10 -2
- package/src/components/form/1-list/index.tsx +32 -17
- package/src/components/form/1-list/table-header.tsx +29 -55
- package/src/components/form/1-list/table.tsx +3 -5
- package/src/components/form/2-details/index.tsx +26 -26
- package/src/components/form/layout-renderer/1-row/index.tsx +9 -7
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +25 -19
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +11 -5
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-custom-function-call.hook.ts +22 -0
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +2 -2
- package/src/components/form/layout-renderer/3-element/2-field-element.tsx +40 -6
- package/src/components/form/layout-renderer/3-element/5-re-captcha.tsx +1 -1
- package/src/components/form/layout-renderer/3-element/6-signature.tsx +2 -3
- package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +142 -61
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +34 -27
- package/src/components/form/layout-renderer/3-element/index.tsx +3 -0
- package/src/components/index.tsx +2 -0
- package/src/components/modals/pdf-preview.modal.tsx +41 -0
- package/src/constants.ts +1 -0
- package/src/enums/form.enum.ts +8 -7
- package/src/enums/index.ts +1 -0
- package/src/functions/forms/conditional-rule-validator.ts +2 -0
- package/src/functions/forms/data-render-functions.tsx +21 -6
- package/src/functions/forms/get-data-list-option-value.ts +3 -3
- package/src/functions/forms/index.ts +33 -4
- package/src/functions/forms/linked-form-joins.ts +19 -0
- package/src/functions/reports/create-blob-url.ts +16 -0
- package/src/functions/reports/index.tsx +1 -0
- package/src/types/forms/data-list/filter-config.ts +5 -16
- package/src/types/forms/data-list/index.ts +2 -0
- package/src/types/forms/index.ts +0 -1
- package/src/types/forms/layout-elements/data-render-config.ts +5 -5
- package/src/types/forms/layout-elements/field-option-source.ts +25 -10
- package/src/types/forms/layout-elements/index.ts +7 -1
- package/src/types/forms/layout-elements/style.ts +0 -4
- package/src/types/forms/relationship/index.ts +6 -6
- package/src/types/index.ts +2 -0
- package/src/functions/forms/breadcrumb-handlers.ts +0 -21
|
@@ -8,17 +8,20 @@ import { IFormContext } from '../1-row'
|
|
|
8
8
|
import client, { clientCancelToken } from '../../../../api/client'
|
|
9
9
|
import { CancelToken } from 'axios'
|
|
10
10
|
import { useBreadcrumb } from '../../../common/custom-hooks/use-breadcrumb.hook'
|
|
11
|
-
import { Spin } from 'antd'
|
|
11
|
+
import { Form, Spin } from 'antd'
|
|
12
|
+
import { constructDynamicFormHref, getIdEqualsQuery, isNewFormDataPage } from '../../../../functions'
|
|
12
13
|
import {
|
|
13
|
-
IFilterByLinkedFormRelPath,
|
|
14
14
|
IFormJoin,
|
|
15
15
|
IFormLayoutFieldOption,
|
|
16
16
|
IOptionSourceConstant,
|
|
17
|
+
IOptionSourceDynamicForm,
|
|
18
|
+
IOptionSourceLinkedForm,
|
|
17
19
|
IOptionSourceReadFromDetails,
|
|
18
20
|
IRadioElement,
|
|
19
21
|
ISelectElement,
|
|
20
22
|
} from '../../../../types'
|
|
21
|
-
import {
|
|
23
|
+
import { useFindDynamiForm } from '../../../common/custom-hooks'
|
|
24
|
+
import { useNavigate } from 'react-router-dom'
|
|
22
25
|
|
|
23
26
|
export default function LayoutRenderer_FieldsWithOptions({
|
|
24
27
|
formContext,
|
|
@@ -26,14 +29,18 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
26
29
|
elementData,
|
|
27
30
|
isDisabled,
|
|
28
31
|
}: ILayoutRenderer_FieldsWithOptions) {
|
|
29
|
-
const
|
|
32
|
+
const navigate = useNavigate()
|
|
33
|
+
const { getFormById } = useFindDynamiForm()
|
|
34
|
+
const { formRef, formId, formDataId } = formContext
|
|
30
35
|
const [options, setOptions] = useState<{ value: string; label: string }[]>([])
|
|
31
36
|
const props = getElementGeneralizedProps(elementData.props)
|
|
32
37
|
const cancelTokenRef = useRef<CancelToken | undefined>(undefined)
|
|
33
|
-
const { breadcrumbs } = useBreadcrumb()
|
|
38
|
+
const { breadcrumbs, push } = useBreadcrumb()
|
|
34
39
|
const [loading, setLoading] = useState(true)
|
|
35
40
|
const isManyToManyPageRef = useRef(false)
|
|
36
41
|
|
|
42
|
+
const selectedValue = Form.useWatch(formItem.path, formRef)
|
|
43
|
+
|
|
37
44
|
const { inPreviewMode } = useFormPreservedItemValues(formRef)
|
|
38
45
|
|
|
39
46
|
const parentRelInfo = useMemo(() => {
|
|
@@ -43,87 +50,109 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
43
50
|
|
|
44
51
|
if (!!breadcrumb.manyToManyRelInfo) isManyToManyPageRef.current = true
|
|
45
52
|
|
|
46
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
formJoins: !!breadcrumb.manyToManyRelInfo ? [] : breadcrumb.formJoins ?? [],
|
|
55
|
+
formDataId: breadcrumb.formDataId ?? '',
|
|
56
|
+
detailPageFormId: breadcrumb.detailPageFormId,
|
|
57
|
+
}
|
|
47
58
|
}, [breadcrumbs])
|
|
48
59
|
|
|
49
60
|
const fetchFormData = useCallback(
|
|
50
|
-
async (
|
|
61
|
+
async (optionSource: IOptionSourceDynamicForm) => {
|
|
62
|
+
if (!optionSource.formId) {
|
|
63
|
+
setLoading(false)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
const filteredJoins = (parentRelInfo.formJoins as IFormJoin[])
|
|
52
|
-
.filter((j) => j.formId !==
|
|
53
|
-
.map((j, jIdx) => (jIdx === 0 ? { ...j, localField: j.localField.replace(`${
|
|
68
|
+
.filter((j) => j.formId !== optionSource.formId)
|
|
69
|
+
.map((j, jIdx) => (jIdx === 0 ? { ...j, localField: j.localField.replace(`${optionSource.formId}.`, '') } : j))
|
|
54
70
|
const lastJoin = filteredJoins.length ? filteredJoins[filteredJoins.length - 1] : null
|
|
55
71
|
|
|
72
|
+
const dynamicFormProjectOtherFields = [
|
|
73
|
+
'field1_RC',
|
|
74
|
+
'field2',
|
|
75
|
+
'field2_RC',
|
|
76
|
+
'field3',
|
|
77
|
+
'field3_RC',
|
|
78
|
+
'field4',
|
|
79
|
+
'field4_RC',
|
|
80
|
+
].reduce(
|
|
81
|
+
(c, f) => ({
|
|
82
|
+
...c,
|
|
83
|
+
[f]: f.endsWith('_RC') ? optionSource[f as IOTherField_RC] : `$${optionSource[f as IOtherField]}`,
|
|
84
|
+
}),
|
|
85
|
+
{},
|
|
86
|
+
)
|
|
56
87
|
client
|
|
57
|
-
.post(`/api/report/data/${
|
|
88
|
+
.post(`/api/report/data/${optionSource.formId}`, {
|
|
58
89
|
joins: filteredJoins,
|
|
59
90
|
match: JSON.stringify(
|
|
60
91
|
parentRelInfo.formDataId && !isManyToManyPageRef.current
|
|
61
92
|
? { DeletedDate: null, ...getIdEqualsQuery(lastJoin?.alias, parentRelInfo.formDataId) }
|
|
62
93
|
: { DeletedDate: null },
|
|
63
94
|
),
|
|
64
|
-
project: JSON.stringify({ value: '$_id', label:
|
|
95
|
+
project: JSON.stringify({ value: '$_id', label: `$${optionSource.field}`, ...dynamicFormProjectOtherFields }),
|
|
65
96
|
})
|
|
66
97
|
.then((res) => {
|
|
67
98
|
if (res.status === 200) {
|
|
68
99
|
const { totalRecords, data } = res.data
|
|
69
|
-
if (totalRecords === 1) formRef?.setFieldValue(formItem.name, data[0]._id)
|
|
100
|
+
if (totalRecords === 1 && isNewFormDataPage(formDataId)) formRef?.setFieldValue(formItem.name, data[0]._id)
|
|
70
101
|
setOptions(data)
|
|
71
102
|
}
|
|
72
103
|
})
|
|
73
104
|
.finally(() => setLoading(false))
|
|
74
105
|
},
|
|
75
|
-
[parentRelInfo, formId, formRef],
|
|
106
|
+
[parentRelInfo, formId, formRef, formDataId],
|
|
76
107
|
)
|
|
77
108
|
|
|
78
|
-
const fetchLinkedFormData = useCallback(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
setOptions(data)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
.finally(() => setLoading(false))
|
|
121
|
-
} else setLoading(false)
|
|
122
|
-
} else setLoading(false)
|
|
123
|
-
}, [])
|
|
109
|
+
const fetchLinkedFormData = useCallback(
|
|
110
|
+
(optionSource: IOptionSourceLinkedForm) => {
|
|
111
|
+
if (!optionSource.baseFormId) {
|
|
112
|
+
setLoading(false)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { groupOtherFields, projectOtherFields } = getOtherFields(optionSource)
|
|
117
|
+
|
|
118
|
+
const groupFields: { [key: string]: string | { $first: any } } = {
|
|
119
|
+
_id: '$_id',
|
|
120
|
+
label: { $first: `$${optionSource.field}` },
|
|
121
|
+
...groupOtherFields,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
client
|
|
125
|
+
.post(`/api/report/data/${optionSource.baseFormId}`, {
|
|
126
|
+
joins: optionSource.joins,
|
|
127
|
+
match: JSON.stringify(
|
|
128
|
+
parentRelInfo.formDataId
|
|
129
|
+
? { ...getIdEqualsQuery(parentRelInfo.detailPageFormId, parentRelInfo.formDataId), DeletedDate: null }
|
|
130
|
+
: { DeletedDate: null },
|
|
131
|
+
),
|
|
132
|
+
group: JSON.stringify(groupFields),
|
|
133
|
+
project: JSON.stringify({ _id: 0, value: '$_id', label: 1, ...projectOtherFields }),
|
|
134
|
+
})
|
|
135
|
+
.then((res) => {
|
|
136
|
+
if (res.status === 200) {
|
|
137
|
+
const { totalRecords, data } = res.data
|
|
138
|
+
if (totalRecords.length === 1 && isNewFormDataPage(formDataId))
|
|
139
|
+
formRef?.setFieldValue(formItem.name, data[0]._id)
|
|
140
|
+
|
|
141
|
+
setOptions(data)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.finally(() => setLoading(false))
|
|
145
|
+
},
|
|
146
|
+
[parentRelInfo, formDataId, formId],
|
|
147
|
+
)
|
|
124
148
|
|
|
125
149
|
const fetchDetailsStaticOptions = useCallback((optionSource: IOptionSourceReadFromDetails) => {
|
|
126
150
|
const { formId, optionFieldPath } = optionSource
|
|
151
|
+
if (!formId) {
|
|
152
|
+
setLoading(false)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
client
|
|
128
157
|
.post(
|
|
129
158
|
`/api/form/${formId}`,
|
|
@@ -158,10 +187,10 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
158
187
|
} else if (!inPreviewMode) {
|
|
159
188
|
switch (props.optionSource.type) {
|
|
160
189
|
case FieldElementOptionSourceEnum.DynamicForm:
|
|
161
|
-
fetchFormData(props.optionSource
|
|
190
|
+
fetchFormData(props.optionSource)
|
|
162
191
|
return
|
|
163
192
|
case FieldElementOptionSourceEnum.LinkedForm:
|
|
164
|
-
fetchLinkedFormData(props.
|
|
193
|
+
fetchLinkedFormData(props.optionSource)
|
|
165
194
|
|
|
166
195
|
return
|
|
167
196
|
case FieldElementOptionSourceEnum.ReadFromDetails:
|
|
@@ -182,11 +211,35 @@ export default function LayoutRenderer_FieldsWithOptions({
|
|
|
182
211
|
fields={[
|
|
183
212
|
{
|
|
184
213
|
...props,
|
|
214
|
+
label: (
|
|
215
|
+
<div className="flex items-center gap-2 w-full">
|
|
216
|
+
<span>{props.label}</span>
|
|
217
|
+
{props.optionSource?.formId &&
|
|
218
|
+
selectedValue &&
|
|
219
|
+
[FieldElementOptionSourceEnum.DynamicForm, FieldElementOptionSourceEnum.LinkedForm].includes(
|
|
220
|
+
props.optionSource.type,
|
|
221
|
+
) && (
|
|
222
|
+
<div
|
|
223
|
+
className="px-2 text-primary font-bold text-12 hover:underline cursor-pointer"
|
|
224
|
+
onClick={() => {
|
|
225
|
+
const formInfo = getFormById(props.optionSource.formId)
|
|
226
|
+
if (!formInfo) return
|
|
227
|
+
|
|
228
|
+
push({ label: `${formInfo.name} details`, href: location.pathname })
|
|
229
|
+
navigate(`${constructDynamicFormHref(formInfo.name)}/${selectedValue}`)
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
Go to Details
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
),
|
|
185
237
|
type: elementData.elementType,
|
|
186
238
|
name: formItem.name,
|
|
187
239
|
validations: elementData.validations,
|
|
188
240
|
options,
|
|
189
241
|
disabled: isDisabled,
|
|
242
|
+
mode: props.optionSource.type === FieldElementOptionSourceEnum.Any ? 'tags' : undefined,
|
|
190
243
|
},
|
|
191
244
|
]}
|
|
192
245
|
formRef={formRef}
|
|
@@ -204,3 +257,31 @@ type ILayoutRenderer_FieldsWithOptions = {
|
|
|
204
257
|
formContext: IFormContext
|
|
205
258
|
elementData: ISelectElement | IRadioElement
|
|
206
259
|
} & IElementBaseProps
|
|
260
|
+
|
|
261
|
+
const getOtherFields = (optionSource: IOptionSourceLinkedForm | IOptionSourceLinkedForm) => {
|
|
262
|
+
const otherFields: {
|
|
263
|
+
field: IOtherField
|
|
264
|
+
rcField: IOTherField_RC
|
|
265
|
+
}[] = [
|
|
266
|
+
{ field: 'field2', rcField: 'field2_RC' },
|
|
267
|
+
{ field: 'field3', rcField: 'field3_RC' },
|
|
268
|
+
{ field: 'field4', rcField: 'field4_RC' },
|
|
269
|
+
]
|
|
270
|
+
const groupOtherFields: { [key: string]: string | { $first: any } } = {}
|
|
271
|
+
const projectOtherFields: { [key: string]: 1 } = {}
|
|
272
|
+
otherFields.forEach((otherField) => {
|
|
273
|
+
if (optionSource[otherField.field]) {
|
|
274
|
+
groupOtherFields[otherField.field] = { $first: `$${optionSource[otherField.field]}` }
|
|
275
|
+
projectOtherFields[otherField.field] = 1
|
|
276
|
+
if (optionSource[otherField.rcField]) {
|
|
277
|
+
groupOtherFields[otherField.rcField] = { $first: { $literal: optionSource[otherField.rcField] } }
|
|
278
|
+
projectOtherFields[otherField.rcField] = 1
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return { groupOtherFields, projectOtherFields }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type IOtherField = 'field2' | 'field3' | 'field4'
|
|
287
|
+
type IOTherField_RC = 'field1_RC' | 'field2_RC' | 'field3_RC' | 'field4_RC'
|
|
@@ -2,21 +2,22 @@ import { IElementBaseProps } from '.'
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import FormDataListTableComponent, { IDataListLayoutConfig } from '../../1-list/table'
|
|
4
4
|
import FormDataListSkeleton_Table from '../../../common/loading-skeletons/table'
|
|
5
|
-
import { getIdEqualsQuery, getProjectionKey } from '../../../../functions'
|
|
6
|
-
import { NEW_FORM_DATA_IDENTIFIER } from '../../../../constants'
|
|
5
|
+
import { getIdEqualsQuery, getProjectionKey, isNewFormDataPage, mergeJoins } from '../../../../functions'
|
|
7
6
|
import { cancelableClient } from '../../../../api/client'
|
|
8
7
|
import { IFormContext } from '../1-row'
|
|
8
|
+
import { FormRelationshipEnum } from '../../../../enums'
|
|
9
9
|
import {
|
|
10
10
|
IFormDataListData,
|
|
11
11
|
IFormDataLoadElementProps,
|
|
12
12
|
IFormJoin,
|
|
13
13
|
IFormRelationshipConfig,
|
|
14
|
+
IFormRelationshipConfig_OneToMany,
|
|
14
15
|
IFormSchema,
|
|
15
16
|
} from '../../../../types'
|
|
16
17
|
|
|
17
18
|
export default function LayoutRenderer_LoadFormData({ formContext, elementProps }: ILayoutRenderer_LoadFormData) {
|
|
18
19
|
const { formId, formRef, formDataId } = formContext
|
|
19
|
-
const { joins, baseFormId,
|
|
20
|
+
const { joins, baseFormId, m2mRelFormId, displayConfigId } = elementProps
|
|
20
21
|
const lastChildInfo = useRef<{ formName: string; parentFormJoins: IFormJoin[] }>({
|
|
21
22
|
formName: '',
|
|
22
23
|
parentFormJoins: [],
|
|
@@ -27,6 +28,8 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
27
28
|
const [dataList, setDataList] = useState<{ data: IFormDataListData[]; total: number }>({ data: [], total: 0 })
|
|
28
29
|
const dataProjectRef = useRef<{ [key: string]: string }>({})
|
|
29
30
|
const reportDataApiCancelFuncRef = useRef<() => void | undefined>(undefined)
|
|
31
|
+
const formJoinsRef = useRef<IFormJoin[]>([])
|
|
32
|
+
const m2mForeignKeysRef = useRef<{ currentForm: string; otherForm: string } | undefined>()
|
|
30
33
|
|
|
31
34
|
const fetchDataList = useCallback(
|
|
32
35
|
(headerAppliedFilters?: string) => {
|
|
@@ -37,7 +40,7 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
37
40
|
if (headerAppliedFilters) matchFilters.push(JSON.parse(headerAppliedFilters))
|
|
38
41
|
|
|
39
42
|
const { request, cancel } = cancelableClient.post(`/api/report/data/${baseFormId}`, {
|
|
40
|
-
joins,
|
|
43
|
+
joins: mergeJoins([joins, formJoinsRef.current]),
|
|
41
44
|
match: matchFilters.length
|
|
42
45
|
? JSON.stringify(matchFilters.length === 1 ? matchFilters[0] : { $and: matchFilters })
|
|
43
46
|
: '',
|
|
@@ -60,10 +63,10 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
useEffect(() => {
|
|
63
|
-
if (!
|
|
66
|
+
if (!baseFormId || isNewFormDataPage(formDataId)) return
|
|
64
67
|
|
|
65
68
|
// fetching display config (base form can be different than display config form)
|
|
66
|
-
const { request, cancel } = cancelableClient.get(`/api/form/${
|
|
69
|
+
const { request, cancel } = cancelableClient.get(`/api/form/${baseFormId}`)
|
|
67
70
|
request.then((res) => {
|
|
68
71
|
if (res.status === 200) {
|
|
69
72
|
const parsedData: IFormSchema | null = JSON.parse(res.data.data)
|
|
@@ -73,31 +76,32 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
73
76
|
const relationship: IFormRelationshipConfig | undefined = relationships.find((r: IFormRelationshipConfig) =>
|
|
74
77
|
r.displayConfigs?.some((dc) => dc.id === displayConfigId),
|
|
75
78
|
)
|
|
76
|
-
const displayConfig = relationship?.displayConfigs?.find((
|
|
79
|
+
const displayConfig = relationship?.displayConfigs?.find((dc) => dc.id === displayConfigId)
|
|
80
|
+
|
|
77
81
|
if (!displayConfig || displayConfig.isRenderChildForm) return
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
if (relationship!.type === FormRelationshipEnum.OneToMany && relationship?.m2mTargetFormFK)
|
|
84
|
+
m2mForeignKeysRef.current = {
|
|
85
|
+
currentForm: relationship!.foreignKey,
|
|
86
|
+
otherForm: (relationship as IFormRelationshipConfig_OneToMany)!.m2mTargetFormFK!,
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
lastChildInfo.current = { formName: res.data.name, parentFormJoins: joins }
|
|
81
90
|
|
|
82
91
|
if (displayConfig.config) {
|
|
92
|
+
formJoinsRef.current = mergeJoins([
|
|
93
|
+
...displayConfig.config.columns.map((c) => c.joins),
|
|
94
|
+
...Object.values(displayConfig.config.header.elements).reduce((c: IFormJoin[], el) => {
|
|
95
|
+
if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
|
|
96
|
+
return [...c, el.props.filter.joins]
|
|
97
|
+
}
|
|
98
|
+
return c
|
|
99
|
+
}, []),
|
|
100
|
+
])
|
|
83
101
|
if (Array.isArray(displayConfig.config.columns))
|
|
84
102
|
dataProjectRef.current = displayConfig.config.columns.reduce((curr, c) => {
|
|
85
103
|
if (!c.key) return curr
|
|
86
104
|
|
|
87
|
-
if (isManyToManyRelationship) {
|
|
88
|
-
// if the key starts with id, it means it's reading a field from other form joins that are already configured
|
|
89
|
-
// example: form id 145 one-to-many 145 many-to-many 146, and 149 is the rel form.
|
|
90
|
-
// the base form is 149, joined 145 (displayConfigFormId below) and 146. In this case, to read fields from 145, it appends 145 at front to make it 145.Data{...}
|
|
91
|
-
// However, to display data from 144, we can't append 145, whose keys already come with 144.Data.{...}. That's why it's checking if it starts with number or not
|
|
92
|
-
return {
|
|
93
|
-
...curr,
|
|
94
|
-
[getProjectionKey(c.key)]: c.key.match(/^\d/) ? `$${c.key}` : `$${displayConfigFormId}.${c.key}`,
|
|
95
|
-
// [getProjectionKey(c.key)]: `$${c.key}`,
|
|
96
|
-
relDataId: '$_id',
|
|
97
|
-
_id: `$${displayConfigFormId}._id`,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
105
|
return { ...curr, [getProjectionKey(c.key)]: `$${c.key}` }
|
|
102
106
|
}, {})
|
|
103
107
|
|
|
@@ -118,25 +122,28 @@ export default function LayoutRenderer_LoadFormData({ formContext, elementProps
|
|
|
118
122
|
cancel()
|
|
119
123
|
reportDataApiCancelFuncRef.current?.()
|
|
120
124
|
}
|
|
121
|
-
}, [
|
|
125
|
+
}, [baseFormId, displayConfigId, formDataId, fetchDataList])
|
|
122
126
|
|
|
123
127
|
const dataListHeaderContext = {
|
|
128
|
+
detailPageFormId: formId,
|
|
124
129
|
formId: baseFormId,
|
|
125
130
|
formDataId,
|
|
126
131
|
formRef,
|
|
127
132
|
...lastChildInfo.current,
|
|
128
133
|
manyToManyRelInfo:
|
|
129
|
-
|
|
134
|
+
m2mRelFormId && m2mRelFormId !== baseFormId && formId
|
|
130
135
|
? {
|
|
131
|
-
middleFormId:
|
|
136
|
+
middleFormId: m2mRelFormId,
|
|
132
137
|
currentFormId: formId,
|
|
133
|
-
otherFormId:
|
|
138
|
+
otherFormId: baseFormId,
|
|
139
|
+
currentFormFK: m2mForeignKeysRef.current?.currentForm,
|
|
140
|
+
otherFormFK: m2mForeignKeysRef.current?.otherForm,
|
|
134
141
|
}
|
|
135
142
|
: null,
|
|
136
143
|
onCustomFunctionCall: () => {},
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
if (!joins || !Array.isArray(joins) || formDataId
|
|
146
|
+
if (!joins || !Array.isArray(joins) || isNewFormDataPage(formDataId)) return <></>
|
|
140
147
|
|
|
141
148
|
return (
|
|
142
149
|
<FormDataListTableComponent
|
package/src/components/index.tsx
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Document, Page, pdfjs } from 'react-pdf'
|
|
3
|
+
// import 'react-pdf/dist/Page/AnnotationLayer.css'
|
|
4
|
+
// import 'react-pdf/dist/Page/TextLayer.css'
|
|
5
|
+
|
|
6
|
+
interface PdfViewerProps {
|
|
7
|
+
fileUrl: string // could be a URL or Blob URL
|
|
8
|
+
initialPage?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`
|
|
12
|
+
|
|
13
|
+
export const PdfViewerModal: React.FC<PdfViewerProps> = ({ fileUrl, initialPage = 1 }) => {
|
|
14
|
+
const [numPages, setNumPages] = useState<number>(0)
|
|
15
|
+
const [pageNumber] = useState<number>(initialPage)
|
|
16
|
+
|
|
17
|
+
const onDocumentLoadSuccess = (pdf: { numPages: number; [key: string]: any }) => {
|
|
18
|
+
setNumPages(pdf.numPages)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex justify-center bg-background py-4">
|
|
23
|
+
<Document
|
|
24
|
+
className="w-max"
|
|
25
|
+
file={fileUrl}
|
|
26
|
+
onLoadSuccess={onDocumentLoadSuccess}
|
|
27
|
+
loading="Loading document…"
|
|
28
|
+
noData="No file specified"
|
|
29
|
+
>
|
|
30
|
+
{Array.from(new Array(numPages), (_, index) => (
|
|
31
|
+
<Page
|
|
32
|
+
key={`page_${index + 1}`}
|
|
33
|
+
pageNumber={pageNumber}
|
|
34
|
+
renderTextLayer={false}
|
|
35
|
+
renderAnnotationLayer={false}
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
38
|
+
</Document>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -27,6 +27,7 @@ export const DEVICE_BREAKPOINTS = {
|
|
|
27
27
|
[DeviceBreakpointEnum.TabletPortrait]: '768px',
|
|
28
28
|
[DeviceBreakpointEnum.TabletLandscape]: '992px',
|
|
29
29
|
[DeviceBreakpointEnum.Laptop]: '1200px',
|
|
30
|
+
[DeviceBreakpointEnum.LaptopLarge]: '1440px',
|
|
30
31
|
[DeviceBreakpointEnum.Default]: 'auto',
|
|
31
32
|
} as const
|
|
32
33
|
export const REPORT_EMPTY_CELL_CONTENT = '-'
|
package/src/enums/form.enum.ts
CHANGED
|
@@ -42,8 +42,9 @@ export enum DataRenderTypeEnum {
|
|
|
42
42
|
// string enum for readability
|
|
43
43
|
Default = 'Default',
|
|
44
44
|
Number = 'Number',
|
|
45
|
-
NumberInWords = 'NumberInWords',
|
|
45
|
+
// NumberInWords = 'NumberInWords',
|
|
46
46
|
Currency = 'Currency',
|
|
47
|
+
Tags = 'Tags',
|
|
47
48
|
Image = 'Image',
|
|
48
49
|
// PhoneNumber = 'PhoneNumber',
|
|
49
50
|
Date = 'Date',
|
|
@@ -67,6 +68,7 @@ export enum ButtonElementSizeEnum {
|
|
|
67
68
|
// Large = 'large',
|
|
68
69
|
}
|
|
69
70
|
export enum FieldElementOptionSourceEnum {
|
|
71
|
+
Any = 'Any',
|
|
70
72
|
Static = 'Static',
|
|
71
73
|
ReadFromDetails = 'ReadFromDetails',
|
|
72
74
|
DynamicForm = 'DynamicForm',
|
|
@@ -112,15 +114,14 @@ export enum JustifyAndAlignContent {
|
|
|
112
114
|
export enum FilterConfigTypeEnum {
|
|
113
115
|
NoFilter = 'NoFilter',
|
|
114
116
|
Custom = 'Custom',
|
|
115
|
-
ByAuthUser = 'ByAuthUser',
|
|
116
|
-
ByLinkedForm = 'ByLinkedForm',
|
|
117
117
|
}
|
|
118
118
|
export enum DeviceBreakpointEnum {
|
|
119
119
|
Default = 'default',
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
LaptopLarge = 'xl',
|
|
121
|
+
Laptop = 'lg',
|
|
122
|
+
TabletLandscape = 'md',
|
|
123
|
+
TabletPortrait = 'sm',
|
|
124
|
+
Mobile = 'xs',
|
|
124
125
|
}
|
|
125
126
|
export enum FormLoadingModalTypeEnum {
|
|
126
127
|
GeneratingPdf = 1,
|
package/src/enums/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ export const validateConditions = (validations: IConditionalValidation_Field[],
|
|
|
68
68
|
return typeof fieldValue === 'string' && emailRegex.test(fieldValue)
|
|
69
69
|
|
|
70
70
|
case FieldValidationEnum.StrictlyEquals:
|
|
71
|
+
if (value === undefined) return [null, undefined, ''].includes(fieldValue) // equals no value
|
|
72
|
+
|
|
71
73
|
return fieldValue === value
|
|
72
74
|
|
|
73
75
|
default:
|
|
@@ -7,7 +7,6 @@ import { INTERNATIONALIZATION_DATA } from '../../constants'
|
|
|
7
7
|
import { evaluateCondition } from './evaluate-condition'
|
|
8
8
|
import { ReactNode } from 'react'
|
|
9
9
|
import { IDataListHeaderLayoutContext } from '../../components/form/1-list/table'
|
|
10
|
-
import { convertIntegerToWords } from './convert-number-into-words'
|
|
11
10
|
import {
|
|
12
11
|
IDataRender_Buttons,
|
|
13
12
|
IDataRender_Conditional,
|
|
@@ -21,12 +20,11 @@ import {
|
|
|
21
20
|
IFormDataTableColumn,
|
|
22
21
|
} from '../../types'
|
|
23
22
|
|
|
24
|
-
export const
|
|
23
|
+
export const renderTableColumns = (
|
|
25
24
|
elements: IFormDataListColumn[],
|
|
26
25
|
contextValues: IDataListHeaderLayoutContext = {},
|
|
27
26
|
) => {
|
|
28
27
|
const { attachmentBaseUrl } = contextValues
|
|
29
|
-
|
|
30
28
|
return elements.map((el) => {
|
|
31
29
|
const col: IFormDataTableColumn = {
|
|
32
30
|
...el,
|
|
@@ -96,17 +94,22 @@ export const generateTableColumns = (
|
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
export const renderData = (data?: any, renderConfig?: IDataRenderConfig): ReactNode => {
|
|
99
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
[null, undefined, ''].includes(data) ||
|
|
99
|
+
!renderConfig ||
|
|
100
|
+
(typeof data === 'object' && renderConfig.type !== DataRenderTypeEnum.Tags)
|
|
101
|
+
)
|
|
102
|
+
return '-'
|
|
100
103
|
|
|
101
104
|
switch (renderConfig.type) {
|
|
102
105
|
case DataRenderTypeEnum.Date:
|
|
103
106
|
return renderDateData(data, renderConfig)
|
|
104
107
|
case DataRenderTypeEnum.Number:
|
|
105
108
|
return renderNumberData(Number(data), renderConfig)
|
|
106
|
-
case DataRenderTypeEnum.NumberInWords:
|
|
107
|
-
return convertIntegerToWords(Number(data))
|
|
108
109
|
case DataRenderTypeEnum.Currency:
|
|
109
110
|
return renderCurrencyData(Number(data), renderConfig)
|
|
111
|
+
case DataRenderTypeEnum.Tags:
|
|
112
|
+
return renderTags(data)
|
|
110
113
|
case DataRenderTypeEnum.Image:
|
|
111
114
|
return renderImageData(data, renderConfig)
|
|
112
115
|
// case DataRenderTypeEnum.PhoneNumber:
|
|
@@ -165,6 +168,18 @@ const renderImageData = (imgFullUrl: string, renderConfig: IDataRender_Image) =>
|
|
|
165
168
|
return <Image src={imgFullUrl} alt={alt} style={style} preview={{ maskClassName: 'rounded' }} />
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
const renderTags = (data: string[]) => {
|
|
172
|
+
if (!Array.isArray(data)) return '-'
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex flex-wrap gap-x-2 gap-y-1">
|
|
176
|
+
{data.map((str) => (
|
|
177
|
+
<span className="bg-gray-200 px-2 rounded-lg text-[11px]">{str}</span>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
// export const renderPhoneData = (data: string = '', country: CountryEnum = CountryEnum.US) => {
|
|
169
184
|
// const format = INTERNATIONALIZATION_DATA[country].phoneFormat
|
|
170
185
|
// if (!format) return 'N/A'
|
|
@@ -48,14 +48,14 @@ export async function processOptions(options: IFormDataListColumn[]): Promise<{
|
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
50
|
} else if (optionSource.type === FieldElementOptionSourceEnum.DynamicForm) {
|
|
51
|
-
const {
|
|
51
|
+
const { formId, field } = optionSource
|
|
52
52
|
|
|
53
53
|
dynamicApiPromises.push(
|
|
54
|
-
fetchFormDataAsLookup(
|
|
54
|
+
fetchFormDataAsLookup(formId).then((dynamicDataList) => {
|
|
55
55
|
dynamicDataList.forEach((dynamicData) => {
|
|
56
56
|
const parsedData = JSON.parse(dynamicData.data)
|
|
57
57
|
|
|
58
|
-
finalResult[dynamicData.id] = parsedData[
|
|
58
|
+
finalResult[dynamicData.id] = parsedData[field]
|
|
59
59
|
})
|
|
60
60
|
}),
|
|
61
61
|
)
|