form-craft-package 1.9.9 → 1.9.10
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 +88 -0
- package/src/components/common/custom-hooks/use-preserved-form-items.hook.ts +11 -0
- package/src/components/common/duplicate-entry-checker/blur-event-bus-provider.tsx +43 -0
- package/src/components/common/duplicate-entry-checker/duplicate-warning.modal.tsx +116 -0
- package/src/components/common/duplicate-entry-checker/index.tsx +34 -0
- package/src/components/form/1-list/index.tsx +10 -1
- package/src/components/form/2-details/index.tsx +61 -42
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +28 -4
- package/src/components/form/layout-renderer/3-element/2-field-element.tsx +8 -2
- package/src/constants.ts +8 -1
- package/src/enums/form.enum.ts +2 -0
- package/src/types/forms/index.ts +12 -1
package/package.json
CHANGED
|
@@ -0,0 +1,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
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useDuplicateOnBlur(
|
|
17
|
+
{ formRef, formKey, formDataId, groups, debounceMs = 0 }: IUseDuplicateOnBlur,
|
|
18
|
+
onResult: (isDuplicateFound: boolean) => void,
|
|
19
|
+
) {
|
|
20
|
+
const { subscribeBlurEvent: subscribe } = useBlurEventBus()
|
|
21
|
+
|
|
22
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
24
|
+
|
|
25
|
+
const relevantFieldGroups = useMemo(() => {
|
|
26
|
+
if (!Array.isArray(groups)) return []
|
|
27
|
+
|
|
28
|
+
return groups.map((group) => group.fields)
|
|
29
|
+
}, [groups])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!formKey) return
|
|
33
|
+
|
|
34
|
+
const unsub = subscribe((blurredField) => {
|
|
35
|
+
const fieldClone: string = Array.isArray(blurredField) ? blurredField.join('.') : (blurredField as string)
|
|
36
|
+
|
|
37
|
+
const fieldGroup = relevantFieldGroups.find((g) => g.includes(`Data.${fieldClone}`))
|
|
38
|
+
|
|
39
|
+
if (!fieldGroup) return
|
|
40
|
+
|
|
41
|
+
const formValues = formRef.getFieldsValue(true)
|
|
42
|
+
|
|
43
|
+
if (fieldGroup.some((f) => !formValues[f.replace('Data.', '')])) return
|
|
44
|
+
|
|
45
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
46
|
+
|
|
47
|
+
timerRef.current = setTimeout(async () => {
|
|
48
|
+
formRef.setFieldValue([FormPreservedItemKeys.DuplicateCheckPending], true)
|
|
49
|
+
|
|
50
|
+
// cancel previous request
|
|
51
|
+
if (abortRef.current) abortRef.current.abort()
|
|
52
|
+
abortRef.current = new AbortController()
|
|
53
|
+
|
|
54
|
+
const matchKeyValuePairs = {
|
|
55
|
+
DeletedDate: null,
|
|
56
|
+
...fieldGroup.reduce((acc, f) => ({ ...acc, [f]: formValues[f.replace('Data.', '')] }), {}),
|
|
57
|
+
}
|
|
58
|
+
const reqData = { project: JSON.stringify({ _id: '$_id' }), match: JSON.stringify(matchKeyValuePairs) }
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await apiClient.post(`/api/site/${formKey}/get`, reqData)
|
|
62
|
+
if (res.status === 200) {
|
|
63
|
+
const isRecordFound = res.data.data.filter(({ _id }: { _id: string }) => _id !== formDataId).length > 0
|
|
64
|
+
|
|
65
|
+
const matchFilterKeysOnly = Object.keys(matchKeyValuePairs).join('::')
|
|
66
|
+
|
|
67
|
+
// set the match filter with the result to display error upon submit button
|
|
68
|
+
formRef.setFieldsValue({
|
|
69
|
+
[FormPreservedItemKeys.DuplicateDataFound]: { [matchFilterKeysOnly]: isRecordFound },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
onResult(isRecordFound)
|
|
73
|
+
}
|
|
74
|
+
} catch (e: any) {
|
|
75
|
+
if (e?.name === 'AbortError') return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
formRef.setFieldValue([FormPreservedItemKeys.DuplicateCheckPending], false)
|
|
79
|
+
}, debounceMs)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
unsub()
|
|
84
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
85
|
+
if (abortRef.current) abortRef.current.abort()
|
|
86
|
+
}
|
|
87
|
+
}, [subscribe, relevantFieldGroups, formKey, formDataId, formRef, debounceMs])
|
|
88
|
+
}
|
|
@@ -11,6 +11,14 @@ export const useFormPreservedItemValues = (formRef?: FormInstance): IFormValues
|
|
|
11
11
|
form: formRef,
|
|
12
12
|
preserve: true,
|
|
13
13
|
})
|
|
14
|
+
const duplicateDataMatches = Form.useWatch(FormPreservedItemKeys.DuplicateDataFound, {
|
|
15
|
+
form: formRef,
|
|
16
|
+
preserve: true,
|
|
17
|
+
})
|
|
18
|
+
const isDuplicateCheckPending = Form.useWatch(FormPreservedItemKeys.DuplicateCheckPending, {
|
|
19
|
+
form: formRef,
|
|
20
|
+
preserve: true,
|
|
21
|
+
})
|
|
14
22
|
|
|
15
23
|
return {
|
|
16
24
|
[FormPreservedItemKeys.BaseServerUrl]: baseServerUrl,
|
|
@@ -19,6 +27,9 @@ export const useFormPreservedItemValues = (formRef?: FormInstance): IFormValues
|
|
|
19
27
|
[FormPreservedItemKeys.SignatureFields]: signatureFields,
|
|
20
28
|
[FormPreservedItemKeys.IsPublic]: isPublic,
|
|
21
29
|
[FormPreservedItemKeys.FormNotifications]: formNotifications,
|
|
30
|
+
[FormPreservedItemKeys.DuplicateDataFound]:
|
|
31
|
+
duplicateDataMatches && Object.values(duplicateDataMatches).some((result) => result),
|
|
32
|
+
[FormPreservedItemKeys.DuplicateCheckPending]: isDuplicateCheckPending,
|
|
22
33
|
}
|
|
23
34
|
}
|
|
24
35
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// blur-event-bus.tsx
|
|
2
|
+
import React, { createContext, useContext, useRef, useCallback } from 'react'
|
|
3
|
+
|
|
4
|
+
type BlurEventPayload = string | (string | number)[]
|
|
5
|
+
|
|
6
|
+
type Subscriber = (payload: BlurEventPayload) => void
|
|
7
|
+
|
|
8
|
+
const BlurEventBusContext = createContext<{
|
|
9
|
+
publishBlurEvent: (payload: BlurEventPayload) => void
|
|
10
|
+
subscribeBlurEvent: (fn: Subscriber) => () => void
|
|
11
|
+
} | null>(null)
|
|
12
|
+
|
|
13
|
+
export const BlurEventBusProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
14
|
+
const subsRef = useRef(new Set<Subscriber>())
|
|
15
|
+
|
|
16
|
+
const publishBlurEvent = useCallback((payload: BlurEventPayload) => {
|
|
17
|
+
subsRef.current.forEach((fn) => fn(payload))
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const subscribeBlurEvent = useCallback((fn: Subscriber) => {
|
|
21
|
+
subsRef.current.add(fn)
|
|
22
|
+
return () => subsRef.current.delete(fn)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<BlurEventBusContext.Provider value={{ publishBlurEvent, subscribeBlurEvent }}>
|
|
27
|
+
{children}
|
|
28
|
+
</BlurEventBusContext.Provider>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useBlurEventBus = () => {
|
|
33
|
+
const ctx = useContext(BlurEventBusContext)
|
|
34
|
+
if (!ctx) throw new Error('useBlurEventBus must be used inside <BlurBusProvider>')
|
|
35
|
+
return ctx
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useBlurEventPublisher(field: string | (string | number)[]) {
|
|
39
|
+
const { publishBlurEvent } = useBlurEventBus()
|
|
40
|
+
return {
|
|
41
|
+
onBlur: () => publishBlurEvent(field),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Divider, FormInstance, Modal } from 'antd'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { useTranslation } from '../custom-hooks'
|
|
4
|
+
import { Button_FillerPortal } from '../button'
|
|
5
|
+
import { FormPreservedItemKeys, TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../enums'
|
|
6
|
+
import { FaExclamationTriangle } from 'react-icons/fa'
|
|
7
|
+
import { useNavigate } from 'react-router-dom'
|
|
8
|
+
import { DUPLICATE_CHECK_TRANSLATION_KEY } from '../../../constants'
|
|
9
|
+
|
|
10
|
+
export default function DuplicateWarningModal({ formRef, formId, closeModal }: IDuplicateWarningModal) {
|
|
11
|
+
const navigate = useNavigate()
|
|
12
|
+
const { t } = useTranslation(formId)
|
|
13
|
+
|
|
14
|
+
const [title, primaryMessage, secondaryMessage, okText, cancelText] = useMemo(
|
|
15
|
+
() =>
|
|
16
|
+
t([
|
|
17
|
+
{
|
|
18
|
+
key: DUPLICATE_CHECK_TRANSLATION_KEY,
|
|
19
|
+
type: TranslationTextTypeEnum.Label,
|
|
20
|
+
subType: TranslationTextSubTypeEnum.ConfirmationTitle,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: DUPLICATE_CHECK_TRANSLATION_KEY,
|
|
24
|
+
type: TranslationTextTypeEnum.Label,
|
|
25
|
+
subType: TranslationTextSubTypeEnum.ConfirmationMsg,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: DUPLICATE_CHECK_TRANSLATION_KEY,
|
|
29
|
+
type: TranslationTextTypeEnum.Description,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: DUPLICATE_CHECK_TRANSLATION_KEY,
|
|
33
|
+
type: TranslationTextTypeEnum.Label,
|
|
34
|
+
subType: TranslationTextSubTypeEnum.ConfirmationOk,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: DUPLICATE_CHECK_TRANSLATION_KEY,
|
|
38
|
+
type: TranslationTextTypeEnum.Label,
|
|
39
|
+
subType: TranslationTextSubTypeEnum.ConfirmationCancel,
|
|
40
|
+
},
|
|
41
|
+
]),
|
|
42
|
+
[],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const fieldLabels: string[] = useMemo(() => {
|
|
46
|
+
const duplicateDataKeys = formRef?.getFieldValue(FormPreservedItemKeys.DuplicateDataFound)
|
|
47
|
+
if (!duplicateDataKeys) return []
|
|
48
|
+
|
|
49
|
+
const matchFilterKeys = Object.entries(duplicateDataKeys).reduce((acc: string[], [matchKeysString, result]) => {
|
|
50
|
+
if (acc.length || !result) return acc
|
|
51
|
+
|
|
52
|
+
return matchKeysString.replaceAll('Data.', '').split('::')
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
return t(
|
|
56
|
+
matchFilterKeys
|
|
57
|
+
.filter((key) => key !== 'DeletedDate')
|
|
58
|
+
.map((key) => ({ key, type: TranslationTextTypeEnum.Label })),
|
|
59
|
+
) as string[]
|
|
60
|
+
}, [formRef])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Modal
|
|
64
|
+
open
|
|
65
|
+
title={
|
|
66
|
+
<div className="flex items-center gap-2 text-warning font-normal">
|
|
67
|
+
<FaExclamationTriangle className="text-warning" />
|
|
68
|
+
{title || 'Duplicate Entry Found'}
|
|
69
|
+
</div>
|
|
70
|
+
}
|
|
71
|
+
width={600}
|
|
72
|
+
maskClosable={false}
|
|
73
|
+
closable={false}
|
|
74
|
+
footer={
|
|
75
|
+
<div className="grid grid-cols-[max-content_max-content] gap-2 justify-between">
|
|
76
|
+
<Button_FillerPortal
|
|
77
|
+
danger
|
|
78
|
+
outline
|
|
79
|
+
onClick={() => {
|
|
80
|
+
navigate(-1)
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{cancelText || 'Cancel and Discard Entry'}
|
|
84
|
+
</Button_FillerPortal>
|
|
85
|
+
<Button_FillerPortal primary outline onClick={closeModal}>
|
|
86
|
+
{okText || 'Continue Editing Entry'}
|
|
87
|
+
</Button_FillerPortal>
|
|
88
|
+
</div>
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
<div className="flex flex-col items-center gap-2">
|
|
92
|
+
<div className="text-16 text-center">
|
|
93
|
+
{primaryMessage || 'We’ve found an existing record with the same details.'}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="text-gray-500 text-[15px] text-center">
|
|
96
|
+
{secondaryMessage || 'Duplicate entries are not allowed in the system.'}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{fieldLabels.length > 0 && (
|
|
100
|
+
<>
|
|
101
|
+
<Divider />
|
|
102
|
+
<div className="bg-background w-full rounded-md p-2 flex flex-col gap-1">
|
|
103
|
+
<span className="font-bold text-primary">Duplicate data found on the following field(s):</span>
|
|
104
|
+
<div className="flex gap-2">{fieldLabels.join(', ')}</div>
|
|
105
|
+
</div>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</Modal>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface IDuplicateWarningModal {
|
|
113
|
+
formRef?: FormInstance
|
|
114
|
+
formId?: number
|
|
115
|
+
closeModal: () => void
|
|
116
|
+
}
|
|
@@ -0,0 +1,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 } 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
|
+
}: IDuplicateEntryChecker) {
|
|
16
|
+
const { isModalOpen, openModal, closeModal } = useLazyModalOpener()
|
|
17
|
+
|
|
18
|
+
useDuplicateOnBlur({ formRef, formKey, formDataId, groups: duplicateCheckConfig?.groups }, (isDuplicateFound) => {
|
|
19
|
+
if (isDuplicateFound) openModal()
|
|
20
|
+
else closeModal()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (!isModalOpen) return
|
|
24
|
+
|
|
25
|
+
return <DuplicateWarningModal formRef={formRef} formId={formId} closeModal={closeModal} />
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IDuplicateEntryChecker {
|
|
29
|
+
formRef: FormInstance
|
|
30
|
+
formId?: number
|
|
31
|
+
formKey?: string
|
|
32
|
+
formDataId: string
|
|
33
|
+
duplicateCheckConfig?: IDuplicateCheckConfig
|
|
34
|
+
}
|
|
@@ -8,8 +8,17 @@ import { getProjectionKey, mergeJoins } from '../../../functions'
|
|
|
8
8
|
import { useCacheFormLayoutConfig } from '../../common/custom-hooks/use-cache-form-layout-config.hook'
|
|
9
9
|
import { useNotification } from '../../common/custom-hooks'
|
|
10
10
|
import { translationStore } from '../../common/custom-hooks/use-translation.hook/store'
|
|
11
|
+
import { BlurEventBusProvider } from '../../common/duplicate-entry-checker/blur-event-bus-provider'
|
|
11
12
|
|
|
12
|
-
export function FormDataListComponent({
|
|
13
|
+
export function FormDataListComponent(props: IFormDataListComponent) {
|
|
14
|
+
return (
|
|
15
|
+
<BlurEventBusProvider>
|
|
16
|
+
<FormDataListComponentChild {...props} />
|
|
17
|
+
</BlurEventBusProvider>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function FormDataListComponentChild({
|
|
13
22
|
formId,
|
|
14
23
|
companyKey,
|
|
15
24
|
baseServerUrl,
|
|
@@ -13,6 +13,8 @@ import { PageViewTypEnum, DeviceBreakpointEnum, FormPreservedItemKeys, LOCAL_STO
|
|
|
13
13
|
import { useCacheFormLayoutConfig } from '../../common/custom-hooks/use-cache-form-layout-config.hook'
|
|
14
14
|
import { translationStore } from '../../common/custom-hooks/use-translation.hook/store'
|
|
15
15
|
import { ELEMENTS_DEFAULT_CLASS } from '../../../constants'
|
|
16
|
+
import { BlurEventBusProvider } from '../../common/duplicate-entry-checker/blur-event-bus-provider'
|
|
17
|
+
import DuplicateEntryChecker from '../../common/duplicate-entry-checker'
|
|
16
18
|
import {
|
|
17
19
|
handleHiddenSet,
|
|
18
20
|
useSetHiddenNodes,
|
|
@@ -34,7 +36,15 @@ import {
|
|
|
34
36
|
queryParamsToObject,
|
|
35
37
|
} from '../../../functions/forms'
|
|
36
38
|
|
|
37
|
-
export default function FormDataDetailsComponent({
|
|
39
|
+
export default function FormDataDetailsComponent(props: IFormDataDetailsComponent) {
|
|
40
|
+
return (
|
|
41
|
+
<BlurEventBusProvider>
|
|
42
|
+
<FormDataDetailsComponentChild {...props} />
|
|
43
|
+
</BlurEventBusProvider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function FormDataDetailsComponentChild({
|
|
38
48
|
isPublic,
|
|
39
49
|
formId,
|
|
40
50
|
formKey,
|
|
@@ -193,47 +203,56 @@ export default function FormDataDetailsComponent({
|
|
|
193
203
|
if (isNotFound) return <NotFound />
|
|
194
204
|
|
|
195
205
|
return (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
206
|
+
<>
|
|
207
|
+
<DuplicateEntryChecker
|
|
208
|
+
formRef={formDataRef}
|
|
209
|
+
formId={formId}
|
|
210
|
+
formKey={formKey}
|
|
211
|
+
formDataId={formDataId}
|
|
212
|
+
duplicateCheckConfig={cachedConfig.detailsConfig.duplicateCheckConfig}
|
|
213
|
+
/>
|
|
214
|
+
<Form
|
|
215
|
+
layout="vertical"
|
|
216
|
+
name="dynamic_form_data_form"
|
|
217
|
+
form={formDataRef}
|
|
218
|
+
className={ELEMENTS_DEFAULT_CLASS.DataDetailsForm}
|
|
219
|
+
onValuesChange={(_changed, allValues) => {
|
|
220
|
+
handleDisabledSet(allValues, currentBreakpoint, cachedConfig?.detailsConfig, formDataId)
|
|
221
|
+
handleHiddenSet(allValues, currentBreakpoint, cachedConfig?.detailsConfig, formDataId)
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
{layout.map((row, rowIdx) => (
|
|
225
|
+
<LayoutRendererRow
|
|
226
|
+
key={rowIdx}
|
|
227
|
+
rowData={row}
|
|
228
|
+
isTopLevel
|
|
229
|
+
formContext={{
|
|
230
|
+
detailPageFormId: formId,
|
|
231
|
+
formId,
|
|
232
|
+
formKey,
|
|
233
|
+
formDataId,
|
|
234
|
+
formRef: formDataRef,
|
|
235
|
+
companyKey,
|
|
236
|
+
}}
|
|
237
|
+
elements={cachedConfig.detailsConfig.elements}
|
|
238
|
+
renderButton={(btnProps) => (
|
|
239
|
+
<DynamicFormButtonRender
|
|
240
|
+
displayStateProps={btnProps as IDynamicButton_DisplayStateProps}
|
|
241
|
+
formContext={{
|
|
242
|
+
detailPageFormId: formId,
|
|
243
|
+
formId,
|
|
244
|
+
formKey,
|
|
245
|
+
formDataId,
|
|
246
|
+
formRef: formDataRef,
|
|
247
|
+
companyKey,
|
|
248
|
+
}}
|
|
249
|
+
onCustomFunctionCall={onCustomFunctionCall}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
/>
|
|
253
|
+
))}
|
|
254
|
+
</Form>
|
|
255
|
+
</>
|
|
237
256
|
)
|
|
238
257
|
}
|
|
239
258
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useFindDynamiForm, useNotification } from '../../../../common/custom-hooks'
|
|
2
|
-
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { useFindDynamiForm, useLazyModalOpener, useNotification } from '../../../../common/custom-hooks'
|
|
2
|
+
import { lazy, memo, useCallback, useMemo, useRef, useState } from 'react'
|
|
3
3
|
import { useFormPreservedItemValues } from '../../../../common/custom-hooks/use-preserved-form-items.hook'
|
|
4
4
|
import { useDuplicateDataAction } from './use-duplicate-data.hook'
|
|
5
5
|
import { useDeleteDataAction } from './use-delete-data.hook'
|
|
@@ -36,16 +36,24 @@ import {
|
|
|
36
36
|
TranslationTextTypeEnum,
|
|
37
37
|
} from '../../../../../enums'
|
|
38
38
|
|
|
39
|
+
const DuplicateWarningModal = lazy(() => import('../../../../common/duplicate-entry-checker/duplicate-warning.modal'))
|
|
40
|
+
|
|
39
41
|
export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
40
42
|
const { displayStateProps = {}, formContext = {}, onCustomFunctionCall = () => {} } = props
|
|
41
43
|
const { btnProps, btnKey } = displayStateProps as IDynamicButton_DisplayStateProps
|
|
42
44
|
const { formDataId, formRef } = formContext as IFormContext
|
|
45
|
+
const duplicateCheckModal = useLazyModalOpener()
|
|
43
46
|
|
|
44
47
|
const { getFormById } = useFindDynamiForm()
|
|
45
48
|
const { success, warning, error, confirmModal } = useNotification()
|
|
46
49
|
const [loading, setLoading] = useState(false) // disables button
|
|
47
50
|
const [dataLoadingType, setDataLoadingType] = useState<FormLoadingModalTypeEnum | undefined>() // displays loading modal
|
|
48
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
isPublic = false,
|
|
53
|
+
submissionPdfConfig,
|
|
54
|
+
isDuplicateDataFound,
|
|
55
|
+
isDuplicateCheckPending,
|
|
56
|
+
} = useFormPreservedItemValues(formRef)
|
|
49
57
|
|
|
50
58
|
const { t } = useTranslation(formContext.formId)
|
|
51
59
|
const isElementHidden = useIsNodeHidden(btnKey)
|
|
@@ -283,6 +291,7 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
283
291
|
formDataId,
|
|
284
292
|
isPublic,
|
|
285
293
|
submissionPdfConfig,
|
|
294
|
+
isDuplicateDataFound,
|
|
286
295
|
onFunctionCall,
|
|
287
296
|
onDuplicateData,
|
|
288
297
|
onDeleteData,
|
|
@@ -300,10 +309,18 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
300
309
|
<>
|
|
301
310
|
<Button_FillerPortal
|
|
302
311
|
{...getButtonRenderProps(btnProps)}
|
|
303
|
-
loading={loading}
|
|
312
|
+
loading={loading || duplicateCheckModal.isPendingTransition || isDuplicateCheckPending}
|
|
304
313
|
disabled={isElementDisabled}
|
|
305
314
|
className={`${ELEMENTS_DEFAULT_CLASS.Button} w-full ${isElementHidden ? 'hidden' : ''}`}
|
|
306
315
|
onClick={(e) => {
|
|
316
|
+
if (
|
|
317
|
+
btnProps.primaryAction.category === ButtonActionCategoryEnum.SaveDataChanges &&
|
|
318
|
+
(isDuplicateDataFound || isDuplicateCheckPending)
|
|
319
|
+
) {
|
|
320
|
+
duplicateCheckModal.openModal()
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
307
324
|
if (
|
|
308
325
|
isPublic &&
|
|
309
326
|
isNewFormDataPage(formDataId) &&
|
|
@@ -346,6 +363,13 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
346
363
|
/>
|
|
347
364
|
)}
|
|
348
365
|
{ChooseTemplateReportModal}
|
|
366
|
+
{duplicateCheckModal.isModalOpen && (
|
|
367
|
+
<DuplicateWarningModal
|
|
368
|
+
formRef={formContext.detailPageFormRef || formContext.formRef}
|
|
369
|
+
formId={formContext.detailPageFormId}
|
|
370
|
+
closeModal={duplicateCheckModal.closeModal}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
349
373
|
</>
|
|
350
374
|
)
|
|
351
375
|
})
|
|
@@ -9,6 +9,7 @@ import { IDataSanitization, IValidationRule } from '../../../../types'
|
|
|
9
9
|
import CurrencyField from '../../../common/currency-field'
|
|
10
10
|
import { ELEMENTS_DEFAULT_CLASS } from '../../../../constants'
|
|
11
11
|
import { Checkbox, Form, Input, Select, Radio, InputNumber, DatePicker, Modal, Switch, FormInstance } from 'antd'
|
|
12
|
+
import { useBlurEventPublisher } from '../../../common/duplicate-entry-checker/blur-event-bus-provider'
|
|
12
13
|
|
|
13
14
|
interface ILayoutRenderer_FieldElement {
|
|
14
15
|
children: () => IMapperFieldObj | IMapperFieldObj[]
|
|
@@ -58,6 +59,7 @@ const FieldFormItemWrapper = ({
|
|
|
58
59
|
formId?: number
|
|
59
60
|
}) => {
|
|
60
61
|
const { name, nameFullPath, label, hasNoLabel, type, validations, sanitization, disabled } = field
|
|
62
|
+
const { onBlur } = useBlurEventPublisher(name)
|
|
61
63
|
|
|
62
64
|
return (
|
|
63
65
|
<Fragment key={fieldIdx}>
|
|
@@ -82,7 +84,7 @@ const FieldFormItemWrapper = ({
|
|
|
82
84
|
{type === ElementTypeEnum.ColorPicker ? (
|
|
83
85
|
<CustomColorField formRef={formRef} name={nameFullPath} disabled={disabled} />
|
|
84
86
|
) : (
|
|
85
|
-
getField(field)
|
|
87
|
+
getField({ ...field, onBlur })
|
|
86
88
|
)}
|
|
87
89
|
</Form.Item>
|
|
88
90
|
{disabled && type !== ElementTypeEnum.Checkbox && <DisabledFieldIndicator />}
|
|
@@ -239,13 +241,16 @@ const fieldRenderers: Partial<Record<ElementTypeEnum, FieldRenderer>> = {
|
|
|
239
241
|
allowClear
|
|
240
242
|
/>
|
|
241
243
|
),
|
|
242
|
-
[ElementTypeEnum.ShortInput]: ({ placeholder, disabled, name }) => (
|
|
244
|
+
[ElementTypeEnum.ShortInput]: ({ placeholder, disabled, name, onBlur }) => (
|
|
243
245
|
<Input
|
|
244
246
|
id={name as string}
|
|
245
247
|
className={ELEMENTS_DEFAULT_CLASS.Input}
|
|
246
248
|
placeholder={placeholder as string}
|
|
247
249
|
disabled={disabled}
|
|
248
250
|
autoComplete="off"
|
|
251
|
+
onBlur={(e) => {
|
|
252
|
+
onBlur?.(e.currentTarget.value)
|
|
253
|
+
}}
|
|
249
254
|
/>
|
|
250
255
|
),
|
|
251
256
|
}
|
|
@@ -279,6 +284,7 @@ interface IMapperFieldObj {
|
|
|
279
284
|
decimalPoint?: number
|
|
280
285
|
mode?: 'multiple' | 'tags'
|
|
281
286
|
disabledDate?: (date: Dayjs) => boolean
|
|
287
|
+
onBlur?: (currentValue: any) => void
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
interface ISelectOption {
|
package/src/constants.ts
CHANGED
|
@@ -175,7 +175,12 @@ export const BSON_DATA_IDENTIFIER_PREFIXES = {
|
|
|
175
175
|
Number: 'Number__',
|
|
176
176
|
}
|
|
177
177
|
export const DEFAULT_FORM_SCHEMA_DATA: IFormSchema = {
|
|
178
|
-
detailsConfig: {
|
|
178
|
+
detailsConfig: {
|
|
179
|
+
elements: {},
|
|
180
|
+
layouts: { [DeviceBreakpointEnum.Default]: [] },
|
|
181
|
+
dataFetchConfig: { joins: [] },
|
|
182
|
+
duplicateCheckConfig: { isEnabled: false },
|
|
183
|
+
},
|
|
179
184
|
dataListConfig: {
|
|
180
185
|
listType: FormDataListViewTypeEnum.Table,
|
|
181
186
|
columns: [],
|
|
@@ -184,6 +189,7 @@ export const DEFAULT_FORM_SCHEMA_DATA: IFormSchema = {
|
|
|
184
189
|
generateConfig: { submissionPdf: { enabled: false } },
|
|
185
190
|
relationships: [],
|
|
186
191
|
translations: {},
|
|
192
|
+
notificationsConfig: { notifications: [] },
|
|
187
193
|
}
|
|
188
194
|
export const REACT_QUERY_CLIENT = new QueryClient({
|
|
189
195
|
defaultOptions: {
|
|
@@ -227,3 +233,4 @@ export const ELEMENTS_DEFAULT_CLASS = {
|
|
|
227
233
|
Button: 'fc-button',
|
|
228
234
|
AutoComplete: 'fc-auto-complete',
|
|
229
235
|
}
|
|
236
|
+
export const DUPLICATE_CHECK_TRANSLATION_KEY = 'DuplicateCheck'
|
package/src/enums/form.enum.ts
CHANGED
|
@@ -140,6 +140,8 @@ export enum FormPreservedItemKeys {
|
|
|
140
140
|
IsPublic = 'isPublic',
|
|
141
141
|
IsDetailsDataSet = 'isDetailsDataSet',
|
|
142
142
|
FormNotifications = 'formNotifications',
|
|
143
|
+
DuplicateDataFound = 'isDuplicateDataFound',
|
|
144
|
+
DuplicateCheckPending = 'isDuplicateCheckPending',
|
|
143
145
|
}
|
|
144
146
|
export enum DataCategoryEnum {
|
|
145
147
|
Number = 'Number',
|
package/src/types/forms/index.ts
CHANGED
|
@@ -27,7 +27,10 @@ export interface IFormConfigDetails {
|
|
|
27
27
|
}
|
|
28
28
|
export interface IFormSchema {
|
|
29
29
|
isRelationshipForm?: boolean
|
|
30
|
-
detailsConfig: IDndLayoutStructure_Responsive & {
|
|
30
|
+
detailsConfig: IDndLayoutStructure_Responsive & {
|
|
31
|
+
dataFetchConfig?: IFormDataDetailsFetchConfig
|
|
32
|
+
duplicateCheckConfig?: IDuplicateCheckConfig
|
|
33
|
+
}
|
|
31
34
|
dataListConfig: IFormDataListConfig
|
|
32
35
|
generateConfig: IFormGenerateConfig
|
|
33
36
|
relationships?: IFormRelationshipConfig[]
|
|
@@ -38,6 +41,14 @@ export interface IFormSchema {
|
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
export interface IDuplicateCheckConfig {
|
|
45
|
+
isEnabled?: boolean
|
|
46
|
+
groups?: IDuplicateCheckConfigGroup[]
|
|
47
|
+
}
|
|
48
|
+
export interface IDuplicateCheckConfigGroup {
|
|
49
|
+
fields: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export interface IFormDataDetailsFetchConfig {
|
|
42
53
|
joins?: IFormJoin[]
|
|
43
54
|
}
|