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.
- package/package.json +1 -1
- package/src/components/common/currency-field.tsx +8 -0
- package/src/components/form/1-list/index.tsx +103 -58
- package/src/components/form/1-list/table-header.tsx +42 -57
- package/src/components/form/1-list/table.tsx +101 -157
- package/src/components/form/layout-renderer/1-row/index.tsx +39 -46
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +1 -1
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +1 -1
- package/src/components/form/layout-renderer/3-element/3-read-field-data.tsx +7 -6
- package/src/components/form/layout-renderer/3-element/5-re-captcha.tsx +11 -3
- package/src/components/form/layout-renderer/3-element/8-fields-with-options.tsx +62 -10
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +173 -171
- package/src/constants.ts +8 -0
- package/src/enums/form.enum.ts +5 -5
- package/src/enums/index.ts +4 -0
- package/src/functions/axios-handler.ts +23 -2
- package/src/functions/forms/convert-number-into-words.ts +47 -0
- package/src/functions/forms/data-render-functions.tsx +11 -9
- package/src/functions/forms/get-data-list-option-value.ts +69 -0
- package/src/functions/forms/index.ts +23 -86
- package/src/types/companies/index.ts +5 -1
- package/src/types/companies/site-layout/authenticated/index.tsx +39 -28
- package/src/types/companies/site-layout/unauthenticated/index.tsx +145 -6
- package/src/types/forms/data-list/index.ts +2 -13
- package/src/types/forms/index.ts +10 -0
- package/src/types/forms/layout-elements/button.ts +5 -3
- package/src/types/forms/layout-elements/data-render-config.ts +5 -0
- package/src/types/forms/layout-elements/field-option-source.ts +2 -1
- package/src/types/forms/layout-elements/index.ts +2 -7
- package/src/types/forms/layout-elements/read-field-data-props.ts +21 -0
package/package.json
CHANGED
|
@@ -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
|
|
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, {
|
|
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 [
|
|
18
|
-
const [
|
|
19
|
-
const [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
51
|
-
|
|
81
|
+
const fetchFormDataList = useCallback(
|
|
82
|
+
(reqData?: IDataListReqData & { skip?: number }) => {
|
|
83
|
+
if (!reqData || !formId) return
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
.
|
|
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
|
-
{
|
|
77
|
-
dataList={
|
|
115
|
+
layoutsConfigs={listLayoutConfig}
|
|
116
|
+
dataList={dataList}
|
|
78
117
|
updateDataList={(reqData) => {
|
|
79
|
-
fetchFormDataList(reqData
|
|
118
|
+
fetchFormDataList(reqData)
|
|
80
119
|
}}
|
|
81
|
-
|
|
120
|
+
parentLoadings={loadings}
|
|
82
121
|
loadingBlock={<FormDataListSkeleton_Table />}
|
|
83
|
-
setParentLoading={
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
...dataListHeaderContext
|
|
24
|
+
updateDynamicFilter,
|
|
25
|
+
headerLayoutContext,
|
|
34
26
|
}: IFormDataListHeaderComponent) {
|
|
35
|
-
const [
|
|
27
|
+
const [filtersFormRef] = Form.useForm()
|
|
36
28
|
const [filterConfigs, setFilterConfigs] = useState<IFilterNested>({})
|
|
37
|
-
const { userId, formId, formName, parentInfo, formDataId } =
|
|
29
|
+
const { userId, formId, formName, parentInfo, formDataId, onCustomFunctionCall } = headerLayoutContext
|
|
30
|
+
const isInitialFetchRef = useRef(true)
|
|
38
31
|
|
|
39
32
|
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
()
|
|
43
|
-
|
|
44
|
-
)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (layoutConfig) setFilterConfigs(extractFiltersFromLayout(layoutConfig.elements))
|
|
36
|
+
}, [layoutConfig])
|
|
45
37
|
|
|
46
|
-
const
|
|
38
|
+
const dndLayout = useMemo(() => {
|
|
39
|
+
const breakpoint = currentBreakpoint || DeviceBreakpointEnum.Default
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(() =>
|
|
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 {
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
100
|
+
[filterConfigs],
|
|
118
101
|
)
|
|
119
102
|
|
|
120
103
|
useEffect(() => {
|
|
121
|
-
if (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:
|
|
126
|
-
[
|
|
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={
|
|
131
|
-
{
|
|
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
|
|
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
|
|
153
|
-
|
|
138
|
+
layoutConfig?: IDndLayoutStructure_Responsive
|
|
139
|
+
updateDynamicFilter: (match?: string) => void
|
|
154
140
|
titleComponent?: ReactNode
|
|
155
|
-
defaultFilter: IReqDataConfig
|
|
156
141
|
startLoading: () => void
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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()
|