form-craft-package 1.7.3 → 1.7.4

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/components/common/currency-field.tsx +8 -0
  3. package/src/components/form/1-list/index.tsx +103 -58
  4. package/src/components/form/1-list/table-header.tsx +42 -57
  5. package/src/components/form/1-list/table.tsx +101 -157
  6. package/src/components/form/layout-renderer/1-row/index.tsx +39 -46
  7. package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +1 -1
  8. package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +1 -1
  9. package/src/components/form/layout-renderer/3-element/3-read-field-data.tsx +7 -6
  10. package/src/components/form/layout-renderer/3-element/5-re-captcha.tsx +11 -3
  11. package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +62 -10
  12. package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +173 -171
  13. package/src/constants.ts +8 -0
  14. package/src/enums/form.enum.ts +5 -5
  15. package/src/enums/index.ts +4 -0
  16. package/src/functions/axios-handler.ts +23 -2
  17. package/src/functions/forms/convert-number-into-words.ts +47 -0
  18. package/src/functions/forms/data-render-functions.tsx +11 -9
  19. package/src/functions/forms/get-data-list-option-value.ts +69 -0
  20. package/src/functions/forms/index.ts +23 -86
  21. package/src/types/companies/index.ts +5 -1
  22. package/src/types/companies/site-layout/authenticated/index.tsx +39 -28
  23. package/src/types/companies/site-layout/unauthenticated/index.tsx +145 -6
  24. package/src/types/forms/data-list/index.ts +2 -13
  25. package/src/types/forms/index.ts +10 -0
  26. package/src/types/forms/layout-elements/button.ts +5 -3
  27. package/src/types/forms/layout-elements/data-render-config.ts +5 -0
  28. package/src/types/forms/layout-elements/field-option-source.ts +2 -1
  29. package/src/types/forms/layout-elements/index.ts +2 -7
  30. package/src/types/forms/layout-elements/read-field-data-props.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -6,6 +6,8 @@ import { INTERNATIONALIZATION_DATA } from '../../constants'
