form-craft-package 1.10.7 → 1.10.8-dev.1
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/custom-hooks/use-duplicate-on-blur.hook.ts +131 -88
- package/src/components/common/duplicate-entry-checker/duplicate-warning.modal.tsx +13 -6
- package/src/components/common/duplicate-entry-checker/index.tsx +62 -34
- package/src/components/form/2-details/index.tsx +30 -29
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +7 -7
- package/src/types/forms/index.ts +5 -4
package/package.json
CHANGED
|
@@ -1,88 +1,131 @@
|
|
|
1
|
-
import { FormInstance } from 'antd'
|
|
2
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
3
|
-
import { useBlurEventBus } from '../duplicate-entry-checker/blur-event-bus-provider'
|
|
4
|
-
import { IDuplicateCheckConfigGroup } from '../../../types'
|
|
5
|
-
import apiClient from '../../../api/client'
|
|
6
|
-
import { FormPreservedItemKeys } from '../../../enums'
|
|
7
|
-
|
|
8
|
-
interface IUseDuplicateOnBlur {
|
|
9
|
-
formRef: FormInstance
|
|
10
|
-
formKey?: string
|
|
11
|
-
formDataId?: string
|
|
12
|
-
groups?: IDuplicateCheckConfigGroup[]
|
|
13
|
-
debounceMs?: number
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
import { FormInstance } from 'antd'
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { useBlurEventBus } from '../duplicate-entry-checker/blur-event-bus-provider'
|
|
4
|
+
import { IDuplicateCheckConfigGroup } from '../../../types'
|
|
5
|
+
import apiClient from '../../../api/client'
|
|
6
|
+
import { FormPreservedItemKeys } from '../../../enums'
|
|
7
|
+
|
|
8
|
+
interface IUseDuplicateOnBlur {
|
|
9
|
+
formRef: FormInstance
|
|
10
|
+
formKey?: string
|
|
11
|
+
formDataId?: string
|
|
12
|
+
groups?: IDuplicateCheckConfigGroup[]
|
|
13
|
+
debounceMs?: number
|
|
14
|
+
functionName?: string
|
|
15
|
+
onCustomFunctionCall?: CustomFunctionCall
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useDuplicateOnBlur(
|
|
19
|
+
{ formRef, formKey, formDataId, groups, debounceMs = 0, functionName, onCustomFunctionCall }: IUseDuplicateOnBlur,
|
|
20
|
+
onResult: (isDuplicateFound: boolean, data?: unknown) => void,
|
|
21
|
+
) {
|
|
22
|
+
const { subscribeBlurEvent: subscribe } = useBlurEventBus()
|
|
23
|
+
|
|
24
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
25
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
26
|
+
|
|
27
|
+
const relevantFieldGroups = useMemo(() => {
|
|
28
|
+
if (!Array.isArray(groups)) return []
|
|
29
|
+
|
|
30
|
+
return groups.map((group) => group.fields)
|
|
31
|
+
}, [groups])
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const shouldUseCustomFunction = Boolean(functionName && onCustomFunctionCall)
|
|
35
|
+
if (!formKey && !shouldUseCustomFunction) return
|
|
36
|
+
|
|
37
|
+
const unsub = subscribe((blurredField) => {
|
|
38
|
+
const fieldClone: string = Array.isArray(blurredField) ? blurredField.join('.') : (blurredField as string)
|
|
39
|
+
|
|
40
|
+
const fieldGroup = relevantFieldGroups.find((g) => g.includes(`Data.${fieldClone}`))
|
|
41
|
+
|
|
42
|
+
if (!fieldGroup) return
|
|
43
|
+
|
|
44
|
+
const formValues = formRef.getFieldsValue(true)
|
|
45
|
+
|
|
46
|
+
if (fieldGroup.some((f) => !formValues[f.replace('Data.', '')])) return
|
|
47
|
+
|
|
48
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
49
|
+
|
|
50
|
+
timerRef.current = setTimeout(async () => {
|
|
51
|
+
formRef.setFieldValue([FormPreservedItemKeys.DuplicateCheckPending], true)
|
|
52
|
+
|
|
53
|
+
// cancel previous request
|
|
54
|
+
if (abortRef.current) abortRef.current.abort()
|
|
55
|
+
abortRef.current = shouldUseCustomFunction ? null : new AbortController()
|
|
56
|
+
|
|
57
|
+
const matchKeyValuePairs = {
|
|
58
|
+
DeletedDate: null,
|
|
59
|
+
...fieldGroup.reduce((acc, f) => ({ ...acc, [f]: formValues[f.replace('Data.', '')] }), {}),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const duplicateRecords = shouldUseCustomFunction
|
|
64
|
+
? normalizeDuplicateRecords(
|
|
65
|
+
await onCustomFunctionCall?.({
|
|
66
|
+
fnName: functionName as string,
|
|
67
|
+
formData: { ...formValues },
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
: normalizeDuplicateRecords(
|
|
71
|
+
(
|
|
72
|
+
await apiClient.post(
|
|
73
|
+
`/api/site/${formKey}/get`,
|
|
74
|
+
{
|
|
75
|
+
project: JSON.stringify({ _id: '$_id' }),
|
|
76
|
+
match: JSON.stringify(matchKeyValuePairs),
|
|
77
|
+
},
|
|
78
|
+
abortRef.current ? { signal: abortRef.current.signal } : undefined,
|
|
79
|
+
)
|
|
80
|
+
).data,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const filteredRecords = filterDuplicateRecords(duplicateRecords, formDataId)
|
|
84
|
+
const isRecordFound = filteredRecords.length > 0
|
|
85
|
+
|
|
86
|
+
const matchFilterKeysOnly = Object.keys(matchKeyValuePairs).join('::')
|
|
87
|
+
|
|
88
|
+
formRef.setFieldsValue({
|
|
89
|
+
[FormPreservedItemKeys.DuplicateDataFound]: { [matchFilterKeysOnly]: isRecordFound },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
onResult(isRecordFound, filteredRecords.length ? filteredRecords[0] : undefined)
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
if (e?.name === 'AbortError') return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
formRef.setFieldValue([FormPreservedItemKeys.DuplicateCheckPending], false)
|
|
98
|
+
}, debounceMs)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
unsub()
|
|
103
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
104
|
+
if (abortRef.current) abortRef.current.abort()
|
|
105
|
+
}
|
|
106
|
+
}, [subscribe, relevantFieldGroups, formKey, formDataId, formRef, debounceMs, functionName, onCustomFunctionCall])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalizeDuplicateRecords = (payload: any): any[] => {
|
|
110
|
+
if (!payload) return []
|
|
111
|
+
const data = payload?.data?.data ?? payload?.data ?? payload
|
|
112
|
+
if (!data) return []
|
|
113
|
+
return Array.isArray(data) ? data : [data]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const filterDuplicateRecords = (records: any[], formDataId?: string) => {
|
|
117
|
+
if (!Array.isArray(records)) return []
|
|
118
|
+
return records.filter((record) => {
|
|
119
|
+
if (!record) return false
|
|
120
|
+
if (!formDataId) return true
|
|
121
|
+
if (record._id && record._id === formDataId) return false
|
|
122
|
+
if (record.id && String(record.id) === formDataId) return false
|
|
123
|
+
return true
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type CustomFunctionCall = (params: {
|
|
128
|
+
fnName: string
|
|
129
|
+
messages?: { success?: string; error?: string }
|
|
130
|
+
formData?: { [key: string]: any }
|
|
131
|
+
}) => Promise<any> | any
|
|
@@ -7,7 +7,12 @@ import { FaExclamationTriangle } from 'react-icons/fa'
|
|
|
7
7
|
import { useNavigate } from 'react-router-dom'
|
|
8
8
|
import { DUPLICATE_CHECK_TRANSLATION_KEY } from '../../../constants'
|
|
9
9
|
|
|
10
|
-
export default function DuplicateWarningModal({
|
|
10
|
+
export default function DuplicateWarningModal({
|
|
11
|
+
formRef,
|
|
12
|
+
formId,
|
|
13
|
+
duplicateRecord,
|
|
14
|
+
closeModal,
|
|
15
|
+
}: IDuplicateWarningModal) {
|
|
11
16
|
const navigate = useNavigate()
|
|
12
17
|
const { t } = useTranslation(formId)
|
|
13
18
|
|
|
@@ -92,9 +97,11 @@ export default function DuplicateWarningModal({ formRef, formId, closeModal }: I
|
|
|
92
97
|
<div className="text-16 text-center">
|
|
93
98
|
{primaryMessage || 'We’ve found an existing record with the same details.'}
|
|
94
99
|
</div>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
{secondaryMessage && (
|
|
101
|
+
<div className="text-gray-500 text-[15px] text-center">
|
|
102
|
+
{secondaryMessage || 'Duplicate entries are not allowed in the system.'}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
98
105
|
</div>
|
|
99
106
|
{fieldLabels.length > 0 && (
|
|
100
107
|
<>
|
|
@@ -102,6 +109,7 @@ export default function DuplicateWarningModal({ formRef, formId, closeModal }: I
|
|
|
102
109
|
<div className="bg-background w-full rounded-md p-2 flex flex-col gap-1">
|
|
103
110
|
<span className="font-bold text-primary">Duplicate data found on the following field(s):</span>
|
|
104
111
|
<div className="flex gap-2">{fieldLabels.join(', ')}</div>
|
|
112
|
+
{typeof duplicateRecord === 'object' && <pre>{JSON.stringify(duplicateRecord, null, 2)}</pre>}
|
|
105
113
|
</div>
|
|
106
114
|
</>
|
|
107
115
|
)}
|
|
@@ -112,7 +120,6 @@ export default function DuplicateWarningModal({ formRef, formId, closeModal }: I
|
|
|
112
120
|
interface IDuplicateWarningModal {
|
|
113
121
|
formRef?: FormInstance
|
|
114
122
|
formId?: number
|
|
123
|
+
duplicateRecord?: unknown
|
|
115
124
|
closeModal: () => void
|
|
116
125
|
}
|
|
117
|
-
|
|
118
|
-
|
|
@@ -1,34 +1,62 @@
|
|
|
1
|
-
import { FormInstance } from 'antd'
|
|
2
|
-
import { useLazyModalOpener } from '../custom-hooks'
|
|
3
|
-
import { IDuplicateCheckConfig } from '../../../types'
|
|
4
|
-
import { useDuplicateOnBlur } from '../custom-hooks/use-duplicate-on-blur.hook'
|
|
5
|
-
import { lazy } from 'react'
|
|
6
|
-
|
|
7
|
-
const DuplicateWarningModal = lazy(() => import('./duplicate-warning.modal'))
|
|
8
|
-
|
|
9
|
-
export default function DuplicateEntryChecker({
|
|
10
|
-
formRef,
|
|
11
|
-
formId,
|
|
12
|
-
formKey,
|
|
13
|
-
formDataId,
|
|
14
|
-
duplicateCheckConfig,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
1
|
+
import { FormInstance } from 'antd'
|
|
2
|
+
import { useLazyModalOpener } from '../custom-hooks'
|
|
3
|
+
import { IDuplicateCheckConfig } from '../../../types'
|
|
4
|
+
import { useDuplicateOnBlur } from '../custom-hooks/use-duplicate-on-blur.hook'
|
|
5
|
+
import { lazy, useRef } from 'react'
|
|
6
|
+
|
|
7
|
+
const DuplicateWarningModal = lazy(() => import('./duplicate-warning.modal'))
|
|
8
|
+
|
|
9
|
+
export default function DuplicateEntryChecker({
|
|
10
|
+
formRef,
|
|
11
|
+
formId,
|
|
12
|
+
formKey,
|
|
13
|
+
formDataId,
|
|
14
|
+
duplicateCheckConfig,
|
|
15
|
+
onCustomFunctionCall,
|
|
16
|
+
}: IDuplicateEntryChecker) {
|
|
17
|
+
const { isModalOpen, openModal, closeModal } = useLazyModalOpener()
|
|
18
|
+
const duplicateDataRef = useRef<unknown>()
|
|
19
|
+
|
|
20
|
+
useDuplicateOnBlur(
|
|
21
|
+
{
|
|
22
|
+
formRef,
|
|
23
|
+
formKey,
|
|
24
|
+
formDataId,
|
|
25
|
+
groups: duplicateCheckConfig?.groups,
|
|
26
|
+
functionName: duplicateCheckConfig?.functionName,
|
|
27
|
+
onCustomFunctionCall,
|
|
28
|
+
},
|
|
29
|
+
(isDuplicateFound, data) => {
|
|
30
|
+
if (isDuplicateFound) {
|
|
31
|
+
duplicateDataRef.current = data
|
|
32
|
+
openModal()
|
|
33
|
+
} else closeModal()
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (!isModalOpen) return
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<DuplicateWarningModal
|
|
41
|
+
formRef={formRef}
|
|
42
|
+
formId={formId}
|
|
43
|
+
closeModal={closeModal}
|
|
44
|
+
duplicateRecord={duplicateDataRef.current}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IDuplicateEntryChecker {
|
|
50
|
+
formRef: FormInstance
|
|
51
|
+
formId?: number
|
|
52
|
+
formKey?: string
|
|
53
|
+
formDataId: string
|
|
54
|
+
duplicateCheckConfig?: IDuplicateCheckConfig
|
|
55
|
+
onCustomFunctionCall?: CustomFunctionCall
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type CustomFunctionCall = (params: {
|
|
59
|
+
fnName: string
|
|
60
|
+
messages?: { success?: string; error?: string }
|
|
61
|
+
formData?: { [key: string]: any }
|
|
62
|
+
}) => Promise<any> | any
|
|
@@ -45,18 +45,18 @@ export default function FormDataDetailsComponent(props: IFormDataDetailsComponen
|
|
|
45
45
|
)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function FormDataDetailsComponentChild({
|
|
49
|
-
isPublic,
|
|
50
|
-
formId,
|
|
51
|
-
formKey,
|
|
52
|
-
formDataId,
|
|
53
|
-
formName,
|
|
54
|
-
baseServerUrl,
|
|
55
|
-
initialValues,
|
|
56
|
-
companyKey,
|
|
57
|
-
customComponents,
|
|
58
|
-
onCustomFunctionCall,
|
|
59
|
-
}: IFormDataDetailsComponent) {
|
|
48
|
+
function FormDataDetailsComponentChild({
|
|
49
|
+
isPublic,
|
|
50
|
+
formId,
|
|
51
|
+
formKey,
|
|
52
|
+
formDataId,
|
|
53
|
+
formName,
|
|
54
|
+
baseServerUrl,
|
|
55
|
+
initialValues,
|
|
56
|
+
companyKey,
|
|
57
|
+
customComponents,
|
|
58
|
+
onCustomFunctionCall,
|
|
59
|
+
}: IFormDataDetailsComponent) {
|
|
60
60
|
const { pushCrumb, breadcrumbs } = useBreadcrumb()
|
|
61
61
|
|
|
62
62
|
const location = useLocation()
|
|
@@ -242,19 +242,20 @@ function FormDataDetailsComponentChild({
|
|
|
242
242
|
return deviceLayout
|
|
243
243
|
}, [cachedConfig?.detailsConfig, currentBreakpoint])
|
|
244
244
|
|
|
245
|
-
if (isConfigLoading || !cachedConfig) return <FormDataListSkeleton_Details />
|
|
245
|
+
if (isConfigLoading || !cachedConfig) return <FormDataListSkeleton_Details />
|
|
246
246
|
|
|
247
247
|
if (isNotFound) return <NotFound />
|
|
248
248
|
|
|
249
249
|
return (
|
|
250
250
|
<>
|
|
251
|
-
<DuplicateEntryChecker
|
|
252
|
-
formRef={formDataRef}
|
|
253
|
-
formId={formId}
|
|
254
|
-
formKey={formKey}
|
|
255
|
-
formDataId={formDataId}
|
|
256
|
-
duplicateCheckConfig={cachedConfig.detailsConfig.duplicateCheckConfig}
|
|
257
|
-
|
|
251
|
+
<DuplicateEntryChecker
|
|
252
|
+
formRef={formDataRef}
|
|
253
|
+
formId={formId}
|
|
254
|
+
formKey={formKey}
|
|
255
|
+
formDataId={formDataId}
|
|
256
|
+
duplicateCheckConfig={cachedConfig.detailsConfig.duplicateCheckConfig}
|
|
257
|
+
onCustomFunctionCall={onCustomFunctionCall}
|
|
258
|
+
/>
|
|
258
259
|
<Form
|
|
259
260
|
layout="vertical"
|
|
260
261
|
name="dynamic_form_data_form"
|
|
@@ -301,15 +302,15 @@ function FormDataDetailsComponentChild({
|
|
|
301
302
|
)
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
type IFormDataDetailsComponent = {
|
|
305
|
-
baseServerUrl?: string
|
|
306
|
-
companyKey?: string
|
|
307
|
-
formDataId: string
|
|
308
|
-
formName?: string
|
|
309
|
-
initialValues?: Record<string, any>
|
|
310
|
-
} & (IDataDetailsPublicProps | IDataDetailsPrivateProps) &
|
|
311
|
-
ICustomFunctionCall &
|
|
312
|
-
ICustomComponents
|
|
305
|
+
type IFormDataDetailsComponent = {
|
|
306
|
+
baseServerUrl?: string
|
|
307
|
+
companyKey?: string
|
|
308
|
+
formDataId: string
|
|
309
|
+
formName?: string
|
|
310
|
+
initialValues?: Record<string, any>
|
|
311
|
+
} & (IDataDetailsPublicProps | IDataDetailsPrivateProps) &
|
|
312
|
+
ICustomFunctionCall &
|
|
313
|
+
ICustomComponents
|
|
313
314
|
|
|
314
315
|
interface IDataDetailsPublicProps {
|
|
315
316
|
isPublic: true
|
|
@@ -513,13 +513,13 @@ export interface IDynamicButton_DisplayStateProps {
|
|
|
513
513
|
btnProps: IButtonElementProps
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
export interface ICustomFunctionCall {
|
|
517
|
-
onCustomFunctionCall?: (params: {
|
|
518
|
-
fnName: string
|
|
519
|
-
messages?: { success?: string; error?: string }
|
|
520
|
-
formData?: { [key: string]: any }
|
|
521
|
-
}) =>
|
|
522
|
-
}
|
|
516
|
+
export interface ICustomFunctionCall {
|
|
517
|
+
onCustomFunctionCall?: (params: {
|
|
518
|
+
fnName: string
|
|
519
|
+
messages?: { success?: string; error?: string }
|
|
520
|
+
formData?: { [key: string]: any }
|
|
521
|
+
}) => Promise<any> | any
|
|
522
|
+
}
|
|
523
523
|
export interface IOnSuccessFunctions {
|
|
524
524
|
onSuccess: (formDataId?: string) => void
|
|
525
525
|
onError: () => void
|
package/src/types/forms/index.ts
CHANGED
|
@@ -45,10 +45,11 @@ export interface IFormSchema {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export interface IDuplicateCheckConfig {
|
|
49
|
-
isEnabled?: boolean
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
export interface IDuplicateCheckConfig {
|
|
49
|
+
isEnabled?: boolean
|
|
50
|
+
functionName?: string
|
|
51
|
+
groups?: IDuplicateCheckConfigGroup[]
|
|
52
|
+
}
|
|
52
53
|
|
|
53
54
|
export interface ISubmissionTemplateReportConfig {
|
|
54
55
|
enabled?: boolean
|