form-craft-package 1.9.10 → 1.10.0
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-preserved-form-items.hook.ts +5 -0
- package/src/components/form/1-list/table-header.tsx +3 -2
- package/src/components/form/2-details/index.tsx +13 -2
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/index.tsx +24 -13
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx +62 -104
- package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-send-notification.hook.ts +241 -25
- package/src/constants.ts +7 -1
- package/src/enums/form.enum.ts +2 -0
- package/src/enums/index.ts +1 -1
- package/src/functions/forms/extended-json-handlers.ts +20 -10
- package/src/functions/forms/index.ts +64 -0
- package/src/types/forms/generate/index.ts +1 -1
- package/src/types/forms/index.ts +8 -10
- package/src/types/forms/layout-elements/button.ts +2 -1
- package/src/types/forms/layout-elements/data-render-config.ts +5 -0
- package/src/types/notifications/index.ts +1 -1
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ export const useFormPreservedItemValues = (formRef?: FormInstance): IFormValues
|
|
|
7
7
|
const signatureFields = Form.useWatch(FormPreservedItemKeys.SignatureFields, { form: formRef, preserve: true })
|
|
8
8
|
const isPublic = Form.useWatch(FormPreservedItemKeys.IsPublic, { form: formRef, preserve: true })
|
|
9
9
|
const formNotifications = Form.useWatch(FormPreservedItemKeys.FormNotifications, { form: formRef, preserve: true })
|
|
10
|
+
const formTemplateReports = Form.useWatch(FormPreservedItemKeys.FormTemplateReports, {
|
|
11
|
+
form: formRef,
|
|
12
|
+
preserve: true,
|
|
13
|
+
})
|
|
10
14
|
const submissionPdfConfig = Form.useWatch(FormPreservedItemKeys.SubmissionPdfConfig, {
|
|
11
15
|
form: formRef,
|
|
12
16
|
preserve: true,
|
|
@@ -27,6 +31,7 @@ export const useFormPreservedItemValues = (formRef?: FormInstance): IFormValues
|
|
|
27
31
|
[FormPreservedItemKeys.SignatureFields]: signatureFields,
|
|
28
32
|
[FormPreservedItemKeys.IsPublic]: isPublic,
|
|
29
33
|
[FormPreservedItemKeys.FormNotifications]: formNotifications,
|
|
34
|
+
[FormPreservedItemKeys.FormTemplateReports]: formTemplateReports,
|
|
30
35
|
[FormPreservedItemKeys.DuplicateDataFound]:
|
|
31
36
|
duplicateDataMatches && Object.values(duplicateDataMatches).some((result) => result),
|
|
32
37
|
[FormPreservedItemKeys.DuplicateCheckPending]: isDuplicateCheckPending,
|
|
@@ -9,6 +9,7 @@ import { IDataListHeaderLayoutContext } from './table'
|
|
|
9
9
|
import {
|
|
10
10
|
BSON_DATA_IDENTIFIER_PREFIXES,
|
|
11
11
|
ELEMENTS_DEFAULT_CLASS,
|
|
12
|
+
MongoDbExtendedJsonObjectKeys,
|
|
12
13
|
VALUE_REPLACEMENT_PLACEHOLDER,
|
|
13
14
|
VALUE_REPLACEMENT_PLACEHOLDER2,
|
|
14
15
|
} from '../../../constants'
|
|
@@ -179,9 +180,9 @@ const handleFilterValues = async (config: IFilterConfig, value: any) => {
|
|
|
179
180
|
: dayjs.utc().endOf('day').toISOString()
|
|
180
181
|
} else value = dayjs.utc(value as Date).toISOString()
|
|
181
182
|
} else if ((config as IFilterCustom).bsonDataType === BSON_DATA_IDENTIFIER_PREFIXES.Number) {
|
|
182
|
-
value = {
|
|
183
|
+
value = { [MongoDbExtendedJsonObjectKeys.Number]: value }
|
|
183
184
|
} else if ((config as IFilterCustom).bsonDataType === BSON_DATA_IDENTIFIER_PREFIXES.ObjectId) {
|
|
184
|
-
value = {
|
|
185
|
+
value = { [MongoDbExtendedJsonObjectKeys.ObjectId]: value }
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
if (config.type === FilterConfigTypeEnum.Custom && (config as IFilterCustom).config) {
|
|
@@ -160,6 +160,7 @@ function FormDataDetailsComponentChild({
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
useEffect(() => {
|
|
163
|
+
// set form notifications
|
|
163
164
|
if (!cachedConfig?.notificationsConfig || !Array.isArray(cachedConfig.notificationsConfig.notifications)) return
|
|
164
165
|
|
|
165
166
|
formDataRef.setFieldValue(FormPreservedItemKeys.FormNotifications, {
|
|
@@ -168,6 +169,16 @@ function FormDataDetailsComponentChild({
|
|
|
168
169
|
}, [cachedConfig])
|
|
169
170
|
|
|
170
171
|
useEffect(() => {
|
|
172
|
+
// set form template reports
|
|
173
|
+
if (!cachedConfig?.templateReportConfig || !Array.isArray(cachedConfig.templateReportConfig.templates)) return
|
|
174
|
+
|
|
175
|
+
formDataRef.setFieldValue(FormPreservedItemKeys.FormTemplateReports, {
|
|
176
|
+
[cachedConfig.id]: cachedConfig.templateReportConfig,
|
|
177
|
+
})
|
|
178
|
+
}, [cachedConfig])
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
// set form text translations
|
|
171
182
|
if (!cachedConfig?.id || !cachedConfig?.translations) return
|
|
172
183
|
|
|
173
184
|
translationStore.setTranslations(cachedConfig.id, cachedConfig.translations)
|
|
@@ -180,8 +191,8 @@ function FormDataDetailsComponentChild({
|
|
|
180
191
|
originalTzFieldsRef.current = getPickerFieldsWithOriginalTz(elements)
|
|
181
192
|
|
|
182
193
|
if (isPublic) {
|
|
183
|
-
if (cachedConfig.
|
|
184
|
-
formDataRef.setFieldValue(FormPreservedItemKeys.SubmissionPdfConfig, cachedConfig.
|
|
194
|
+
if (cachedConfig.submissionPdfConfig?.enabled)
|
|
195
|
+
formDataRef.setFieldValue(FormPreservedItemKeys.SubmissionPdfConfig, cachedConfig.submissionPdfConfig)
|
|
185
196
|
formDataRef.setFieldValue(FormPreservedItemKeys.IsDetailsDataSet, true)
|
|
186
197
|
} else fetchFormData(formId, cachedConfig.detailsConfig.dataFetchConfig)
|
|
187
198
|
}, [cachedConfig, formId, isPublic, formDataRef, fetchFormData])
|
|
@@ -114,9 +114,15 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
114
114
|
},
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
(
|
|
119
|
-
|
|
117
|
+
const handleFollowUpAction = useCallback(
|
|
118
|
+
({
|
|
119
|
+
actionLevel = 'secondaryAction',
|
|
120
|
+
formDataId,
|
|
121
|
+
}: {
|
|
122
|
+
actionLevel?: 'secondaryAction' | 'tertiaryAction'
|
|
123
|
+
formDataId?: string
|
|
124
|
+
}) => {
|
|
125
|
+
if (!btnProps[actionLevel] || Object.values(btnProps[actionLevel]).length === 0) {
|
|
120
126
|
if (formInfo?.name && formDataId) {
|
|
121
127
|
const detailsUrl = `${constructDynamicFormHref(formInfo.name)}/${formDataId}`
|
|
122
128
|
|
|
@@ -129,36 +135,41 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
129
135
|
return
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
138
|
+
const hasTertiaryAction = actionLevel === 'secondaryAction' && !!btnProps.tertiaryAction
|
|
139
|
+
if (hasTertiaryAction) handleFollowUpAction({ actionLevel: 'tertiaryAction', formDataId })
|
|
140
|
+
|
|
141
|
+
const { category } = btnProps[actionLevel]
|
|
142
|
+
if (category === ButtonActionCategoryEnum.Navigate) onButtonNavigate(btnProps[actionLevel], btnProps.formId)
|
|
134
143
|
else if (category === ButtonActionCategoryEnum.CustomFunction) {
|
|
135
144
|
setLoading(false)
|
|
136
|
-
onFunctionCall({ functionName: btnProps.
|
|
137
|
-
} else if (category === ButtonActionCategoryEnum.SendNotification) onSendNotification(btnProps
|
|
145
|
+
onFunctionCall({ functionName: btnProps[actionLevel].functionName, newFormDataId: formDataId })
|
|
146
|
+
} else if (category === ButtonActionCategoryEnum.SendNotification) onSendNotification(btnProps[actionLevel])
|
|
138
147
|
else {
|
|
139
148
|
// fallback
|
|
149
|
+
if (hasTertiaryAction) return
|
|
150
|
+
|
|
140
151
|
setLoading(false)
|
|
141
152
|
setDataLoadingType(undefined)
|
|
142
153
|
}
|
|
143
154
|
},
|
|
144
|
-
[btnProps, onButtonNavigate, formInfo,
|
|
155
|
+
[btnProps, onButtonNavigate, formInfo, onSendNotification],
|
|
145
156
|
)
|
|
146
157
|
|
|
147
158
|
const onDuplicateData = useDuplicateDataAction({
|
|
148
159
|
...formContext,
|
|
149
|
-
onSuccess: () =>
|
|
160
|
+
onSuccess: () => handleFollowUpAction({}),
|
|
150
161
|
onError: () => displayResultMessage(false),
|
|
151
162
|
onFinal: () => setLoading(false),
|
|
152
163
|
})
|
|
153
164
|
const onDeleteData = useDeleteDataAction({
|
|
154
165
|
...formContext,
|
|
155
|
-
onSuccess: () =>
|
|
166
|
+
onSuccess: () => handleFollowUpAction({}),
|
|
156
167
|
onError: () => displayResultMessage(false),
|
|
157
168
|
onFinal: () => setLoading(false),
|
|
158
169
|
})
|
|
159
170
|
const onPublishData = usePublishDataAction({
|
|
160
171
|
...formContext,
|
|
161
|
-
onSuccess: () =>
|
|
172
|
+
onSuccess: () => handleFollowUpAction({}),
|
|
162
173
|
onError: () => displayResultMessage(false),
|
|
163
174
|
onFinal: () => setLoading(false),
|
|
164
175
|
})
|
|
@@ -169,7 +180,7 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
169
180
|
setDataLoadingType(undefined)
|
|
170
181
|
setLoading(false)
|
|
171
182
|
},
|
|
172
|
-
onSuccess: (formDataId?: string) =>
|
|
183
|
+
onSuccess: (formDataId?: string) => handleFollowUpAction({ formDataId }),
|
|
173
184
|
onError: () => {
|
|
174
185
|
setDataLoadingType(undefined)
|
|
175
186
|
displayResultMessage(false)
|
|
@@ -181,7 +192,7 @@ export const DynamicFormButtonRender = memo((props: IDynamicButton) => {
|
|
|
181
192
|
setDataLoadingType,
|
|
182
193
|
onSuccess: () => {
|
|
183
194
|
displayResultMessage()
|
|
184
|
-
|
|
195
|
+
handleFollowUpAction({})
|
|
185
196
|
},
|
|
186
197
|
onError: () => {
|
|
187
198
|
setDataLoadingType(undefined)
|
package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-generate-report.hook.tsx
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
2
|
import { Modal, Select, Spin } from 'antd'
|
|
3
3
|
import { FaCaretDown } from 'react-icons/fa6'
|
|
4
|
-
import { IDynamicForm,
|
|
4
|
+
import { IDynamicForm, IFormTemplateReport } from '../../../../../types'
|
|
5
5
|
import { Button_FillerPortal } from '../../../../common/button'
|
|
6
6
|
import { useNotification } from '../../../../common/custom-hooks'
|
|
7
|
-
import {
|
|
7
|
+
import { replaceAndGetFileBlob } from '../../../../../functions/forms'
|
|
8
8
|
import client from '../../../../../api/client'
|
|
9
|
-
import {
|
|
9
|
+
import { LOCAL_STORAGE_KEYS_ENUM } from '../../../../../enums'
|
|
10
10
|
import { IFormContext } from '../../1-row'
|
|
11
11
|
import { IOnSuccessFunctions } from '.'
|
|
12
|
+
import { useFormPreservedItemValues } from '../../../../common/custom-hooks/use-preserved-form-items.hook'
|
|
12
13
|
|
|
13
14
|
/* --------------------------------------------------------------------------
|
|
14
15
|
Generates a report by logging current form values.
|
|
@@ -17,6 +18,7 @@ export const useGenerateReportAction = ({
|
|
|
17
18
|
onSuccess,
|
|
18
19
|
onError,
|
|
19
20
|
onFinal,
|
|
21
|
+
formRef,
|
|
20
22
|
...formContext
|
|
21
23
|
}: IOnSuccessFunctions & IFormContext) => {
|
|
22
24
|
const { companyKey, formName, formDataId } = formContext
|
|
@@ -26,49 +28,72 @@ export const useGenerateReportAction = ({
|
|
|
26
28
|
const [templateReports, setTemplateReports] = useState<IFormTemplateReport[]>([])
|
|
27
29
|
const [loading, setLoading] = useState(false)
|
|
28
30
|
const [formData, setFormData] = useState<{ [key: string]: any }>({})
|
|
29
|
-
const
|
|
31
|
+
const { formTemplateReports } = useFormPreservedItemValues(formRef)
|
|
30
32
|
|
|
31
33
|
const onGenerateReport = useCallback(async () => {
|
|
32
34
|
setLoading(true)
|
|
33
35
|
setIsModalOpen(true)
|
|
34
36
|
try {
|
|
35
37
|
const storedData = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.DynamicForms)
|
|
36
|
-
if (storedData) {
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (!storedData) {
|
|
39
|
+
setLoading(false)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const parsedData: IDynamicForm[] = JSON.parse(storedData)
|
|
43
|
+
const form = parsedData.find((entry) => entry.name.toLowerCase() === formName?.toLowerCase())
|
|
39
44
|
|
|
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
|
-
.finally(() => setLoading(false))
|
|
65
|
-
} else setLoading(false)
|
|
66
|
-
} else setLoading(false)
|
|
45
|
+
if (!form) {
|
|
46
|
+
setLoading(false)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const { templates = [], joins = [] } = formTemplateReports?.[form.id] || { templates: [], joins: [] }
|
|
50
|
+
|
|
51
|
+
if (templates.length === 0) {
|
|
52
|
+
setLoading(false)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
client
|
|
57
|
+
.post(`/api/report/${form.id}/${formDataId}`, { joins })
|
|
58
|
+
.then((res) => {
|
|
59
|
+
if (res.status === 200) {
|
|
60
|
+
setFormData(res.data)
|
|
61
|
+
setTemplateReports(templates)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.finally(() => setLoading(false))
|
|
67
65
|
} catch (error) {
|
|
68
66
|
console.error('Error reading or parsing localStorage data', error)
|
|
69
67
|
setLoading(false)
|
|
70
68
|
}
|
|
71
|
-
}, [
|
|
69
|
+
}, [formTemplateReports, formName, formDataId])
|
|
70
|
+
|
|
71
|
+
const handleDataReplacement = useCallback(async () => {
|
|
72
|
+
setLoading(true)
|
|
73
|
+
const selectedTemplateInfo: IFormTemplateReport = templateReports.find(
|
|
74
|
+
(t: IFormTemplateReport) => t.fileBlobName === selectedTemplate,
|
|
75
|
+
)!
|
|
76
|
+
|
|
77
|
+
const replacedFileBlob = await replaceAndGetFileBlob(selectedTemplateInfo, formData, companyKey)
|
|
78
|
+
if (replacedFileBlob) {
|
|
79
|
+
const url = window.URL.createObjectURL(new Blob([replacedFileBlob]))
|
|
80
|
+
const link = document.createElement('a')
|
|
81
|
+
link.href = url
|
|
82
|
+
link.setAttribute(
|
|
83
|
+
'download',
|
|
84
|
+
selectedTemplateInfo.templateName.includes('.pdf')
|
|
85
|
+
? selectedTemplateInfo.templateName
|
|
86
|
+
: `${selectedTemplateInfo.templateName}.pdf`,
|
|
87
|
+
) //or any other extension
|
|
88
|
+
document.body.appendChild(link)
|
|
89
|
+
link.click()
|
|
90
|
+
onSuccess()
|
|
91
|
+
} else onError()
|
|
92
|
+
|
|
93
|
+
onFinal()
|
|
94
|
+
setLoading(false)
|
|
95
|
+
setIsModalOpen(false)
|
|
96
|
+
}, [templateReports, selectedTemplate])
|
|
72
97
|
|
|
73
98
|
const ChooseTemplateReportModal = isModalOpen ? (
|
|
74
99
|
<Modal
|
|
@@ -93,74 +118,7 @@ export const useGenerateReportAction = ({
|
|
|
93
118
|
return
|
|
94
119
|
}
|
|
95
120
|
|
|
96
|
-
|
|
97
|
-
const selectedTemplateInfo: IFormTemplateReport = templateReports.find(
|
|
98
|
-
(t: IFormTemplateReport) => t.fileBlobName === selectedTemplate,
|
|
99
|
-
)!
|
|
100
|
-
|
|
101
|
-
const replacements = await Promise.all(
|
|
102
|
-
selectedTemplateInfo.replacements.map(async (rep) => {
|
|
103
|
-
const value = rep.field.split('.').reduce((curr, n) => curr[n] ?? {}, formData)
|
|
104
|
-
|
|
105
|
-
if (!!rep.optionSource) {
|
|
106
|
-
if (rep.optionSource.type === FieldElementOptionSourceEnum.Static) {
|
|
107
|
-
return {
|
|
108
|
-
placeholder: rep.placeholder,
|
|
109
|
-
value: renderData(
|
|
110
|
-
rep.optionSource.options.find((op) => op.id === (value as unknown as string))?.value,
|
|
111
|
-
rep.renderConfig,
|
|
112
|
-
),
|
|
113
|
-
}
|
|
114
|
-
} else if (rep.optionSource.type === FieldElementOptionSourceEnum.DynamicForm) {
|
|
115
|
-
const formDataResData = await fetchFormDataAsLookup(rep.optionSource.baseFormId)
|
|
116
|
-
|
|
117
|
-
const field = rep.optionSource.optionRender?.fields?.[0].field
|
|
118
|
-
return {
|
|
119
|
-
placeholder: rep.placeholder,
|
|
120
|
-
value: renderData(
|
|
121
|
-
formDataResData.find((data) => data.id === (value as unknown as string))?.[
|
|
122
|
-
field ? `Data.${field}` : field
|
|
123
|
-
],
|
|
124
|
-
rep.renderConfig,
|
|
125
|
-
),
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
placeholder: rep.placeholder,
|
|
131
|
-
value: renderData(value, rep.renderConfig),
|
|
132
|
-
type: rep.type,
|
|
133
|
-
}
|
|
134
|
-
}),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
client
|
|
138
|
-
.post(
|
|
139
|
-
`/api/template/${companyKey}/${selectedTemplateInfo.fileBlobName}`,
|
|
140
|
-
replacements.map((r) => ({
|
|
141
|
-
key: r.placeholder,
|
|
142
|
-
value: r.value?.toString() ?? '-',
|
|
143
|
-
type: r.type,
|
|
144
|
-
})),
|
|
145
|
-
{ responseType: 'blob' },
|
|
146
|
-
)
|
|
147
|
-
.then((res) => {
|
|
148
|
-
if (res.status === 200) {
|
|
149
|
-
const url = window.URL.createObjectURL(new Blob([res.data]))
|
|
150
|
-
const link = document.createElement('a')
|
|
151
|
-
link.href = url
|
|
152
|
-
link.setAttribute('download', `${selectedTemplateInfo.templateName}.pdf`) //or any other extension
|
|
153
|
-
document.body.appendChild(link)
|
|
154
|
-
link.click()
|
|
155
|
-
onSuccess()
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
.catch(() => onError())
|
|
159
|
-
.finally(() => {
|
|
160
|
-
onFinal()
|
|
161
|
-
setLoading(false)
|
|
162
|
-
setIsModalOpen(false)
|
|
163
|
-
})
|
|
121
|
+
handleDataReplacement()
|
|
164
122
|
}}
|
|
165
123
|
>
|
|
166
124
|
Continue
|
package/src/components/form/layout-renderer/3-element/1-dynamic-button/use-send-notification.hook.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import { useCallback, useMemo } from 'react'
|
|
2
|
-
import { NotificationTypeEnum } from '../../../../../enums'
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
3
2
|
import client from '../../../../../api/client'
|
|
4
3
|
import { IFormContext } from '../../1-row'
|
|
5
4
|
import { IOnSuccessFunctions } from '.'
|
|
6
5
|
import { useNotification } from '../../../../common/custom-hooks'
|
|
7
|
-
import { IButtonProps_SendNotification, IEmail_Attachment, IFormJoin, IFormNotification } from '../../../../../types'
|
|
8
6
|
import { useFormPreservedItemValues } from '../../../../common/custom-hooks/use-preserved-form-items.hook'
|
|
9
|
-
import { REGEX_PATTERNS } from '../../../../../constants'
|
|
7
|
+
import { MongoDbExtendedJsonObjectKeys, REGEX_PATTERNS } from '../../../../../constants'
|
|
8
|
+
import { renderData, replaceAndGetFileBlob } from '../../../../../functions'
|
|
9
|
+
import {
|
|
10
|
+
CountryEnum,
|
|
11
|
+
DataRenderTypeEnum,
|
|
12
|
+
LOCAL_STORAGE_KEYS_ENUM,
|
|
13
|
+
NotificationTypeEnum,
|
|
14
|
+
TranslationTextTypeEnum,
|
|
15
|
+
} from '../../../../../enums'
|
|
16
|
+
import {
|
|
17
|
+
IButtonProps_SendNotification,
|
|
18
|
+
IEmail_Attachment,
|
|
19
|
+
IFormJoin,
|
|
20
|
+
IFormNotification,
|
|
21
|
+
IFormTemplateReport,
|
|
22
|
+
} from '../../../../../types'
|
|
10
23
|
|
|
11
24
|
/* --------------------------------------------------------------------------
|
|
12
25
|
Send notification: Email | Text | Push
|
|
@@ -16,20 +29,78 @@ export const useSendNotificationAction = ({
|
|
|
16
29
|
formRef,
|
|
17
30
|
formDataId,
|
|
18
31
|
formId,
|
|
32
|
+
companyKey,
|
|
19
33
|
onSuccess,
|
|
20
34
|
onError,
|
|
21
35
|
onFinal,
|
|
22
36
|
}: IOnSuccessFunctions & IFormContext) => {
|
|
23
|
-
const { warning } = useNotification()
|
|
24
|
-
const {
|
|
37
|
+
const { success, warning } = useNotification()
|
|
38
|
+
const {
|
|
39
|
+
baseServerUrl,
|
|
40
|
+
formNotifications: allNotifications,
|
|
41
|
+
formTemplateReports,
|
|
42
|
+
} = useFormPreservedItemValues(formRef)
|
|
43
|
+
const blobDataToDeleteRef = useRef<string[]>([])
|
|
44
|
+
|
|
45
|
+
const fileBaseUrl = useMemo(() => {
|
|
46
|
+
const domain = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.Domain)
|
|
47
|
+
return `${baseServerUrl}/api/attachment/${domain}`
|
|
48
|
+
}, [baseServerUrl])
|
|
25
49
|
|
|
26
50
|
const formNotifications: { joins: IFormJoin[]; notifications: IFormNotification[] } = useMemo(() => {
|
|
27
51
|
const defaultNotifs = { joins: [], notifications: [] }
|
|
28
52
|
|
|
29
|
-
if (!formId || !
|
|
53
|
+
if (!formId || !allNotifications || !allNotifications[formId]) return defaultNotifs
|
|
54
|
+
|
|
55
|
+
return allNotifications[formId]
|
|
56
|
+
}, [allNotifications, formId])
|
|
57
|
+
|
|
58
|
+
const handleTemplateReport = useCallback(
|
|
59
|
+
async (attachments: IEmail_Attachment[]): Promise<IEmail_Attachment[]> => {
|
|
60
|
+
try {
|
|
61
|
+
if (!formId) return []
|
|
62
|
+
|
|
63
|
+
const templateReports = formTemplateReports[formId]
|
|
64
|
+
|
|
65
|
+
const joinedFormData = await client
|
|
66
|
+
.post(`/api/report/${formId}/${formDataId}`, { joins: templateReports.joins })
|
|
67
|
+
.then((res) => (res.status === 200 ? res.data : {}))
|
|
68
|
+
.catch(() => ({}))
|
|
30
69
|
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
const dataReplacedFormAttachments: IEmail_Attachment[] = []
|
|
71
|
+
|
|
72
|
+
for (const attachment of attachments) {
|
|
73
|
+
const selectedTemplateInfo = templateReports.templates.find(
|
|
74
|
+
(tR: IFormTemplateReport) => tR.fileBlobName === attachment.blobName,
|
|
75
|
+
)
|
|
76
|
+
if (!selectedTemplateInfo) continue
|
|
77
|
+
|
|
78
|
+
const replacedFileBlob = await replaceAndGetFileBlob(selectedTemplateInfo, joinedFormData, companyKey)
|
|
79
|
+
|
|
80
|
+
if (replacedFileBlob) {
|
|
81
|
+
const formData = new FormData()
|
|
82
|
+
formData.append('file', replacedFileBlob)
|
|
83
|
+
const blobName = await client
|
|
84
|
+
.post('/api/attachment', formData)
|
|
85
|
+
.then((res) => (res.status === 200 ? res.data : ''))
|
|
86
|
+
.catch(() => '')
|
|
87
|
+
|
|
88
|
+
blobDataToDeleteRef.current = [...blobDataToDeleteRef.current, blobName]
|
|
89
|
+
dataReplacedFormAttachments.push({
|
|
90
|
+
...attachment,
|
|
91
|
+
blobName,
|
|
92
|
+
fileName: attachment.fileName.replace('.docx', '.pdf'),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return dataReplacedFormAttachments
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('handleTemplateReport failed: ', err)
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[formTemplateReports, formId, formDataId, companyKey],
|
|
103
|
+
)
|
|
33
104
|
|
|
34
105
|
const onSendNotification = useCallback(
|
|
35
106
|
async (notifActionProps: IButtonProps_SendNotification) => {
|
|
@@ -44,6 +115,10 @@ export const useSendNotificationAction = ({
|
|
|
44
115
|
return
|
|
45
116
|
}
|
|
46
117
|
|
|
118
|
+
let formTemplateReportAttachments: IEmail_Attachment[] = []
|
|
119
|
+
if (Array.isArray(formNotif?.attachments) && formNotif.attachments.length > 0)
|
|
120
|
+
formTemplateReportAttachments = await handleTemplateReport(formNotif?.attachments)
|
|
121
|
+
|
|
47
122
|
try {
|
|
48
123
|
const notifConfigRes = await client.get(`/api/notificationconfig/${notificationId}/data`)
|
|
49
124
|
const formDataRes = await client.post(`/api/report/${formId}/${formDataId}`, { joins })
|
|
@@ -55,19 +130,63 @@ export const useSendNotificationAction = ({
|
|
|
55
130
|
) {
|
|
56
131
|
let notifConfig = notifConfigRes.data.data || ''
|
|
57
132
|
|
|
58
|
-
for (const [placeholderKey,
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
for (const [placeholderKey, placeholderReplacement] of Object.entries(formNotif.replacements)) {
|
|
134
|
+
const { field: fieldPath, renderConfig } = placeholderReplacement
|
|
135
|
+
|
|
136
|
+
if (renderConfig?.type === DataRenderTypeEnum.Image) {
|
|
137
|
+
const imgTag = buildImageTag(`${fileBaseUrl}/${encodeURIComponent(fieldPath)}`, {
|
|
138
|
+
width: renderConfig.style?.width,
|
|
139
|
+
height: renderConfig.style?.height,
|
|
140
|
+
alt: 'Image is attached as attachment!',
|
|
141
|
+
})
|
|
142
|
+
notifConfig = notifConfig.replace(placeholderKey, imgTag)
|
|
143
|
+
} else if (fieldPath) {
|
|
144
|
+
let repValue = fieldPath.split('.').reduce((accData, path) => accData[path] || accData, formDataRes.data)
|
|
145
|
+
|
|
146
|
+
if (typeof repValue === 'object' && MongoDbExtendedJsonObjectKeys.Date in repValue)
|
|
147
|
+
repValue = repValue[MongoDbExtendedJsonObjectKeys.Date]
|
|
148
|
+
else if (typeof repValue === 'object' && MongoDbExtendedJsonObjectKeys.Number in repValue)
|
|
149
|
+
repValue = repValue[MongoDbExtendedJsonObjectKeys.Number]
|
|
150
|
+
|
|
151
|
+
if (renderConfig?.type === DataRenderTypeEnum.FieldOption) {
|
|
152
|
+
try {
|
|
153
|
+
const fieldPathParts = fieldPath.split('.')
|
|
154
|
+
const fieldFormId = !isNaN(Number(fieldPathParts[0])) ? Number(fieldPathParts[0]) : formId
|
|
155
|
+
const fieldKey = fieldPathParts[fieldPathParts.length - 1]
|
|
156
|
+
|
|
157
|
+
const tKey = `${fieldKey}__${TranslationTextTypeEnum.OptionValue}__${repValue}`
|
|
158
|
+
const selectedLanguage =
|
|
159
|
+
localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.SelectedLanguage) || CountryEnum.US
|
|
160
|
+
const projectPath = `Data.translations.${tKey}`
|
|
161
|
+
|
|
162
|
+
await client
|
|
163
|
+
.post(`/api/form/${fieldFormId}`, { project: JSON.stringify({ [projectPath]: 1 }) })
|
|
164
|
+
.then((res) => {
|
|
165
|
+
if (res.status === 200) {
|
|
166
|
+
const translationsPerCountry =
|
|
167
|
+
projectPath.split('.').reduce((acc, p) => acc[p] || acc, res.data) || {}
|
|
168
|
+
if (typeof translationsPerCountry[selectedLanguage] === 'string')
|
|
169
|
+
repValue = translationsPerCountry[selectedLanguage]
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
62
174
|
|
|
63
|
-
|
|
64
|
-
|
|
175
|
+
if (['string', 'boolean', 'number'].includes(typeof repValue))
|
|
176
|
+
notifConfig = notifConfig.replaceAll(
|
|
177
|
+
placeholderKey,
|
|
178
|
+
renderConfig ? renderData(repValue, renderConfig) : repValue,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
65
181
|
}
|
|
66
182
|
|
|
67
183
|
const parsedNotifConfig = JSON.parse(notifConfig) || {}
|
|
68
|
-
const notifTypes =
|
|
184
|
+
const notifTypes =
|
|
185
|
+
Object.keys(parsedNotifConfig).filter((key) =>
|
|
186
|
+
Object.values(NotificationTypeEnum).includes(key as NotificationTypeEnum),
|
|
187
|
+
) || []
|
|
69
188
|
|
|
70
|
-
await Promise.all(
|
|
189
|
+
const notifRes = await Promise.all(
|
|
71
190
|
notifTypes.map((type) => {
|
|
72
191
|
let endpoint = '/api/notification/email'
|
|
73
192
|
if (type === NotificationTypeEnum.Push) endpoint = '/api/notification/push'
|
|
@@ -75,20 +194,27 @@ export const useSendNotificationAction = ({
|
|
|
75
194
|
|
|
76
195
|
const typeReqData = parsedNotifConfig[type] || {}
|
|
77
196
|
|
|
197
|
+
const selectedLanguage = localStorage.getItem(LOCAL_STORAGE_KEYS_ENUM.SelectedLanguage) || CountryEnum.US
|
|
198
|
+
|
|
78
199
|
if (type === NotificationTypeEnum.Email) {
|
|
79
200
|
if (!typeReqData.subject) typeReqData.subject = ''
|
|
201
|
+
else typeReqData.subject = typeReqData.subject[selectedLanguage]
|
|
202
|
+
|
|
80
203
|
if (!typeReqData.to) typeReqData.to = ''
|
|
204
|
+
|
|
81
205
|
if (!typeReqData.body) typeReqData.body = ''
|
|
82
206
|
else {
|
|
83
207
|
// used safeNormalizeForEmail, otherwise, it's received with big line heights
|
|
84
|
-
typeReqData.body = safeNormalizeForEmail(typeReqData.body)
|
|
208
|
+
typeReqData.body = safeNormalizeForEmail(typeReqData.body[selectedLanguage])
|
|
85
209
|
}
|
|
86
210
|
|
|
87
211
|
if (Array.isArray(typeReqData.attachments))
|
|
88
|
-
typeReqData.attachments = typeReqData.attachments.map(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
212
|
+
typeReqData.attachments = [...typeReqData.attachments, ...formTemplateReportAttachments].map(
|
|
213
|
+
(att: IEmail_Attachment) => {
|
|
214
|
+
const { type, ...restAtt } = att
|
|
215
|
+
return restAtt
|
|
216
|
+
},
|
|
217
|
+
)
|
|
92
218
|
|
|
93
219
|
const emailFields = ['to', 'from', 'cc', 'bcc']
|
|
94
220
|
emailFields.forEach((f) => {
|
|
@@ -99,11 +225,28 @@ export const useSendNotificationAction = ({
|
|
|
99
225
|
}
|
|
100
226
|
|
|
101
227
|
const reqData = { notificationId: formNotif.id, ...typeReqData }
|
|
102
|
-
|
|
228
|
+
|
|
229
|
+
return client.post(endpoint, reqData).then((res) => {
|
|
230
|
+
if (res.status === 200) return type
|
|
231
|
+
})
|
|
103
232
|
}),
|
|
104
233
|
)
|
|
105
234
|
|
|
106
|
-
|
|
235
|
+
if (notifRes.filter(Boolean).length > 0) {
|
|
236
|
+
if (blobDataToDeleteRef.current.length)
|
|
237
|
+
setTimeout(async () => {
|
|
238
|
+
await Promise.all(
|
|
239
|
+
blobDataToDeleteRef.current.map((blobName) => client.delete(`/api/attachment/${blobName}`)),
|
|
240
|
+
)
|
|
241
|
+
blobDataToDeleteRef.current = []
|
|
242
|
+
}, 500)
|
|
243
|
+
success({
|
|
244
|
+
message: `Successfully sent out the ${notifRes.join(', ').toLowerCase()} notification${
|
|
245
|
+
notifRes.length === 1 ? '' : 's'
|
|
246
|
+
}!`,
|
|
247
|
+
})
|
|
248
|
+
onSuccess()
|
|
249
|
+
}
|
|
107
250
|
}
|
|
108
251
|
|
|
109
252
|
onFinal()
|
|
@@ -112,7 +255,7 @@ export const useSendNotificationAction = ({
|
|
|
112
255
|
onError()
|
|
113
256
|
}
|
|
114
257
|
},
|
|
115
|
-
[onSuccess, onError, onFinal, formNotifications, formId, formDataId],
|
|
258
|
+
[onSuccess, onError, onFinal, formNotifications, formId, formDataId, handleTemplateReport, fileBaseUrl],
|
|
116
259
|
)
|
|
117
260
|
|
|
118
261
|
return onSendNotification
|
|
@@ -140,6 +283,8 @@ function safeNormalizeForEmail(
|
|
|
140
283
|
listIndentPx?: number // default 20
|
|
141
284
|
} = {},
|
|
142
285
|
) {
|
|
286
|
+
if (!html) return ''
|
|
287
|
+
|
|
143
288
|
const baseFontPx = opts.baseFontPx ?? 14
|
|
144
289
|
const lineHeightPx = opts.lineHeightPx ?? 20
|
|
145
290
|
const listIndentPx = opts.listIndentPx ?? 20
|
|
@@ -188,3 +333,74 @@ function safeNormalizeForEmail(
|
|
|
188
333
|
|
|
189
334
|
return out
|
|
190
335
|
}
|
|
336
|
+
type Dim = number | string | undefined
|
|
337
|
+
|
|
338
|
+
function normCssDim(v: Dim, fallback: string): string {
|
|
339
|
+
if (v == null) return fallback
|
|
340
|
+
if (typeof v === 'number') return `${v}px`
|
|
341
|
+
return v
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function pxNumber(v: Dim): number | undefined {
|
|
345
|
+
if (v == null) return undefined
|
|
346
|
+
if (typeof v === 'number') return v
|
|
347
|
+
if (v.endsWith('px')) {
|
|
348
|
+
const n = parseInt(v, 10)
|
|
349
|
+
return Number.isFinite(n) ? n : undefined
|
|
350
|
+
}
|
|
351
|
+
return undefined // don't emit % as attributes
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function escAttrSingle(s: string): string {
|
|
355
|
+
return s.replace(/&/g, '&').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Email-safe <img> generator (single-quoted attributes).
|
|
360
|
+
* - If height (px) is provided and width is not, we emit:
|
|
361
|
+
* height='N' + style 'height:Npx;width:auto;max-width:100%' => closest to "contain by height"
|
|
362
|
+
* - If width is % (e.g., '100%'), it's emitted only in CSS (not as an attribute).
|
|
363
|
+
* - If width/height are numeric (px), attributes + matching CSS are emitted for Outlook reliability.
|
|
364
|
+
*/
|
|
365
|
+
export function buildImageTag(
|
|
366
|
+
src: string,
|
|
367
|
+
opts?: {
|
|
368
|
+
alt?: string
|
|
369
|
+
width?: Dim // 300 | '300px' | '100%'
|
|
370
|
+
height?: Dim // 30 | '30px' | '50%'
|
|
371
|
+
extraStyle?: string
|
|
372
|
+
className?: string
|
|
373
|
+
},
|
|
374
|
+
): string {
|
|
375
|
+
const { alt = '', width, height, extraStyle, className } = opts || {}
|
|
376
|
+
|
|
377
|
+
const cssW = normCssDim(width, 'auto')
|
|
378
|
+
const cssH = normCssDim(height, 'auto')
|
|
379
|
+
|
|
380
|
+
const wAttrNum = pxNumber(width)
|
|
381
|
+
const hAttrNum = pxNumber(height)
|
|
382
|
+
|
|
383
|
+
let attrs = ` src='${escAttrSingle(src)}' alt='${alt}'`
|
|
384
|
+
if (wAttrNum !== undefined) attrs += ` width='${wAttrNum}'`
|
|
385
|
+
if (hAttrNum !== undefined) attrs += ` height='${hAttrNum}'`
|
|
386
|
+
if (className) attrs += ` class='${escAttrSingle(className)}'`
|
|
387
|
+
|
|
388
|
+
const styles = [
|
|
389
|
+
'display:block',
|
|
390
|
+
'border:0',
|
|
391
|
+
'outline:0',
|
|
392
|
+
'text-decoration:none',
|
|
393
|
+
'-ms-interpolation-mode:bicubic',
|
|
394
|
+
`width:${cssW}`,
|
|
395
|
+
`height:${cssH}`,
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
// If we're constraining by height only, keep width fluid but prevent overflow
|
|
399
|
+
if (height && !width) styles.push('max-width:100%')
|
|
400
|
+
|
|
401
|
+
if (extraStyle?.trim()) styles.push(extraStyle.trim())
|
|
402
|
+
|
|
403
|
+
attrs += ` style='${styles.join(';')}'`
|
|
404
|
+
|
|
405
|
+
return `<img${attrs} />`
|
|
406
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -174,6 +174,11 @@ export const BSON_DATA_IDENTIFIER_PREFIXES = {
|
|
|
174
174
|
Date: 'Date__',
|
|
175
175
|
Number: 'Number__',
|
|
176
176
|
}
|
|
177
|
+
export enum MongoDbExtendedJsonObjectKeys {
|
|
178
|
+
Date = '$date',
|
|
179
|
+
ObjectId = '$oid',
|
|
180
|
+
Number = '$numberDecimal',
|
|
181
|
+
}
|
|
177
182
|
export const DEFAULT_FORM_SCHEMA_DATA: IFormSchema = {
|
|
178
183
|
detailsConfig: {
|
|
179
184
|
elements: {},
|
|
@@ -186,7 +191,8 @@ export const DEFAULT_FORM_SCHEMA_DATA: IFormSchema = {
|
|
|
186
191
|
columns: [],
|
|
187
192
|
header: { elements: {}, layouts: { [DeviceBreakpointEnum.Default]: [] } },
|
|
188
193
|
},
|
|
189
|
-
|
|
194
|
+
submissionPdfConfig: { enabled: false },
|
|
195
|
+
templateReportConfig: { templates: [], joins: [] },
|
|
190
196
|
relationships: [],
|
|
191
197
|
translations: {},
|
|
192
198
|
notificationsConfig: { notifications: [] },
|
package/src/enums/form.enum.ts
CHANGED
|
@@ -48,6 +48,7 @@ export enum DataRenderTypeEnum {
|
|
|
48
48
|
Image = 'Image',
|
|
49
49
|
// PhoneNumber = 'PhoneNumber',
|
|
50
50
|
Date = 'Date',
|
|
51
|
+
FieldOption = 'FieldOption',
|
|
51
52
|
Buttons = 'Buttons',
|
|
52
53
|
Conditional = 'Conditional',
|
|
53
54
|
// Concatenation = 'Concatenation',
|
|
@@ -142,6 +143,7 @@ export enum FormPreservedItemKeys {
|
|
|
142
143
|
FormNotifications = 'formNotifications',
|
|
143
144
|
DuplicateDataFound = 'isDuplicateDataFound',
|
|
144
145
|
DuplicateCheckPending = 'isDuplicateCheckPending',
|
|
146
|
+
FormTemplateReports = 'formTemplateReports',
|
|
145
147
|
}
|
|
146
148
|
export enum DataCategoryEnum {
|
|
147
149
|
Number = 'Number',
|
package/src/enums/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BSON_DATA_IDENTIFIER_PREFIXES } from '../../constants'
|
|
1
|
+
import { BSON_DATA_IDENTIFIER_PREFIXES, MongoDbExtendedJsonObjectKeys } from '../../constants'
|
|
2
2
|
import { isValidMongoDbId } from '..'
|
|
3
3
|
import dayjs from 'dayjs'
|
|
4
4
|
|
|
@@ -7,21 +7,21 @@ export function toMongoDbExtendedJSON(input: Record<string, any>): Record<string
|
|
|
7
7
|
|
|
8
8
|
for (const [key, val] of Object.entries(input)) {
|
|
9
9
|
if (key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.ObjectId)) {
|
|
10
|
-
if (typeof val === 'string' && isValidMongoDbId(val)) out[key] = {
|
|
10
|
+
if (typeof val === 'string' && isValidMongoDbId(val)) out[key] = { [MongoDbExtendedJsonObjectKeys.ObjectId]: val }
|
|
11
11
|
else {
|
|
12
12
|
console.warn(`Invalid ObjectId for "${key}":`, val)
|
|
13
13
|
out[key] = val
|
|
14
14
|
}
|
|
15
15
|
} else if (key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Date)) {
|
|
16
16
|
if (val && dayjs(val).isValid()) {
|
|
17
|
-
out[key] = {
|
|
17
|
+
out[key] = { [MongoDbExtendedJsonObjectKeys.Date]: dayjs(val).utc().toISOString() }
|
|
18
18
|
} else {
|
|
19
19
|
console.warn(`Invalid Date for "${key}":`, val)
|
|
20
20
|
out[key] = val
|
|
21
21
|
}
|
|
22
22
|
} else if (key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Number)) {
|
|
23
23
|
const num = typeof val === 'number' ? val : Number(val)
|
|
24
|
-
if (!isNaN(num)) out[key] = {
|
|
24
|
+
if (!isNaN(num)) out[key] = { [MongoDbExtendedJsonObjectKeys.Number]: num }
|
|
25
25
|
else {
|
|
26
26
|
console.warn(`Invalid Number for "${key}":`, val)
|
|
27
27
|
out[key] = val
|
|
@@ -39,18 +39,28 @@ export function fromMongoDbExtendedJSON(
|
|
|
39
39
|
const out: Record<string, any> = {}
|
|
40
40
|
|
|
41
41
|
for (const [key, val] of Object.entries(input)) {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
if (
|
|
43
|
+
key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.ObjectId) &&
|
|
44
|
+
val &&
|
|
45
|
+
typeof val === 'object' &&
|
|
46
|
+
MongoDbExtendedJsonObjectKeys.ObjectId in val
|
|
47
|
+
) {
|
|
48
|
+
out[key] = (val as any)[MongoDbExtendedJsonObjectKeys.ObjectId]
|
|
49
|
+
} else if (
|
|
50
|
+
key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Date) &&
|
|
51
|
+
val &&
|
|
52
|
+
typeof val === 'object' &&
|
|
53
|
+
MongoDbExtendedJsonObjectKeys.Date in val
|
|
54
|
+
) {
|
|
55
|
+
const date = (val as any)[MongoDbExtendedJsonObjectKeys.Date]
|
|
46
56
|
out[key] = date ? (originalTzFields.includes(key) ? dayjs(date).utc() : dayjs(date)) : date
|
|
47
57
|
} else if (
|
|
48
58
|
key.startsWith(BSON_DATA_IDENTIFIER_PREFIXES.Number) &&
|
|
49
59
|
val &&
|
|
50
60
|
typeof val === 'object' &&
|
|
51
|
-
|
|
61
|
+
MongoDbExtendedJsonObjectKeys.Number in val
|
|
52
62
|
) {
|
|
53
|
-
const n = (val as any)
|
|
63
|
+
const n = (val as any)[MongoDbExtendedJsonObjectKeys.Number]
|
|
54
64
|
out[key] = typeof n === 'number' ? n : Number(n)
|
|
55
65
|
} else out[key] = val
|
|
56
66
|
}
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
IGridContainerConfig,
|
|
16
16
|
IDynamicForm,
|
|
17
17
|
IFormDataApiReqConfig,
|
|
18
|
+
IFormTemplateReport,
|
|
18
19
|
} from '../../types'
|
|
20
|
+
import { renderData } from '..'
|
|
19
21
|
|
|
20
22
|
export const extractFiltersFromLayout = (elements: { [key: string]: IDndLayoutElement }) => {
|
|
21
23
|
const filters: IFilterNested = {}
|
|
@@ -197,4 +199,66 @@ export const isNewFormDataPage = (formDataId?: string) => !formDataId || formDat
|
|
|
197
199
|
|
|
198
200
|
/** --------------------------------------------------------------------------------------------------------- */
|
|
199
201
|
|
|
202
|
+
export const replaceAndGetFileBlob = async (
|
|
203
|
+
tInfo: IFormTemplateReport,
|
|
204
|
+
formData: any,
|
|
205
|
+
companyKey?: string,
|
|
206
|
+
): Promise<string> => {
|
|
207
|
+
if (!companyKey) return ''
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const replacements = await Promise.all(
|
|
211
|
+
tInfo.replacements.map(async (rep) => {
|
|
212
|
+
const value = rep.field.split('.').reduce((curr, n) => curr?.[n] ?? {}, formData)
|
|
213
|
+
|
|
214
|
+
if (rep.optionSource) {
|
|
215
|
+
if (rep.optionSource.type === FieldElementOptionSourceEnum.Static) {
|
|
216
|
+
return {
|
|
217
|
+
placeholder: rep.placeholder,
|
|
218
|
+
value: renderData(
|
|
219
|
+
rep.optionSource.options.find((op) => op.id === (value as unknown as string))?.value,
|
|
220
|
+
rep.renderConfig,
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
} else if (rep.optionSource.type === FieldElementOptionSourceEnum.DynamicForm) {
|
|
224
|
+
const formDataResData = await fetchFormDataAsLookup(rep.optionSource.baseFormId)
|
|
225
|
+
const field = rep.optionSource.optionRender?.fields?.[0]?.field
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
placeholder: rep.placeholder,
|
|
229
|
+
value: renderData(
|
|
230
|
+
formDataResData.find((data) => data.id === (value as unknown as string))?.[
|
|
231
|
+
field ? `Data.${field}` : field
|
|
232
|
+
],
|
|
233
|
+
rep.renderConfig,
|
|
234
|
+
),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
placeholder: rep.placeholder,
|
|
241
|
+
value: renderData(value, rep.renderConfig),
|
|
242
|
+
type: rep.type,
|
|
243
|
+
}
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return await client
|
|
248
|
+
.post(
|
|
249
|
+
`/api/template/${companyKey}/${tInfo.fileBlobName}`,
|
|
250
|
+
replacements.map((r) => ({ key: r.placeholder, value: r.value?.toString() ?? '-', type: r.type })),
|
|
251
|
+
{ responseType: 'blob' },
|
|
252
|
+
)
|
|
253
|
+
.then((res) => (res.status === 200 ? res.data : ''))
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('replaceAndGetFileBlob failed:', err)
|
|
256
|
+
return ''
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** --------------------------------------------------------------------------------------------------------- */
|
|
261
|
+
|
|
262
|
+
/** --------------------------------------------------------------------------------------------------------- */
|
|
263
|
+
|
|
200
264
|
/** --------------------------------------------------------------------------------------------------------- */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TemplateDataReplacementFieldTypesEnum } from '../../../enums'
|
|
2
2
|
import { IDataRenderConfig, IFieldElementOptionSource } from '../layout-elements'
|
|
3
3
|
|
|
4
|
-
export interface
|
|
4
|
+
export interface IFormSubmissionPdfConfig { // only for public forms
|
|
5
5
|
enabled: false
|
|
6
6
|
title?: { values: { value: string; isDynamic: boolean; order: number }[]; delimiter: string }
|
|
7
7
|
showDownloadModal?: boolean
|
package/src/types/forms/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ export * from './generate'
|
|
|
4
4
|
export * from './relationship'
|
|
5
5
|
|
|
6
6
|
import { IDataListSorter, IFormDataListColumn, IFormDataListPagination } from './data-list'
|
|
7
|
-
import {
|
|
7
|
+
import { IFormSubmissionPdfConfig, IFormTemplateReport } from './generate'
|
|
8
8
|
import { IFormRelationshipConfig } from './relationship'
|
|
9
|
-
import { IDndLayoutStructure_Responsive, IEmail_Attachment } from '..'
|
|
9
|
+
import { IDataRenderConfig, IDndLayoutStructure_Responsive, IEmail_Attachment } from '..'
|
|
10
10
|
import {
|
|
11
11
|
ButtonElementSizeEnum,
|
|
12
12
|
ButtonElementTypeEnum,
|
|
@@ -32,7 +32,11 @@ export interface IFormSchema {
|
|
|
32
32
|
duplicateCheckConfig?: IDuplicateCheckConfig
|
|
33
33
|
}
|
|
34
34
|
dataListConfig: IFormDataListConfig
|
|
35
|
-
|
|
35
|
+
submissionPdfConfig?: IFormSubmissionPdfConfig
|
|
36
|
+
templateReportConfig?: {
|
|
37
|
+
templates?: IFormTemplateReport[]
|
|
38
|
+
joins?: IFormJoin[]
|
|
39
|
+
}
|
|
36
40
|
relationships?: IFormRelationshipConfig[]
|
|
37
41
|
translations?: IFormTranslations
|
|
38
42
|
notificationsConfig?: {
|
|
@@ -58,7 +62,7 @@ export type IFormTranslations = { [key: string]: { [key in CountryEnum]?: string
|
|
|
58
62
|
export interface IFormNotification {
|
|
59
63
|
id: number
|
|
60
64
|
name?: string
|
|
61
|
-
replacements?: { [key: string]: string }
|
|
65
|
+
replacements?: { [key: string]: { field: string; renderConfig: IDataRenderConfig } }
|
|
62
66
|
attachments?: IEmail_Attachment[]
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -74,12 +78,6 @@ export interface IFormDataListConfig {
|
|
|
74
78
|
noDataText?: string
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
export interface IFormGenerateConfig {
|
|
78
|
-
submissionPdf?: IFormSubmissionPdf
|
|
79
|
-
templateReports?: IFormTemplateReport[]
|
|
80
|
-
formJoins?: IFormJoin[]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
81
|
export interface IFormDndLayoutRowHeader {
|
|
84
82
|
isCollapsible?: boolean
|
|
85
83
|
defaultCollapsed?: boolean
|
|
@@ -12,8 +12,9 @@ export type IButtonElementProps = IButtonPropsBase & {
|
|
|
12
12
|
| IButtonProps_CustomFunctionCall
|
|
13
13
|
| IButtonProps_SendNotification
|
|
14
14
|
| IButtonProps_Other
|
|
15
|
-
// secondaryAction only applies if category is NOT [CustomFunction, SendNotification, Navigate]
|
|
15
|
+
// secondaryAction & tertiaryAction only applies if category is NOT [CustomFunction, SendNotification, Navigate]
|
|
16
16
|
secondaryAction?: IButtonProps_Navigate | IButtonProps_CustomFunctionCall | IButtonProps_SendNotification
|
|
17
|
+
tertiaryAction?: IButtonProps_Navigate | IButtonProps_CustomFunctionCall | IButtonProps_SendNotification
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface IButtonPropsBase {
|
|
@@ -8,6 +8,7 @@ export type IDataRenderConfig =
|
|
|
8
8
|
| IDataRender_Image
|
|
9
9
|
// | IDataRender_PhoneNumber
|
|
10
10
|
| IDataRender_Date
|
|
11
|
+
| IDataRender_FieldOption
|
|
11
12
|
| IDataRender_Buttons
|
|
12
13
|
| IDataRender_Conditional
|
|
13
14
|
| IDataRender_Tags
|
|
@@ -48,6 +49,10 @@ export interface IDataRender_Date {
|
|
|
48
49
|
displayInOriginalTz?: boolean
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
export interface IDataRender_FieldOption {
|
|
53
|
+
type: DataRenderTypeEnum.FieldOption
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
export interface IDataRender_Conditional {
|
|
52
57
|
type: DataRenderTypeEnum.Conditional
|
|
53
58
|
conditions: { [key: string]: any } // similar to filter conditions
|
|
@@ -7,7 +7,7 @@ export type INotificationConfig = {
|
|
|
7
7
|
[NotificationTypeEnum.Push]: INotificationConfig_Push
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
interface INotificationConfig_Email {
|
|
10
|
+
export interface INotificationConfig_Email {
|
|
11
11
|
type: NotificationTypeEnum.Email
|
|
12
12
|
subject: Record<CountryEnum, string>
|
|
13
13
|
body: Record<CountryEnum, string>
|