6
6
  export default function CurrencyField({
7
7
  country = CountryEnum.US,
8
8
  decimalPoint = 0,
9
+ value,
10
+ onChange,
9
11
  ...restProps
10
12
  }: ICurrencyField) {
11
13
  const inputRef = useRef<HTMLInputElement>(null)
@@ -44,6 +46,10 @@ export default function CurrencyField({
44
46
  ref={inputRef}
45
47
  onFocus={handleFocus}
46
48
  precision={decimalPoint}
49
+ value={value}
50
+ onChange={(val) => {
51
+ if (typeof val === 'number' && onChange) onChange(val)
52
+ }}
47
53
  formatter={(value) => {
48
54
  if (!value) return `${currencySymbol}${parseInt('0').toFixed(decimalPoint)}`
49
55
 
@@ -65,4 +71,6 @@ interface ICurrencyField {
65
71
  decimalPoint?: number
66
72
  placeholder?: string
67
73
  disabled?: boolean
74
+ value?: number
75
+ onChange?: (val: number) => void
68
76
  }
@@ -1,10 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react'
2
- import client from '../../../functions/axios-handler'
3
- import { parseJSON } from '../../../functions/forms/json-handlers'
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { cancelableClient } from '../../../functions/axios-handler'
4
3
  import FormDataListSkeleton_Table from '../../common/loading-skeletons/table'
5
- import { IFormData, IFormDataListConfig, IFormSchema, IDndLayoutStructure_Responsive } from '../../../types'
6
4
  import { ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-button'
7
- import FormDataListTableComponent, { IReqDataConfig } from './table'
5
+ import FormDataListTableComponent, { IDataListLayoutConfig } from './table'
6
+ import { IFormSchema, IFormJoin, IFormDataListData } from '../../../types'
8
7
 
9
8
  function FormDataListComponent({
10
9
  formName,
@@ -14,75 +13,114 @@ function FormDataListComponent({
14
13
  baseServerUrl,
15
14
  onCustomFunctionCall,
16
15
  }: IFormDataListComponent) {
17
- const [loading, setLoading] = useState(true)
18
- const [formData, setFormData] = useState<{ data: IFormData[]; total: number }>({ data: [], total: 0 })
19
- const [dataListConfig, setDataListConfig] = useState<
20
- | {
21
- dataListConfig: IFormDataListConfig & { configForFormId: number }
22
- detailsLayoutConfig: IDndLayoutStructure_Responsive
23
- }
24
- | undefined
25
- >()
16
+ const [loadings, setLoadings] = useState({ initial: true, data: true })
17
+ const [dataList, setDataList] = useState<{ data: IFormDataListData[]; total: number }>({ data: [], total: 0 })
18
+ const [listLayoutConfig, setListLayoutConfig] = useState<IDataListLayoutConfig>()
19
+
20
+ const reportDataApiCancelFuncRef = useRef<() => void | undefined>(undefined)
21
+ const dataProjectRef = useRef<{ [key: string]: string }>({})
22
+ const formJoinsRef = useRef<IFormJoin[]>([])
23
+ const defaultFilterReqDataRef = useRef<IDataListReqData | undefined>(undefined)
26
24
 
27
25
  const attachmentBaseUrl = useMemo(() => `${baseServerUrl}/api/attachment/${companyKey}`, [baseServerUrl, companyKey])
28
- const dataListHeaderContext = { formId, userId, attachmentBaseUrl, formName, companyKey }
26
+ const headerLayoutContext = { formId, userId, attachmentBaseUrl, formName, companyKey, onCustomFunctionCall }
27
+
28
+ useEffect(() => {
29
+ setLoadings({ initial: true, data: true })
30
+ setDataList({ data: [], total: 0 })
31
+ dataProjectRef.current = {}
32
+ formJoinsRef.current = []
33
+ defaultFilterReqDataRef.current = undefined
34
+ }, [location.pathname])
29
35
 
30
36
  useEffect(() => {
31
- if (formId) {
32
- client.get(`/api/form/${formId}`).then(async (res) => {
33
- if (res.status === 200) {
34
- const parsedData: IFormSchema | null = parseJSON(res.data.data)
35
- if (parsedData) {
36
- // configForFormId is passed to ensure that the table shows the correct data list when the forms are switched quickly
37
- setDataListConfig({
38
- dataListConfig: { ...parsedData.dataListConfig, configForFormId: formId },
39
- detailsLayoutConfig: {
40
- elements: parsedData.detailsConfig.elements,
41
- layouts: parsedData.detailsConfig.layouts,
42
- },
43
- })
37
+ if (!formId) return
38
+ const { request, cancel } = cancelableClient.get(`/api/form/${formId}`)
39
+ request.then(async (res) => {
40
+ if (res.status === 200) {
41
+ const parsedData: IFormSchema | null = JSON.parse(res.data.data)
42
+ if (parsedData) {
43
+ const { columns, formJoins, pagination, defaultFilter, defaultSorter } = parsedData.dataListConfig
44
+
45
+ if (Array.isArray(columns))
46
+ dataProjectRef.current = columns.reduce(
47
+ (curr, c) => (c.key ? { ...curr, [c.key.replace(/\./g, '_')]: `$${c.key}` } : curr),
48
+ {},
49
+ )
50
+ if (Array.isArray(formJoins)) formJoinsRef.current = formJoins
51
+
52
+ const defaultReqData: IDataListReqData = {}
53
+ if (!pagination?.hasNoPagination) {
54
+ defaultReqData.current = 1
55
+ defaultReqData.limit = pagination?.defaultPageSize ?? 10
44
56
  }
57
+ if (defaultSorter) defaultReqData.sort = JSON.stringify({ [defaultSorter.field]: defaultSorter.order })
58
+ if (defaultFilter?.config && Object.values(defaultFilter.config).length)
59
+ // The operators are stored using @, and here it replaces @ with $. Otherwise, MongoDB's JSON-to-BSON conversion driver was detecting certain operators, especially $regex, and automatically modified them. To prevent data manipulation by the MongoDB driver, @ is used instead of $.
60
+ defaultReqData.match = JSON.stringify(defaultFilter.config).replaceAll('@', '$')
61
+
62
+ defaultFilterReqDataRef.current = defaultReqData
63
+
64
+ fetchFormDataList(defaultReqData)
65
+
66
+ // configForFormId is passed to ensure that the table shows the correct data list when the forms are switched quickly
67
+ setListLayoutConfig({
68
+ configForFormId: formId,
69
+ dataListConfig: {
70
+ ...parsedData.dataListConfig,
71
+ columns: columns.map((c) => (c.key ? { ...c, key: c.key.replace(/\./g, '_') } : c)),
72
+ },
73
+ })
45
74
  }
46
- })
47
- }
75
+ }
76
+ })
77
+
78
+ return () => cancel()
48
79
  }, [formId])
49
80
 
50
- const fetchFormDataList = useCallback((reqData: IReqDataConfig, dynamicFormId?: number) => {
51
- if (!reqData || !dynamicFormId) return
81
+ const fetchFormDataList = useCallback(
82
+ (reqData?: IDataListReqData & { skip?: number }) => {
83
+ if (!reqData || !formId) return
52
84
 
53
- client
54
- .post(`/api/formdata/list/${dynamicFormId}`, {
55
- ...reqData,
56
- pagination: {
57
- number: reqData.pagination.current,
58
- size: reqData.pagination.pageSize,
59
- },
60
- })
61
- .then((res) => {
62
- if (res.status === 200)
63
- setFormData({
64
- ...res.data,
65
- data: res.data.data.map((d: IFormData) => {
66
- const { data, ...restD } = d
67
- return { ...restD, ...JSON.parse(data) }
68
- }),
69
- })
85
+ if (!reqData.current && defaultFilterReqDataRef.current?.current)
86
+ reqData.current = defaultFilterReqDataRef.current.current
87
+ if (!reqData.limit && defaultFilterReqDataRef.current?.limit)
88
+ reqData.limit = defaultFilterReqDataRef.current.limit
89
+ if (!reqData.sort && defaultFilterReqDataRef.current?.sort) reqData.sort = defaultFilterReqDataRef.current.sort
90
+ if (!reqData.match && defaultFilterReqDataRef.current?.match)
91
+ reqData.match = defaultFilterReqDataRef.current.match
92
+
93
+ if (reqData.current && reqData.limit) reqData.skip = (reqData.current - 1) * reqData.limit
94
+
95
+ const { current, ...restReqData } = reqData
96
+
97
+ const { request, cancel } = cancelableClient.post(`/api/report/data/${formId}`, {
98
+ joins: formJoinsRef.current,
99
+ ...restReqData,
100
+ project: JSON.stringify(dataProjectRef.current),
70
101
  })
71
- .finally(() => setLoading(false))
72
- }, [])
102
+ reportDataApiCancelFuncRef.current = cancel
103
+
104
+ request
105
+ .then((res) => {
106
+ if (res.status === 200) setDataList({ data: res.data.data, total: res.data.totalRecords })
107
+ })
108
+ .finally(() => setLoadings({ initial: false, data: false }))
109
+ },
110
+ [formId],
111
+ )
73
112
 
74
113
  return (
75
114
  <FormDataListTableComponent
76
- {...dataListConfig}
77
- dataList={formData}
115
+ layoutsConfigs={listLayoutConfig}
116
+ dataList={dataList}
78
117
  updateDataList={(reqData) => {
79
- fetchFormDataList(reqData, formId)
118
+ fetchFormDataList(reqData)
80
119
  }}
81
- parentLoading={loading}
120
+ parentLoadings={loadings}
82
121
  loadingBlock={<FormDataListSkeleton_Table />}
83
- setParentLoading={setLoading}
84
- onCustomFunctionCall={onCustomFunctionCall}
85
- {...dataListHeaderContext}
122
+ setParentLoading={(bool) => setLoadings((c) => ({ ...c, data: bool as boolean }))}
123
+ headerLayoutContext={headerLayoutContext}
86
124
  />
87
125
  )
88
126
  }
@@ -95,3 +133,10 @@ type IFormDataListComponent = {
95
133
  formId?: number
96
134
  userId: string | number
97
135
  } & ICustomFunctionCall
136
+
137
+ export interface IDataListReqData {
138
+ current?: number
139
+ limit?: number
140
+ sort?: string
141
+ match?: string
142
+ }
@@ -1,19 +1,13 @@
1
1
  import { Form } from 'antd'
2
- import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
3
  import dayjs from 'dayjs'
4
4
  import { extractFiltersFromLayout } from '../../../functions/forms'
5
5
  import { DeviceBreakpointEnum, FilterConfigTypeEnum, FormPreservedItemKeys } from '../../../enums'
6
6
  import { LayoutRendererRow } from '../layout-renderer/1-row'
7
7
  import { VALUE_REPLACEMENT_PLACEHOLDER } from '../../../constants'
8
- import { DynamicFormButtonRender, ICustomFunctionCall } from '../layout-renderer/3-element/1-dynamic-button'
8
+ import { DynamicFormButtonRender } from '../layout-renderer/3-element/1-dynamic-button'
9
9
  import useGetCurrentBreakpoint from '../../common/custom-hooks/use-window-width.hook'
10
- import {
11
- DEFAULT_NO_FILTER,
12
- DEFAULT_NO_SORTER,
13
- DEFAULT_PAGINATION,
14
- IDataListHeaderContext,
15
- IReqDataConfig,
16
- } from './table'
10
+ import { IDataListHeaderLayoutContext } from './table'
17
11
  import {
18
12
  IDndLayoutStructure_Responsive,
19
13
  IFilterByAuthUser,
@@ -26,30 +20,34 @@ import {
26
20
  export default function FormDataListHeaderComponent({
27
21
  layoutConfig,
28
22
  titleComponent,
29
- defaultFilter,
30
23
  startLoading,
31
- updateReqData,
32
- onCustomFunctionCall,
33
- ...dataListHeaderContext
24
+ updateDynamicFilter,
25
+ headerLayoutContext,
34
26
  }: IFormDataListHeaderComponent) {
35
- const [dataListHeaderFormRef] = Form.useForm()
27
+ const [filtersFormRef] = Form.useForm()
36
28
  const [filterConfigs, setFilterConfigs] = useState<IFilterNested>({})
37
- const { userId, formId, formName, parentInfo, formDataId } = dataListHeaderContext
29
+ const { userId, formId, formName, parentInfo, formDataId, onCustomFunctionCall } = headerLayoutContext
30
+ const isInitialFetchRef = useRef(true)
38
31
 
39
32
  const currentBreakpoint = useGetCurrentBreakpoint()
40
33
 
41
- const layout = useMemo(
42
- () => layoutConfig.layouts[currentBreakpoint] ?? layoutConfig.layouts[DeviceBreakpointEnum.Default] ?? [],
43
- [layoutConfig.layouts, currentBreakpoint],
44
- )
34
+ useEffect(() => {
35
+ if (layoutConfig) setFilterConfigs(extractFiltersFromLayout(layoutConfig.elements))
36
+ }, [layoutConfig])
45
37
 
46
- const filterValues = Form.useWatch([], dataListHeaderFormRef)
38
+ const dndLayout = useMemo(() => {
39
+ const breakpoint = currentBreakpoint || DeviceBreakpointEnum.Default
47
40
 
48
- useEffect(() => {
49
- if (dataListHeaderFormRef) dataListHeaderFormRef.setFieldsValue({ [FormPreservedItemKeys.InPreviewMode]: false })
50
- }, [dataListHeaderFormRef])
41
+ if (!layoutConfig || !layoutConfig.layouts[breakpoint]) return []
42
+
43
+ return layoutConfig.layouts[breakpoint]
44
+ }, [layoutConfig, currentBreakpoint])
45
+
46
+ const filterValues = Form.useWatch([], filtersFormRef)
51
47
 
52
- useEffect(() => setFilterConfigs(extractFiltersFromLayout(layoutConfig.elements)), [layoutConfig])
48
+ useEffect(() => {
49
+ if (filtersFormRef) filtersFormRef.setFieldsValue({ [FormPreservedItemKeys.InPreviewMode]: false })
50
+ }, [filtersFormRef])
53
51
 
54
52
  const organizeFilterData = useCallback(
55
53
  async (values: { [key: string]: any }) => {
@@ -67,7 +65,7 @@ export default function FormDataListHeaderComponent({
67
65
  if (config.type === FilterConfigTypeEnum.ByLinkedForm) startLoading()
68
66
 
69
67
  const filterData = await handleFilterValues(config as IFilterConfig, value, { userId })
70
- filtersToApply.push(filterData)
68
+ if (filterData) filtersToApply.push(filterData)
71
69
  } else {
72
70
  // cases like radio buttons, where each has its own filter, but only 1 needs to apply
73
71
  const nestedConfig = config as IFilterSimple
@@ -76,11 +74,11 @@ export default function FormDataListHeaderComponent({
76
74
  if (eachConfig.type === FilterConfigTypeEnum.ByLinkedForm) startLoading()
77
75
 
78
76
  const filterData = await handleFilterValues(eachConfig as IFilterConfig, value, { userId })
79
- filtersToApply.push(filterData)
77
+ if (filterData) filtersToApply.push(filterData)
80
78
  }
81
79
  }
82
80
 
83
- const { customFilter, dynamicFilters } = filtersToApply.reduce(
81
+ const { dynamicFilters } = filtersToApply.reduce(
84
82
  (curr: { customFilter: { [key: string]: any }; dynamicFilters: { [key: string]: any }[] }, next) => {
85
83
  if (!next) return curr
86
84
  const parsedDFilter = JSON.parse(next.dynamicFilter)
@@ -95,46 +93,34 @@ export default function FormDataListHeaderComponent({
95
93
  const updatedDynamicFilter = JSON.stringify(
96
94
  dynamicFilters.length === 1 ? dynamicFilters[0] : { $and: dynamicFilters },
97
95
  )
98
- updateReqData((c) => {
99
- if (!c)
100
- return {
101
- ...DEFAULT_NO_SORTER,
102
- pagination: DEFAULT_PAGINATION,
103
- customFilter,
104
- dynamicFilter: updatedDynamicFilter,
105
- }
106
96
 
107
- return {
108
- ...c,
109
- sort: defaultFilter.sort,
110
- pagination: { ...defaultFilter.pagination, current: 1 },
111
- customFilter,
112
- dynamicFilter: updatedDynamicFilter,
113
- }
114
- })
115
- } else updateReqData(defaultFilter)
97
+ updateDynamicFilter(updatedDynamicFilter)
98
+ } else updateDynamicFilter()
116
99
  },
117
- [filterConfigs, defaultFilter],
100
+ [filterConfigs],
118
101
  )
119
102
 
120
103
  useEffect(() => {
121
- if (filterValues) organizeFilterData(filterValues)
104
+ if (filterValues) {
105
+ if (!isInitialFetchRef.current) organizeFilterData(filterValues)
106
+ else isInitialFetchRef.current = false
107
+ }
122
108
  }, [filterValues])
123
109
 
124
110
  const formContext = useMemo(
125
- () => ({ formRef: dataListHeaderFormRef, formName, formId, formDataId, linkedParentsDataIds: parentInfo?.dataIds }),
126
- [dataListHeaderFormRef, formName, formId, formDataId, parentInfo],
111
+ () => ({ formRef: filtersFormRef, formName, formId, formDataId, linkedParentsDataIds: parentInfo?.dataIds }),
112
+ [filtersFormRef, formName, formId, formDataId, parentInfo],
127
113
  )
128
114
 
129
115
  return (
130
- <Form layout="vertical" form={dataListHeaderFormRef}>
131
- {layout.map((row, rowIdx) => (
116
+ <Form layout="vertical" form={filtersFormRef}>
117
+ {dndLayout.map((row, rowIdx) => (
132
118
  <LayoutRendererRow
133
119
  key={rowIdx}
134
120
  rowData={row}
135
121
  titleComponent={titleComponent}
136
122
  formContext={formContext}
137
- elements={layoutConfig.elements}
123
+ elements={layoutConfig?.elements ?? {}}
138
124
  renderButton={(btnProps, conditions) => (
139
125
  <DynamicFormButtonRender
140
126
  displayStateProps={{ btnProps, conditions, stateToPass: { parentInfo } }}
@@ -149,20 +135,19 @@ export default function FormDataListHeaderComponent({
149
135
  }
150
136
 
151
137
  type IFormDataListHeaderComponent = {
152
- layoutConfig: IDndLayoutStructure_Responsive
153
- updateReqData: React.Dispatch<React.SetStateAction<IReqDataConfig | undefined>>
138
+ layoutConfig?: IDndLayoutStructure_Responsive
139
+ updateDynamicFilter: (match?: string) => void
154
140
  titleComponent?: ReactNode
155
- defaultFilter: IReqDataConfig
156
141
  startLoading: () => void
157
- } & ICustomFunctionCall &
158
- IDataListHeaderContext
142
+ headerLayoutContext: IDataListHeaderLayoutContext
143
+ }
159
144
 
160
145
  const handleFilterValues = async (
161
146
  config: IFilterConfig,
162
147
  value: any,
163
148
  contextValues: { userId?: string | number; isDateFilter?: boolean },
164
149
  ) => {
165
- if (config.type === FilterConfigTypeEnum.NoFilter) return DEFAULT_NO_FILTER
150
+ if (config.type === FilterConfigTypeEnum.NoFilter) return null
166
151
  const { userId } = contextValues
167
152
 
168
153
  if ((config as IFilterCustom).isDateFilter) value = dayjs(value as Date).toISOString()