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.
Files changed (50) hide show
  1. package/index.ts +4 -1
  2. package/package.json +4 -4
  3. package/src/api/client.ts +10 -0
  4. package/src/components/common/countdown.tsx +44 -0
  5. package/src/components/common/custom-hooks/index.ts +2 -0
  6. package/src/components/common/custom-hooks/use-breadcrumb.hook.ts +18 -0
  7. package/src/components/common/custom-hooks/use-check-element-conditions.hook.ts +3 -2
  8. package/src/components/common/custom-hooks/use-dayjs-extender.hook.ts +8 -0
  9. package/src/components/common/custom-hooks/use-many-to-many-connector.hook.ts +2 -3
  10. package/src/components/common/custom-hooks/use-notification.hook.tsx +1 -1
  11. package/src/components/common/custom-hooks/use-window-width.hook.ts +6 -4
  12. package/src/components/common/loading-skeletons/details.tsx +61 -6
  13. package/src/components/common/loading-skeletons/index.tsx +10 -2
  14. package/src/components/form/1-list/index.tsx +32 -17
  15. package/src/components/form/1-list/table-header.tsx +29 -55
  16. package/src/components/form/1-list/table.tsx +3 -5
  17. package/src/components/form/2-details/index.tsx +26 -26
  18. package/src/components/form/layout-renderer/1-row/index.tsx +9 -7
  19. package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +25 -19
  20. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-button-navigate.hook.tsx +11 -5
  21. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-custom-function-call.hook.ts +22 -0
  22. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +2 -2
  23. package/src/components/form/layout-renderer/3-element/2-field-element.tsx +40 -6
  24. package/src/components/form/layout-renderer/3-element/5-re-captcha.tsx +1 -1
  25. package/src/components/form/layout-renderer/3-element/6-signature.tsx +2 -3
  26. package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +142 -61
  27. package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +34 -27
  28. package/src/components/form/layout-renderer/3-element/index.tsx +3 -0
  29. package/src/components/index.tsx +2 -0
  30. package/src/components/modals/pdf-preview.modal.tsx +41 -0
  31. package/src/constants.ts +1 -0
  32. package/src/enums/form.enum.ts +8 -7
  33. package/src/enums/index.ts +1 -0
  34. package/src/functions/forms/conditional-rule-validator.ts +2 -0
  35. package/src/functions/forms/data-render-functions.tsx +21 -6
  36. package/src/functions/forms/get-data-list-option-value.ts +3 -3
  37. package/src/functions/forms/index.ts +33 -4
  38. package/src/functions/forms/linked-form-joins.ts +19 -0
  39. package/src/functions/reports/create-blob-url.ts +16 -0
  40. package/src/functions/reports/index.tsx +1 -0
  41. package/src/types/forms/data-list/filter-config.ts +5 -16
  42. package/src/types/forms/data-list/index.ts +2 -0
  43. package/src/types/forms/index.ts +0 -1
  44. package/src/types/forms/layout-elements/data-render-config.ts +5 -5
  45. package/src/types/forms/layout-elements/field-option-source.ts +25 -10
  46. package/src/types/forms/layout-elements/index.ts +7 -1
  47. package/src/types/forms/layout-elements/style.ts +0 -4
  48. package/src/types/forms/relationship/index.ts +6 -6
  49. package/src/types/index.ts +2 -0
  50. 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 { getIdEqualsQuery } from '../../../../functions'
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 { formRef, parentFormJoins, formId, formDataId } = formContext
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 { formJoins: breadcrumb.formJoins ?? [], formDataId: breadcrumb.formDataId ?? '' }
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 (selectedFormId: number, selectedFormField: string) => {
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 !== selectedFormId)
53
- .map((j, jIdx) => (jIdx === 0 ? { ...j, localField: j.localField.replace(`${selectedFormId}.`, '') } : j))
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/${selectedFormId}`, {
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: `$Data.${selectedFormField}` }),
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((relationshipPath: IFilterByLinkedFormRelPath[] = []) => {
79
- const lastPath = relationshipPath[relationshipPath.length - 1]
80
-
81
- if (lastPath && lastPath.field) {
82
- // parentInfo.formJoins is the DIRECT joins between the details page form and base form of the LoadFormData
83
- const parentJoinOfLastPathIdx =
84
- parentFormJoins?.findIndex((j) => j.localField.startsWith(lastPath.formId.toString())) ?? -1
85
-
86
- if (parentJoinOfLastPathIdx > -1) {
87
- const slicedJoinsForLastPath =
88
- parentFormJoins
89
- ?.slice(parentJoinOfLastPathIdx)
90
- .map((j, jIdx) =>
91
- jIdx === 0 ? { ...j, localField: j.localField.replace(`${lastPath.formId}.`, '') } : j,
92
- ) ?? []
93
-
94
- const lastJoin = slicedJoinsForLastPath.length
95
- ? slicedJoinsForLastPath[slicedJoinsForLastPath.length - 1]
96
- : null
97
-
98
- client
99
- .post(`/api/report/data/${lastPath.formId}`, {
100
- joins: slicedJoinsForLastPath,
101
- match: JSON.stringify(
102
- formDataId && lastJoin && !isManyToManyPageRef.current
103
- ? {
104
- ...getIdEqualsQuery(lastJoin.alias, formDataId),
105
- DeletedDate: null,
106
- }
107
- : { DeletedDate: null },
108
- ),
109
- project: slicedJoinsForLastPath.length
110
- ? JSON.stringify({ value: '$_id', label: `$Data.${lastPath.field}` })
111
- : '',
112
- })
113
- .then((res) => {
114
- if (res.status === 200) {
115
- const { totalRecords, data } = res.data
116
- if (totalRecords.length === 1) formRef?.setFieldValue(formItem.name, data[0]._id)
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.form.id, props.optionSource.form.field)
190
+ fetchFormData(props.optionSource)
162
191
  return
163
192
  case FieldElementOptionSourceEnum.LinkedForm:
164
- fetchLinkedFormData(props.filter.relationshipPath)
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, displayConfigFormId, displayConfigId } = elementProps
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 (!displayConfigFormId || !formDataId || formDataId === NEW_FORM_DATA_IDENTIFIER) return
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/${displayConfigFormId}`)
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((config) => config.id === displayConfigId)
79
+ const displayConfig = relationship?.displayConfigs?.find((dc) => dc.id === displayConfigId)
80
+
77
81
  if (!displayConfig || displayConfig.isRenderChildForm) return
