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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-craft-package",
3
- "version": "1.9.9",
3
+ "version": "1.9.10",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -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
- <Form
197
- layout="vertical"
198
- name="dynamic_form_data_form"
199
- form={formDataRef}
200
- className={ELEMENTS_DEFAULT_CLASS.DataDetailsForm}
201
- onValuesChange={(_changed, allValues) => {
202
- handleDisabledSet(allValues, currentBreakpoint, cachedConfig?.detailsConfig, formDataId)
203
- handleHiddenSet(allValues, currentBreakpoint, cachedConfig?.detailsConfig, formDataId)
204
- }}
205
- >
206
- {layout.map((row, rowIdx) => (
207
- <LayoutRendererRow
208
- key={rowIdx}
209
- rowData={row}
210
- isTopLevel
211
- formContext={{
212
- detailPageFormId: formId,
213
- formId,
214
- formKey,
215
- formDataId,
216
- formRef: formDataRef,
217
- companyKey,
218
- }}
219
- elements={cachedConfig.detailsConfig.elements}
220
- renderButton={(btnProps) => (
221
- <DynamicFormButtonRender
222
- displayStateProps={btnProps as IDynamicButton_DisplayStateProps}
223
- formContext={{
224
- detailPageFormId: formId,
225
- formId,
226
- formKey,
227
- formDataId,
228
- formRef: formDataRef,
229
- companyKey,
230
- }}
231
- onCustomFunctionCall={onCustomFunctionCall}
232
- />
233
- )}
234
- />
235
- ))}
236
- </Form>
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 { isPublic = false, submissionPdfConfig } = useFormPreservedItemValues(formRef)
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: { elements: {}, layouts: { [DeviceBreakpointEnum.Default]: [] }, dataFetchConfig: { joins: [] } },
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'
@@ -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',
@@ -27,7 +27,10 @@ export interface IFormConfigDetails {
27
27
  }
28
28
  export interface IFormSchema {
29
29
  isRelationshipForm?: boolean
30
- detailsConfig: IDndLayoutStructure_Responsive & { dataFetchConfig?: IFormDataDetailsFetchConfig }
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
  }