78
82
 
79
- const isManyToManyRelationship = displayConfigFormId !== baseFormId
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
- }, [displayConfigFormId, baseFormId, displayConfigId, formDataId, fetchDataList])
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
- displayConfigFormId !== baseFormId && formId
134
+ m2mRelFormId && m2mRelFormId !== baseFormId && formId
130
135
  ? {
131
- middleFormId: baseFormId,
136
+ middleFormId: m2mRelFormId,
132
137
  currentFormId: formId,
133
- otherFormId: displayConfigFormId,
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 === NEW_FORM_DATA_IDENTIFIER) return <></>
146
+ if (!joins || !Array.isArray(joins) || isNewFormDataPage(formDataId)) return <></>
140
147
 
141
148
  return (
142
149
  <FormDataListTableComponent
@@ -177,6 +177,9 @@ function LayoutRendererElement({
177
177
  case ElementTypeEnum.Breadcrumb:
178
178
  return <LayoutRenderer_Breadcrumb />
179
179
 
180
+ case ElementTypeEnum.Placeholder:
181
+ return <div />
182
+
180
183
  default:
181
184
  return (
182
185
  <LayoutRenderer_FieldElement
@@ -6,3 +6,5 @@ export * from './form/3-preview'
6
6
  export * from './common/button'
7
7
  export * from './report/1-list'
8
8
  export * from './companies'
9
+ export * from './common/countdown'
10
+ export * from './modals/pdf-preview.modal'
@@ -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 = '-'
@@ -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
- Laptop = 'xl',
121
- TabletLandscape = 'lg',
122
- TabletPortrait = 'md',
123
- Mobile = 'sm',
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,
@@ -18,6 +18,7 @@ export enum ElementTypeEnum {
18
18
  Switch = 'Switch',
19
19
  Password = 'Password',
20
20
  Container = 'Container',
21
+ Placeholder = 'Placeholder',
21
22
  RichTextEditor = 'RichTextEditor',
22
23
  ReportTable = 'ReportTable',
23
24
  FormTable = 'FormTable',
@@ -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 generateTableColumns = (
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 ([null, undefined, ''].includes(data) || typeof data === 'object' || !renderConfig) return 'N/A'
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 { form = {} } = optionSource
51
+ const { formId, field } = optionSource
52
52
 
53
53
  dynamicApiPromises.push(
54
- fetchFormDataAsLookup(form.id).then((dynamicDataList) => {
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[form.field]
58
+ finalResult[dynamicData.id] = parsedData[field]
59
59
  })
60
60
  }),
61
61
  )