form-craft-package 1.11.2 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/common/custom-hooks/use-lazy-modal-opener.hook.ts +4 -12
- package/src/components/form/layout-renderer/3-element/18-gallery/delete.modal.tsx +148 -0
- package/src/components/form/layout-renderer/3-element/18-gallery/index.tsx +264 -0
- package/src/components/form/layout-renderer/3-element/18-gallery/preview.modal.tsx +33 -0
- package/src/components/form/layout-renderer/3-element/18-gallery/upload.modal.tsx +185 -0
- package/src/components/form/layout-renderer/3-element/7-file-upload.tsx +9 -10
- package/src/components/form/layout-renderer/3-element/9-form-data-render.tsx +69 -6
- package/src/components/form/layout-renderer/3-element/index.tsx +13 -0
- package/src/enums/index.ts +7 -0
- package/src/types/forms/layout-elements/index.ts +26 -1
- package/src/types/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
export const useLazyModalOpener = () => {
|
|
4
4
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
5
|
-
const
|
|
5
|
+
const isPendingTransition = false
|
|
6
6
|
|
|
7
|
-
const openModal = () =>
|
|
8
|
-
startTransition(() => {
|
|
9
|
-
setIsModalOpen(true)
|
|
10
|
-
})
|
|
11
|
-
}
|
|
7
|
+
const openModal = () => setIsModalOpen(true)
|
|
12
8
|
|
|
13
|
-
const closeModal = () =>
|
|
14
|
-
startTransition(() => {
|
|
15
|
-
setIsModalOpen(false)
|
|
16
|
-
})
|
|
17
|
-
}
|
|
9
|
+
const closeModal = () => setIsModalOpen(false)
|
|
18
10
|
|
|
19
11
|
return { isModalOpen, isPendingTransition, openModal, closeModal }
|
|
20
12
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import client from '../../../../../api/client'
|
|
2
|
+
import { resolveConditionalText } from '../../../../../functions'
|
|
3
|
+
import { TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../../enums'
|
|
4
|
+
import { Button_FillerPortal } from '../../../../common/button'
|
|
5
|
+
import { useTranslation } from '../../../../common/custom-hooks'
|
|
6
|
+
import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
|
|
7
|
+
import { IElementBaseProps } from '..'
|
|
8
|
+
import { IFormContext } from '../../1-row'
|
|
9
|
+
import { Checkbox, Form, Image, Modal } from 'antd'
|
|
10
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
11
|
+
|
|
12
|
+
export default function GalleryDeleteModal({
|
|
13
|
+
isOpen,
|
|
14
|
+
formContext,
|
|
15
|
+
formItem,
|
|
16
|
+
elementKey,
|
|
17
|
+
textConditions,
|
|
18
|
+
blobNames,
|
|
19
|
+
imageUrls,
|
|
20
|
+
onClose,
|
|
21
|
+
onSuccess,
|
|
22
|
+
}: {
|
|
23
|
+
isOpen: boolean
|
|
24
|
+
formContext: IFormContext
|
|
25
|
+
blobNames: string[]
|
|
26
|
+
imageUrls: string[]
|
|
27
|
+
onClose: () => void
|
|
28
|
+
onSuccess: (blobNames: string[]) => void
|
|
29
|
+
} & IElementBaseProps) {
|
|
30
|
+
const { formRef, formDataId, formId } = formContext
|
|
31
|
+
const { t } = useTranslation(formId)
|
|
32
|
+
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
33
|
+
const [selectedBlobNames, setSelectedBlobNames] = useState<string[]>([])
|
|
34
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
35
|
+
const allValues = Form.useWatch([], { form: formRef, preserve: true })
|
|
36
|
+
|
|
37
|
+
const label = useMemo(
|
|
38
|
+
() =>
|
|
39
|
+
resolveConditionalText({
|
|
40
|
+
elementKey,
|
|
41
|
+
textConditions,
|
|
42
|
+
t,
|
|
43
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
44
|
+
formDataId,
|
|
45
|
+
currentBreakpoint,
|
|
46
|
+
type: TranslationTextTypeEnum.Label,
|
|
47
|
+
}),
|
|
48
|
+
[allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const [title, discardText, deleteText] = useMemo(
|
|
52
|
+
() =>
|
|
53
|
+
t([
|
|
54
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.DeleteModalTitle },
|
|
55
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.DeleteModalDiscard },
|
|
56
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.DeleteModalDelete },
|
|
57
|
+
]),
|
|
58
|
+
[elementKey, t],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const toggleBlobName = useCallback(
|
|
62
|
+
(blobName: string) =>
|
|
63
|
+
setSelectedBlobNames((prev) =>
|
|
64
|
+
prev.includes(blobName) ? prev.filter((selectedBlobName) => selectedBlobName !== blobName) : [...prev, blobName],
|
|
65
|
+
),
|
|
66
|
+
[],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const closeModal = useCallback(() => {
|
|
70
|
+
setSelectedBlobNames([])
|
|
71
|
+
onClose()
|
|
72
|
+
}, [onClose])
|
|
73
|
+
|
|
74
|
+
const deletePictures = useCallback(async () => {
|
|
75
|
+
if (!formId || !formDataId || !selectedBlobNames.length) return
|
|
76
|
+
|
|
77
|
+
setIsDeleting(true)
|
|
78
|
+
try {
|
|
79
|
+
await Promise.all(selectedBlobNames.map((blobName) => client.delete(`/api/attachment/${blobName}`)))
|
|
80
|
+
const formDataRes = await client.get(`/api/formdata/${formId}/${formDataId}`)
|
|
81
|
+
const parsedData = JSON.parse(formDataRes.data.data)
|
|
82
|
+
const fieldPath = Array.isArray(formItem.path) ? formItem.path : [formItem.path]
|
|
83
|
+
const existingBlobNames = fieldPath.reduce((current: any, key) => current?.[key], parsedData)
|
|
84
|
+
const remainingBlobNames = (Array.isArray(existingBlobNames) ? existingBlobNames : []).filter(
|
|
85
|
+
(blobName) => !selectedBlobNames.includes(blobName),
|
|
86
|
+
)
|
|
87
|
+
let current = parsedData
|
|
88
|
+
fieldPath.slice(0, -1).forEach((key) => {
|
|
89
|
+
current = current[key]
|
|
90
|
+
})
|
|
91
|
+
current[fieldPath[fieldPath.length - 1]] = remainingBlobNames
|
|
92
|
+
const res = await client.put(`/api/formdata/${formId}/${formDataId}`, {
|
|
93
|
+
name: formDataRes.data.name,
|
|
94
|
+
version: formDataRes.data.version,
|
|
95
|
+
private: formDataRes.data.private,
|
|
96
|
+
data: JSON.stringify(parsedData),
|
|
97
|
+
})
|
|
98
|
+
if (res.status === 200) {
|
|
99
|
+
setSelectedBlobNames([])
|
|
100
|
+
onSuccess(remainingBlobNames)
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
setIsDeleting(false)
|
|
104
|
+
}
|
|
105
|
+
}, [formDataId, formId, formItem.path, onSuccess, selectedBlobNames])
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Modal
|
|
109
|
+
open={isOpen}
|
|
110
|
+
title={title || label || 'Delete pictures'}
|
|
111
|
+
onCancel={closeModal}
|
|
112
|
+
footer={
|
|
113
|
+
<div className="flex justify-between">
|
|
114
|
+
<Button_FillerPortal outline onClick={closeModal}>
|
|
115
|
+
{discardText || 'Discard'}
|
|
116
|
+
</Button_FillerPortal>
|
|
117
|
+
<Button_FillerPortal primary loading={isDeleting} onClick={deletePictures}>
|
|
118
|
+
{deleteText || 'Delete Selected'}
|
|
119
|
+
</Button_FillerPortal>
|
|
120
|
+
</div>
|
|
121
|
+
}
|
|
122
|
+
width={800}
|
|
123
|
+
>
|
|
124
|
+
<div className="grid grid-cols-4 gap-2">
|
|
125
|
+
{imageUrls.map((src, idx) => (
|
|
126
|
+
<button
|
|
127
|
+
key={src}
|
|
128
|
+
type="button"
|
|
129
|
+
className={`overflow-hidden rounded-md border bg-gray-200 p-2 aspect-square relative ${selectedBlobNames.includes(blobNames[idx]) ? 'border-primary' : 'border-gray-300'}`}
|
|
130
|
+
onClick={() => toggleBlobName(blobNames[idx])}
|
|
131
|
+
>
|
|
132
|
+
<span className="absolute top-2 right-2 z-[1]">
|
|
133
|
+
<Checkbox checked={selectedBlobNames.includes(blobNames[idx])} />
|
|
134
|
+
</span>
|
|
135
|
+
<Image
|
|
136
|
+
src={src}
|
|
137
|
+
alt={`gallery_delete_${idx}`}
|
|
138
|
+
preview={false}
|
|
139
|
+
className="w-full h-full"
|
|
140
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
141
|
+
wrapperStyle={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
142
|
+
/>
|
|
143
|
+
</button>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</Modal>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Empty, Form, Image } from 'antd'
|
|
2
|
+
import { memo, useMemo, useState } from 'react'
|
|
3
|
+
import { FaPlus, FaTrashAlt } from 'react-icons/fa'
|
|
4
|
+
import { FaUpload } from 'react-icons/fa6'
|
|
5
|
+
import { isNewFormDataPage, mapToFormItemRules, resolveConditionalText } from '../../../../../functions'
|
|
6
|
+
import { DeviceBreakpointEnum, FormElementConditionalKeyEnum, TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../../enums'
|
|
7
|
+
import { IGalleryElementProps, IValidationRule } from '../../../../../types'
|
|
8
|
+
import { Button_FillerPortal } from '../../../../common/button'
|
|
9
|
+
import { useTranslation } from '../../../../common/custom-hooks'
|
|
10
|
+
import { isValidationsMet } from '../../../../common/custom-hooks/use-node-condition.hook/visibility-utils'
|
|
11
|
+
import { useFormPreservedItemValues } from '../../../../common/custom-hooks/use-preserved-form-items.hook'
|
|
12
|
+
import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
|
|
13
|
+
import { IElementBaseProps } from '..'
|
|
14
|
+
import { IFormContext } from '../../1-row'
|
|
15
|
+
import GalleryDeleteModal from './delete.modal'
|
|
16
|
+
import GalleryUploadModal from './upload.modal'
|
|
17
|
+
import GalleryPreviewModal from './preview.modal'
|
|
18
|
+
|
|
19
|
+
function LayoutRenderer_Gallery({
|
|
20
|
+
formContext,
|
|
21
|
+
formItem,
|
|
22
|
+
elementProps,
|
|
23
|
+
isDisabled,
|
|
24
|
+
elementKey,
|
|
25
|
+
textConditions,
|
|
26
|
+
validations,
|
|
27
|
+
}: {
|
|
28
|
+
formContext: IFormContext
|
|
29
|
+
elementProps: IGalleryElementProps
|
|
30
|
+
validations?: IValidationRule[]
|
|
31
|
+
} & IElementBaseProps) {
|
|
32
|
+
const { formRef, formDataId, companyKey, formId } = formContext
|
|
33
|
+
const { t } = useTranslation(formId)
|
|
34
|
+
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
35
|
+
const { baseServerUrl } = useFormPreservedItemValues(formRef)
|
|
36
|
+
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false)
|
|
37
|
+
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false)
|
|
38
|
+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
|
39
|
+
const allValues = Form.useWatch([], { form: formRef, preserve: true })
|
|
40
|
+
const savedBlobNames = ((Form.useWatch(formItem.path, { form: formRef, preserve: true }) as string[]) ??
|
|
41
|
+
[]) as string[]
|
|
42
|
+
const label = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
resolveConditionalText({
|
|
45
|
+
elementKey,
|
|
46
|
+
textConditions,
|
|
47
|
+
t,
|
|
48
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
49
|
+
formDataId,
|
|
50
|
+
currentBreakpoint,
|
|
51
|
+
type: TranslationTextTypeEnum.Label,
|
|
52
|
+
}),
|
|
53
|
+
[allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const placeholder = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
resolveConditionalText({
|
|
59
|
+
elementKey,
|
|
60
|
+
textConditions,
|
|
61
|
+
t,
|
|
62
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
63
|
+
formDataId,
|
|
64
|
+
currentBreakpoint,
|
|
65
|
+
type: TranslationTextTypeEnum.Placeholder,
|
|
66
|
+
}),
|
|
67
|
+
[allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
|
|
68
|
+
)
|
|
69
|
+
const [emptyDescription = 'No pictures uploaded'] = useMemo(
|
|
70
|
+
() =>
|
|
71
|
+
t([
|
|
72
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Description, subType: TranslationTextSubTypeEnum.EmptyState },
|
|
73
|
+
]),
|
|
74
|
+
[elementKey, t],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const validationRules = useMemo(() => {
|
|
78
|
+
const baseRules = mapToFormItemRules(elementKey, validations ?? [], formId, {
|
|
79
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
80
|
+
formDataId,
|
|
81
|
+
currentBreakpoint,
|
|
82
|
+
})
|
|
83
|
+
const requiredRule = baseRules.find((rule: any) => 'required' in rule && rule.required)
|
|
84
|
+
if (!requiredRule) return baseRules
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
...baseRules.filter((rule: any) => !('required' in rule && rule.required)),
|
|
88
|
+
{
|
|
89
|
+
validator(_: unknown, value: unknown) {
|
|
90
|
+
return Array.isArray(value) && value.length > 0
|
|
91
|
+
? Promise.resolve()
|
|
92
|
+
: Promise.reject(new Error(requiredRule.message as string))
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
}, [allValues, currentBreakpoint, elementKey, formDataId, formId, validations])
|
|
97
|
+
|
|
98
|
+
const { imageCountPerRow, imageUrls, visibleImageUrls, overflowCount } = useMemo(() => {
|
|
99
|
+
const gridConfig = elementProps.grid?.[currentBreakpoint] ?? elementProps.grid?.[DeviceBreakpointEnum.Default]
|
|
100
|
+
const imageCountPerRow = gridConfig?.count ?? 8
|
|
101
|
+
const visibleLimit = gridConfig?.row ? imageCountPerRow * gridConfig.row : savedBlobNames.length
|
|
102
|
+
const hasOverflow = gridConfig?.row != null && savedBlobNames.length > visibleLimit
|
|
103
|
+
const visibleBlobNames = hasOverflow ? savedBlobNames.slice(0, Math.max(visibleLimit - 1, 0)) : savedBlobNames
|
|
104
|
+
return {
|
|
105
|
+
imageCountPerRow,
|
|
106
|
+
imageUrls: savedBlobNames.map((blobName) => `${baseServerUrl}/api/attachment/${companyKey}/${blobName}`),
|
|
107
|
+
visibleImageUrls: visibleBlobNames.map((blobName) => `${baseServerUrl}/api/attachment/${companyKey}/${blobName}`),
|
|
108
|
+
overflowCount: hasOverflow ? savedBlobNames.length - visibleBlobNames.length : 0,
|
|
109
|
+
}
|
|
110
|
+
}, [baseServerUrl, companyKey, currentBreakpoint, elementProps.grid, savedBlobNames])
|
|
111
|
+
const isUploadButtonHidden = !isValidationsMet(
|
|
112
|
+
FormElementConditionalKeyEnum.ShowIf,
|
|
113
|
+
(allValues ?? {}) as { [key: string]: any },
|
|
114
|
+
elementProps.uploadButtonConditions,
|
|
115
|
+
{ currentBreakpoint, formDataId },
|
|
116
|
+
)
|
|
117
|
+
const isUploadButtonDisabled = !isValidationsMet(
|
|
118
|
+
FormElementConditionalKeyEnum.EnableIf,
|
|
119
|
+
(allValues ?? {}) as { [key: string]: any },
|
|
120
|
+
elementProps.uploadButtonConditions,
|
|
121
|
+
{ currentBreakpoint, formDataId },
|
|
122
|
+
)
|
|
123
|
+
const isDeleteButtonHidden = !isValidationsMet(
|
|
124
|
+
FormElementConditionalKeyEnum.ShowIf,
|
|
125
|
+
(allValues ?? {}) as { [key: string]: any },
|
|
126
|
+
elementProps.deleteButtonConditions,
|
|
127
|
+
{ currentBreakpoint, formDataId },
|
|
128
|
+
)
|
|
129
|
+
const isDeleteButtonDisabled = !isValidationsMet(
|
|
130
|
+
FormElementConditionalKeyEnum.EnableIf,
|
|
131
|
+
(allValues ?? {}) as { [key: string]: any },
|
|
132
|
+
elementProps.deleteButtonConditions,
|
|
133
|
+
{ currentBreakpoint, formDataId },
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
<Form.Item
|
|
139
|
+
name={formItem.name}
|
|
140
|
+
label={
|
|
141
|
+
<div className="w-full flex items-center justify-between gap-2">
|
|
142
|
+
{!elementProps.hasNoLabel && label ? (
|
|
143
|
+
<span>
|
|
144
|
+
{label} ({imageUrls.length})
|
|
145
|
+
</span>
|
|
146
|
+
) : (
|
|
147
|
+
<span />
|
|
148
|
+
)}
|
|
149
|
+
<div className="flex items-center">
|
|
150
|
+
{!isDeleteButtonHidden && (
|
|
151
|
+
<Button_FillerPortal
|
|
152
|
+
link
|
|
153
|
+
short
|
|
154
|
+
disabled={isDisabled || isNewFormDataPage(formDataId) || isDeleteButtonDisabled}
|
|
155
|
+
onClick={() => setIsDeleteModalOpen(true)}
|
|
156
|
+
className="text-danger underline hover:text-danger"
|
|
157
|
+
>
|
|
158
|
+
<FaTrashAlt />
|
|
159
|
+
Delete
|
|
160
|
+
</Button_FillerPortal>
|
|
161
|
+
)}
|
|
162
|
+
{!isUploadButtonHidden && (
|
|
163
|
+
<Button_FillerPortal
|
|
164
|
+
link
|
|
165
|
+
short
|
|
166
|
+
disabled={isDisabled || isNewFormDataPage(formDataId) || isUploadButtonDisabled}
|
|
167
|
+
onClick={() => setIsUploadModalOpen(true)}
|
|
168
|
+
>
|
|
169
|
+
<FaUpload />
|
|
170
|
+
{placeholder || 'Upload pictures'}
|
|
171
|
+
</Button_FillerPortal>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
}
|
|
176
|
+
labelAlign="left"
|
|
177
|
+
rules={validationRules}
|
|
178
|
+
>
|
|
179
|
+
{imageUrls.length ? (
|
|
180
|
+
<Image.PreviewGroup items={imageUrls}>
|
|
181
|
+
<div
|
|
182
|
+
className="grid gap-2"
|
|
183
|
+
style={{ gridTemplateColumns: `repeat(${imageCountPerRow}, minmax(0px, 1fr))` }}
|
|
184
|
+
>
|
|
185
|
+
{visibleImageUrls.map((src, idx) => (
|
|
186
|
+
<div
|
|
187
|
+
key={src}
|
|
188
|
+
className="overflow-hidden rounded-lg border border-gray-300 aspect-square p-2 bg-gray-200"
|
|
189
|
+
>
|
|
190
|
+
<Image
|
|
191
|
+
src={src}
|
|
192
|
+
alt={`${elementKey}_${idx}`}
|
|
193
|
+
className="rounded-md object-contain flex items-center justify-center"
|
|
194
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
195
|
+
wrapperStyle={{
|
|
196
|
+
width: '100%',
|
|
197
|
+
height: '100%',
|
|
198
|
+
display: 'flex',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
{overflowCount > 0 && (
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
className="overflow-hidden rounded-md border border-gray-300 aspect-square text-primary text-[24px] font-bold bg-gray-200 flex items-center justify-center gap-2 hover:bg-gray-300"
|
|
209
|
+
onClick={() => setIsPreviewModalOpen(true)}
|
|
210
|
+
>
|
|
211
|
+
<FaPlus size={18} />
|
|
212
|
+
{overflowCount}
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</Image.PreviewGroup>
|
|
217
|
+
) : (
|
|
218
|
+
<Empty description={emptyDescription || undefined} />
|
|
219
|
+
)}
|
|
220
|
+
</Form.Item>
|
|
221
|
+
<GalleryUploadModal
|
|
222
|
+
isOpen={isUploadModalOpen}
|
|
223
|
+
formContext={formContext}
|
|
224
|
+
formItem={formItem}
|
|
225
|
+
elementKey={elementKey}
|
|
226
|
+
textConditions={textConditions}
|
|
227
|
+
isDisabled={isDisabled}
|
|
228
|
+
onDiscardSuccess={() => setIsUploadModalOpen(false)}
|
|
229
|
+
onSaveSuccess={(uploadedBlobNames) => {
|
|
230
|
+
formRef?.setFieldValue(formItem.path, [
|
|
231
|
+
...((Array.isArray(formRef?.getFieldValue(formItem.path))
|
|
232
|
+
? formRef?.getFieldValue(formItem.path)
|
|
233
|
+
: []) as string[]),
|
|
234
|
+
...uploadedBlobNames,
|
|
235
|
+
])
|
|
236
|
+
setIsUploadModalOpen(false)
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
<GalleryPreviewModal
|
|
240
|
+
title={label}
|
|
241
|
+
isOpen={isPreviewModalOpen}
|
|
242
|
+
imageUrls={imageUrls}
|
|
243
|
+
onClose={() => setIsPreviewModalOpen(false)}
|
|
244
|
+
/>
|
|
245
|
+
<GalleryDeleteModal
|
|
246
|
+
isOpen={isDeleteModalOpen}
|
|
247
|
+
formContext={formContext}
|
|
248
|
+
formItem={formItem}
|
|
249
|
+
elementKey={elementKey}
|
|
250
|
+
textConditions={textConditions}
|
|
251
|
+
isDisabled={isDisabled}
|
|
252
|
+
blobNames={savedBlobNames}
|
|
253
|
+
imageUrls={imageUrls}
|
|
254
|
+
onClose={() => setIsDeleteModalOpen(false)}
|
|
255
|
+
onSuccess={(remainingBlobNames) => {
|
|
256
|
+
formRef?.setFieldValue(formItem.path, remainingBlobNames)
|
|
257
|
+
setIsDeleteModalOpen(false)
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
</>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default memo(LayoutRenderer_Gallery)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Image, Modal } from 'antd'
|
|
2
|
+
|
|
3
|
+
export default function GalleryPreviewModal({
|
|
4
|
+
title,
|
|
5
|
+
isOpen,
|
|
6
|
+
imageUrls,
|
|
7
|
+
onClose,
|
|
8
|
+
}: {
|
|
9
|
+
title?: string
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
imageUrls: string[]
|
|
12
|
+
onClose: () => void
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<Modal open={isOpen} onCancel={onClose} footer={null} title={title} width={800}>
|
|
16
|
+
<Image.PreviewGroup items={imageUrls}>
|
|
17
|
+
<div className="grid grid-cols-4 gap-2">
|
|
18
|
+
{imageUrls.map((src, idx) => (
|
|
19
|
+
<div key={src} className="overflow-hidden rounded-md border border-gray-300 bg-gray-200 p-2 aspect-square">
|
|
20
|
+
<Image
|
|
21
|
+
src={src}
|
|
22
|
+
alt={`gallery_${idx}`}
|
|
23
|
+
className="w-full h-full"
|
|
24
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
25
|
+
wrapperStyle={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
</Image.PreviewGroup>
|
|
31
|
+
</Modal>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import client from '../../../../../api/client'
|
|
2
|
+
import { isNewFormDataPage, resolveConditionalText } from '../../../../../functions'
|
|
3
|
+
import { saveFile } from '../../../../../functions/forms/form'
|
|
4
|
+
import { TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../../enums'
|
|
5
|
+
import { RcFile, UploadFile, UploadProps } from 'antd/es/upload'
|
|
6
|
+
import { Form, Modal, Spin, Upload } from 'antd'
|
|
7
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
8
|
+
import { FaUpload } from 'react-icons/fa6'
|
|
9
|
+
import { Button_FillerPortal } from '../../../../common/button'
|
|
10
|
+
import { useNotification, useTranslation } from '../../../../common/custom-hooks'
|
|
11
|
+
import useGetCurrentBreakpoint from '../../../../common/custom-hooks/use-window-width.hook'
|
|
12
|
+
import { IElementBaseProps } from '..'
|
|
13
|
+
import { IFormContext } from '../../1-row'
|
|
14
|
+
|
|
15
|
+
export default function GalleryUploadModal({
|
|
16
|
+
isOpen,
|
|
17
|
+
formContext,
|
|
18
|
+
formItem,
|
|
19
|
+
elementKey,
|
|
20
|
+
textConditions,
|
|
21
|
+
onDiscardSuccess,
|
|
22
|
+
onSaveSuccess,
|
|
23
|
+
}: {
|
|
24
|
+
isOpen: boolean
|
|
25
|
+
formContext: IFormContext
|
|
26
|
+
onDiscardSuccess: () => void
|
|
27
|
+
onSaveSuccess: (blobNames: string[]) => void
|
|
28
|
+
} & IElementBaseProps) {
|
|
29
|
+
const { formRef, formDataId, companyKey, formId } = formContext
|
|
30
|
+
const { t } = useTranslation(formId)
|
|
31
|
+
const currentBreakpoint = useGetCurrentBreakpoint()
|
|
32
|
+
const { warningModal } = useNotification()
|
|
33
|
+
|
|
34
|
+
const [uploadedFiles, setUploadedFiles] = useState<UploadFile[]>([])
|
|
35
|
+
const [isUploadLoading, setIsUploadLoading] = useState(false)
|
|
36
|
+
const [isSaving, setIsSaving] = useState(false)
|
|
37
|
+
const [isDiscarding, setIsDiscarding] = useState(false)
|
|
38
|
+
|
|
39
|
+
const allValues = Form.useWatch([], { form: formRef, preserve: true })
|
|
40
|
+
|
|
41
|
+
const label = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
resolveConditionalText({
|
|
44
|
+
elementKey,
|
|
45
|
+
textConditions,
|
|
46
|
+
t,
|
|
47
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
48
|
+
formDataId,
|
|
49
|
+
currentBreakpoint,
|
|
50
|
+
type: TranslationTextTypeEnum.Label,
|
|
51
|
+
}),
|
|
52
|
+
[allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const placeholder = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
resolveConditionalText({
|
|
58
|
+
elementKey,
|
|
59
|
+
textConditions,
|
|
60
|
+
t,
|
|
61
|
+
fieldValues: (allValues ?? {}) as Record<string, any>,
|
|
62
|
+
formDataId,
|
|
63
|
+
currentBreakpoint,
|
|
64
|
+
type: TranslationTextTypeEnum.Placeholder,
|
|
65
|
+
}),
|
|
66
|
+
[allValues, currentBreakpoint, elementKey, formDataId, t, textConditions],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const [title = label, text = placeholder, discardText, saveText] = useMemo(
|
|
70
|
+
() =>
|
|
71
|
+
t([
|
|
72
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.UploadModalTitle },
|
|
73
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Placeholder, subType: TranslationTextSubTypeEnum.Secondary },
|
|
74
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.Discard },
|
|
75
|
+
{ key: elementKey, type: TranslationTextTypeEnum.Label, subType: TranslationTextSubTypeEnum.UploadSave },
|
|
76
|
+
]),
|
|
77
|
+
[elementKey, label, placeholder, t],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const uploadedBlobNames = useMemo(
|
|
81
|
+
() =>
|
|
82
|
+
uploadedFiles
|
|
83
|
+
.map((file) =>
|
|
84
|
+
typeof file.response === 'object' && file.response ? (file.response as { blobName?: string }).blobName : '',
|
|
85
|
+
)
|
|
86
|
+
.filter((blobName): blobName is string => !!blobName),
|
|
87
|
+
[uploadedFiles],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const uploadProps: UploadProps = {
|
|
91
|
+
multiple: true,
|
|
92
|
+
accept: 'image/*',
|
|
93
|
+
onChange: ({ fileList }) => setUploadedFiles(fileList),
|
|
94
|
+
showUploadList: { showRemoveIcon: false },
|
|
95
|
+
beforeUpload: (file) => {
|
|
96
|
+
if (!file.type.startsWith('image/')) {
|
|
97
|
+
warningModal({ title: '', content: 'Invalid file type!', okText: 'Okay' })
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
return true
|
|
101
|
+
},
|
|
102
|
+
customRequest: async ({ file, onSuccess, onError }) => {
|
|
103
|
+
setIsUploadLoading(true)
|
|
104
|
+
const blobName = await saveFile(file as RcFile, (file as RcFile).name, companyKey)
|
|
105
|
+
setIsUploadLoading(false)
|
|
106
|
+
if (blobName) onSuccess?.({ blobName }, file as RcFile)
|
|
107
|
+
else onError?.(new Error('Upload failed'))
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const discardUploadedFiles = useCallback(async () => {
|
|
112
|
+
setIsDiscarding(true)
|
|
113
|
+
try {
|
|
114
|
+
await Promise.all(uploadedBlobNames.map((blobName) => client.delete(`/api/attachment/${blobName}`)))
|
|
115
|
+
onDiscardSuccess()
|
|
116
|
+
} finally {
|
|
117
|
+
setIsDiscarding(false)
|
|
118
|
+
}
|
|
119
|
+
}, [onDiscardSuccess, uploadedBlobNames])
|
|
120
|
+
|
|
121
|
+
const saveUploadedFiles = useCallback(async () => {
|
|
122
|
+
if (!formId || !formDataId || isNewFormDataPage(formDataId)) return
|
|
123
|
+
|
|
124
|
+
setIsSaving(true)
|
|
125
|
+
try {
|
|
126
|
+
const formDataRes = await client.get(`/api/formdata/${formId}/${formDataId}`)
|
|
127
|
+
const parsedData = JSON.parse(formDataRes.data.data)
|
|
128
|
+
const fieldPath = Array.isArray(formItem.path) ? formItem.path : [formItem.path]
|
|
129
|
+
const existingBlobNames = fieldPath.reduce((current: any, key) => current?.[key], parsedData)
|
|
130
|
+
let current = parsedData
|
|
131
|
+
fieldPath.slice(0, -1).forEach((key) => {
|
|
132
|
+
current = current[key]
|
|
133
|
+
})
|
|
134
|
+
current[fieldPath[fieldPath.length - 1]] = [
|
|
135
|
+
...(Array.isArray(existingBlobNames) ? existingBlobNames : []),
|
|
136
|
+
...uploadedBlobNames,
|
|
137
|
+
]
|
|
138
|
+
const res = await client.put(`/api/formdata/${formId}/${formDataId}`, {
|
|
139
|
+
name: formDataRes.data.name,
|
|
140
|
+
version: formDataRes.data.version,
|
|
141
|
+
private: formDataRes.data.private,
|
|
142
|
+
data: JSON.stringify(parsedData),
|
|
143
|
+
})
|
|
144
|
+
if (res.status === 200) onSaveSuccess(uploadedBlobNames)
|
|
145
|
+
} finally {
|
|
146
|
+
setIsSaving(false)
|
|
147
|
+
}
|
|
148
|
+
}, [formDataId, formId, formItem.path, onSaveSuccess, uploadedBlobNames])
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Modal
|
|
152
|
+
open={isOpen}
|
|
153
|
+
closable={false}
|
|
154
|
+
maskClosable={false}
|
|
155
|
+
onCancel={() => {}}
|
|
156
|
+
title={title || 'Upload pictures'}
|
|
157
|
+
footer={
|
|
158
|
+
<div className="flex justify-between">
|
|
159
|
+
<Button_FillerPortal outline disabled={isUploadLoading} loading={isDiscarding} onClick={discardUploadedFiles}>
|
|
160
|
+
{discardText || 'Discard & Close'}
|
|
161
|
+
</Button_FillerPortal>
|
|
162
|
+
<Button_FillerPortal
|
|
163
|
+
primary
|
|
164
|
+
disabled={isUploadLoading || isDiscarding}
|
|
165
|
+
loading={isSaving}
|
|
166
|
+
onClick={saveUploadedFiles}
|
|
167
|
+
>
|
|
168
|
+
{saveText || 'Save Pictures'}
|
|
169
|
+
</Button_FillerPortal>
|
|
170
|
+
</div>
|
|
171
|
+
}
|
|
172
|
+
>
|
|
173
|
+
<Spin spinning={isUploadLoading}>
|
|
174
|
+
<Upload.Dragger {...uploadProps} fileList={uploadedFiles}>
|
|
175
|
+
<div className="flex flex-col items-center">
|
|
176
|
+
<FaUpload size={24} className="text-primary" />
|
|
177
|
+
<span className="font-semibold mt-2 text-secondary">
|
|
178
|
+
{text || 'Click or drag image files to this area'}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
</Upload.Dragger>
|
|
182
|
+
</Spin>
|
|
183
|
+
</Modal>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { Form, Input, Spin, Upload } from 'antd'
|
|
2
2
|
import { memo, useCallback, useMemo, useState } from 'react'
|
|
3
|
-
import { Button_FillerPortal } from '../../../common/button'
|
|
4
|
-
import client from '../../../../api/client'
|
|
5
|
-
import { FaEye, FaFileCircleCheck, FaUpload } from 'react-icons/fa6'
|
|
6
|
-
import { IFileUploadElementProps, IFileUploadElementRules } from '../../../../types'
|
|
7
|
-
import { useNotification } from '../../../common/custom-hooks'
|
|
8
3
|
import { FaTrashAlt } from 'react-icons/fa'
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import { IFormContext } from '../1-row'
|
|
4
|
+
import { FaEye, FaFileCircleCheck, FaUpload } from 'react-icons/fa6'
|
|
5
|
+
import client from '../../../../api/client'
|
|
12
6
|
import { ELEMENTS_DEFAULT_CLASS } from '../../../../constants'
|
|
7
|
+
import { resolveConditionalText } from '../../../../functions'
|
|
8
|
+
import { saveFile } from '../../../../functions/forms/form'
|
|
13
9
|
import { TranslationTextSubTypeEnum, TranslationTextTypeEnum } from '../../../../enums'
|
|
10
|
+
import { IFileUploadElementProps, IFileUploadElementRules } from '../../../../types'
|
|
11
|
+
import { Button_FillerPortal } from '../../../common/button'
|
|
12
|
+
import { useNotification } from '../../../common/custom-hooks'
|
|
14
13
|
import { useTranslation } from '../../../common/custom-hooks/use-translation.hook/hook'
|
|
15
14
|
import { translationStore } from '../../../common/custom-hooks/use-translation.hook/store'
|
|
16
15
|
import useGetCurrentBreakpoint from '../../../common/custom-hooks/use-window-width.hook'
|
|
17
|
-
import {
|
|
16
|
+
import { IElementBaseProps } from '.'
|
|
17
|
+
import { IFormContext } from '../1-row'
|
|
18
18
|
|
|
19
19
|
const { VITE_API_BASE_URL } = import.meta.env
|
|
20
20
|
|
|
@@ -260,4 +260,3 @@ const getUploadRuleErrorMsg = (
|
|
|
260
260
|
|
|
261
261
|
return null
|
|
262
262
|
}
|
|
263
|
-
|
|
@@ -33,6 +33,7 @@ const layoutConfigCache = new Map<string, LayoutCacheEntry>()
|
|
|
33
33
|
function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }: ILayoutRenderer_LoadFormData) {
|
|
34
34
|
const { formId, formRef, formDataId } = formContext
|
|
35
35
|
const { joins, baseFormId, targetFormId, idMatch, m2mRelFormId, displayConfig, filter: initialFilter } = elementProps
|
|
36
|
+
const savedDisplayConfig = displayConfig?.config
|
|
36
37
|
const defaultHeaderFilter = useMemo(() => {
|
|
37
38
|
if (!initialFilter || initialFilter.type !== FilterConfigTypeEnum.Custom) return ''
|
|
38
39
|
return JSON.stringify(initialFilter.config).replaceAll('@', '$')
|
|
@@ -67,7 +68,7 @@ function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }:
|
|
|
67
68
|
)
|
|
68
69
|
const [loading, setLoading] = useState(() => !cachedDataEntry)
|
|
69
70
|
const [checkingForUpdates, setCheckingForUpdates] = useState(false)
|
|
70
|
-
const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(targetFormId)
|
|
71
|
+
const { cachedConfig, isConfigLoading } = useCacheFormLayoutConfig(savedDisplayConfig ? undefined : targetFormId)
|
|
71
72
|
const [hasInitialData, setHasInitialData] = useState(() => Boolean(cachedDataEntry))
|
|
72
73
|
const lastChildInfo = useRef<{ formName: string; parentFormJoins: IFormJoin[] }>({
|
|
73
74
|
formName: '',
|
|
@@ -211,6 +212,10 @@ function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }:
|
|
|
211
212
|
|
|
212
213
|
useEffect(() => {
|
|
213
214
|
if (!layoutCacheKey || !cachedLayoutEntry) return
|
|
215
|
+
if (savedDisplayConfig) {
|
|
216
|
+
setListLayoutConfig((curr) => curr ?? cachedLayoutEntry.layout)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
214
219
|
if (cachedLayoutPrimedRef.current) {
|
|
215
220
|
triggerInitialFetch()
|
|
216
221
|
return
|
|
@@ -222,7 +227,65 @@ function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }:
|
|
|
222
227
|
m2mForeignKeysRef.current = cachedLayoutEntry.m2m
|
|
223
228
|
setListLayoutConfig((curr) => curr ?? cachedLayoutEntry.layout)
|
|
224
229
|
triggerInitialFetch()
|
|
225
|
-
}, [cachedLayoutEntry, layoutCacheKey, triggerInitialFetch])
|
|
230
|
+
}, [cachedLayoutEntry, layoutCacheKey, savedDisplayConfig, triggerInitialFetch])
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (!targetFormId || !displayConfig?.translations) return
|
|
234
|
+
translationStore.setTranslations(targetFormId, displayConfig.translations)
|
|
235
|
+
}, [displayConfig?.translations, targetFormId])
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (isNewFormDataPage(formDataId) || !savedDisplayConfig || !targetFormId) return
|
|
239
|
+
|
|
240
|
+
lastChildInfo.current = { formName: displayConfig?.formName ?? '', parentFormJoins: joins }
|
|
241
|
+
formJoinsRef.current = mergeJoins([
|
|
242
|
+
...savedDisplayConfig.columns.map((c) => c.joins || []),
|
|
243
|
+
...Object.values(savedDisplayConfig.header.elements).reduce((c: IFormJoin[], el) => {
|
|
244
|
+
if (el.props && 'filter' in el.props && Array.isArray(el.props.filter?.joins)) {
|
|
245
|
+
return [...c, el.props.filter.joins]
|
|
246
|
+
}
|
|
247
|
+
return c
|
|
248
|
+
}, []),
|
|
249
|
+
])
|
|
250
|
+
dataProjectRef.current = savedDisplayConfig.columns.reduce((curr, c) => {
|
|
251
|
+
if (!c.key) return curr
|
|
252
|
+
|
|
253
|
+
return { ...curr, [getProjectionKey(c.key)]: `$${c.key}` }
|
|
254
|
+
}, {})
|
|
255
|
+
m2mForeignKeysRef.current =
|
|
256
|
+
displayConfig?.m2mCurrentFormFK && displayConfig?.m2mOtherFormFK
|
|
257
|
+
? { currentForm: displayConfig.m2mCurrentFormFK, otherForm: displayConfig.m2mOtherFormFK }
|
|
258
|
+
: undefined
|
|
259
|
+
|
|
260
|
+
const nextLayoutConfig = {
|
|
261
|
+
configForFormId: targetFormId,
|
|
262
|
+
dataListConfig: {
|
|
263
|
+
...savedDisplayConfig,
|
|
264
|
+
columns: savedDisplayConfig.columns.map((c) => (c.key ? { ...c, key: getProjectionKey(c.key) } : c)),
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
setListLayoutConfig(nextLayoutConfig)
|
|
268
|
+
if (layoutCacheKey)
|
|
269
|
+
layoutConfigCache.set(layoutCacheKey, {
|
|
270
|
+
layout: nextLayoutConfig,
|
|
271
|
+
joins: formJoinsRef.current,
|
|
272
|
+
project: { ...dataProjectRef.current },
|
|
273
|
+
lastChildInfo: lastChildInfo.current,
|
|
274
|
+
m2m: m2mForeignKeysRef.current,
|
|
275
|
+
})
|
|
276
|
+
cachedLayoutPrimedRef.current = true
|
|
277
|
+
triggerInitialFetch()
|
|
278
|
+
}, [
|
|
279
|
+
displayConfig?.formName,
|
|
280
|
+
displayConfig?.m2mCurrentFormFK,
|
|
281
|
+
displayConfig?.m2mOtherFormFK,
|
|
282
|
+
formDataId,
|
|
283
|
+
joins,
|
|
284
|
+
layoutCacheKey,
|
|
285
|
+
savedDisplayConfig,
|
|
286
|
+
targetFormId,
|
|
287
|
+
triggerInitialFetch,
|
|
288
|
+
])
|
|
226
289
|
|
|
227
290
|
useEffect(() => {
|
|
228
291
|
if (cachedConfig?.id && cachedConfig.translations)
|
|
@@ -230,7 +293,7 @@ function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }:
|
|
|
230
293
|
}, [cachedConfig])
|
|
231
294
|
|
|
232
295
|
useEffect(() => {
|
|
233
|
-
if (isNewFormDataPage(formDataId) || !cachedConfig) return
|
|
296
|
+
if (savedDisplayConfig || isNewFormDataPage(formDataId) || !cachedConfig) return
|
|
234
297
|
|
|
235
298
|
const { relationships = [] } = cachedConfig
|
|
236
299
|
|
|
@@ -293,10 +356,10 @@ function LayoutRenderer_LoadFormData({ elementKey, formContext, elementProps }:
|
|
|
293
356
|
cachedConfig,
|
|
294
357
|
displayConfig?.id,
|
|
295
358
|
formDataId,
|
|
296
|
-
triggerInitialFetch,
|
|
297
|
-
baseFormId,
|
|
298
|
-
targetFormId,
|
|
299
359
|
layoutCacheKey,
|
|
360
|
+
savedDisplayConfig,
|
|
361
|
+
targetFormId,
|
|
362
|
+
triggerInitialFetch,
|
|
300
363
|
])
|
|
301
364
|
|
|
302
365
|
useEffect(
|
|
@@ -60,6 +60,7 @@ const LayoutRenderer_AutoComplete = lazy(() => import('./14-auto-complete'))
|
|
|
60
60
|
const LayoutRenderer_FormSubmissionPdf = lazy(() => import('./15-form-submission-pdf'))
|
|
61
61
|
const LayoutRenderer_UserRoles = lazy(() => import('./16-user-role'))
|
|
62
62
|
const LayoutRenderer_CustomComponent = lazy(() => import('./17-custom-component'))
|
|
63
|
+
const LayoutRenderer_Gallery = lazy(() => import('./18-gallery'))
|
|
63
64
|
|
|
64
65
|
type ILayoutRendererElement = {
|
|
65
66
|
basePath: (string | number)[]
|
|
@@ -306,6 +307,18 @@ function LayoutRendererElement({
|
|
|
306
307
|
)
|
|
307
308
|
},
|
|
308
309
|
|
|
310
|
+
[ElementTypeEnum.Gallery]: () => {
|
|
311
|
+
if (!key) return <span className="text-warning">Gallery field could not render due to missing KEY!</span>
|
|
312
|
+
return (
|
|
313
|
+
<LayoutRenderer_Gallery
|
|
314
|
+
elementProps={props}
|
|
315
|
+
formContext={formContext}
|
|
316
|
+
validations={validationsAfterIsHidden}
|
|
317
|
+
{...elementBaseProps}
|
|
318
|
+
/>
|
|
319
|
+
)
|
|
320
|
+
},
|
|
321
|
+
|
|
309
322
|
[ElementTypeEnum.ReadFieldData]: () => {
|
|
310
323
|
let style = (elementData as IReadFieldDataElement).style?.[currentBreakpoint]
|
|
311
324
|
if (!style) style = (elementData as IReadFieldDataElement).style?.[DeviceBreakpointEnum.Default]
|
package/src/enums/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export enum ElementTypeEnum {
|
|
|
17
17
|
RangePicker = 'RangePicker',
|
|
18
18
|
TimePicker = 'TimePicker',
|
|
19
19
|
FileUpload = 'FileUpload',
|
|
20
|
+
Gallery = 'Gallery',
|
|
20
21
|
Switch = 'Switch',
|
|
21
22
|
Container = 'Container',
|
|
22
23
|
Placeholder = 'Placeholder',
|
|
@@ -134,7 +135,13 @@ export enum TranslationTextSubTypeEnum {
|
|
|
134
135
|
RepeatButtonAdd = 'RepeatAdd',
|
|
135
136
|
RepeatButtonRemove = 'RepeatRemove',
|
|
136
137
|
Secondary = '2',
|
|
138
|
+
EmptyState = 'EmptyState',
|
|
139
|
+
DeleteModalTitle = 'DeleteModalTitle',
|
|
140
|
+
DeleteModalDiscard = 'DeleteModalDiscard',
|
|
141
|
+
DeleteModalDelete = 'DeleteModalDelete',
|
|
137
142
|
UploadFileType = 'FileType',
|
|
143
|
+
UploadModalTitle = 'UploadModalTitle',
|
|
144
|
+
UploadSave = 'UploadSave',
|
|
138
145
|
UploadSizeLimit = 'SizeLimit',
|
|
139
146
|
ConfirmationTitle = 'ConfTitle',
|
|
140
147
|
ConfirmationMsg = 'ConfMsg',
|
|
@@ -22,12 +22,15 @@ import {
|
|
|
22
22
|
IDataFetchConfig,
|
|
23
23
|
IEvaluationConfig,
|
|
24
24
|
IFilterConfig,
|
|
25
|
+
IFormDataListConfig,
|
|
26
|
+
IFormTranslations,
|
|
25
27
|
IReadFieldDataElementProps,
|
|
26
28
|
IReportTableElementProps,
|
|
27
29
|
} from '..'
|
|
28
30
|
import {
|
|
29
31
|
AlignTypeEnum,
|
|
30
32
|
CountryEnum,
|
|
33
|
+
DeviceBreakpointEnum,
|
|
31
34
|
ElementTypeEnum,
|
|
32
35
|
FileTypeEnum,
|
|
33
36
|
DndLayoutAddableNodeEnum,
|
|
@@ -179,6 +182,23 @@ export interface IFileUploadElementRules {
|
|
|
179
182
|
|
|
180
183
|
/** -------------------------------------------------------------------------------------------- */
|
|
181
184
|
|
|
185
|
+
export interface IGalleryElement extends BaseFormLayoutElement {
|
|
186
|
+
elementType: ElementTypeEnum.Gallery
|
|
187
|
+
props: IGalleryElementProps
|
|
188
|
+
}
|
|
189
|
+
export interface IGalleryElementProps {
|
|
190
|
+
hasNoLabel?: boolean
|
|
191
|
+
grid?: Partial<Record<DeviceBreakpointEnum, IGalleryElementGridConfig | null>>
|
|
192
|
+
uploadButtonConditions?: IFormLayoutElementConditions
|
|
193
|
+
deleteButtonConditions?: IFormLayoutElementConditions
|
|
194
|
+
}
|
|
195
|
+
export interface IGalleryElementGridConfig {
|
|
196
|
+
count?: number | null
|
|
197
|
+
row?: number | null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** -------------------------------------------------------------------------------------------- */
|
|
201
|
+
|
|
182
202
|
export interface IBooleanElement extends BaseFormLayoutElement {
|
|
183
203
|
elementType: ElementTypeEnum.Switch | ElementTypeEnum.Checkbox
|
|
184
204
|
props: {
|
|
@@ -284,9 +304,14 @@ export interface IFormDataLoadElementProps extends IDataFetchConfig {
|
|
|
284
304
|
filter?: IFilterConfig
|
|
285
305
|
pageBreak?: ReportPageBreakEnum // used for report
|
|
286
306
|
}
|
|
287
|
-
interface IFormDataLoadDisplayConfig {
|
|
307
|
+
export interface IFormDataLoadDisplayConfig {
|
|
288
308
|
id: string
|
|
289
309
|
name: string
|
|
310
|
+
config?: IFormDataListConfig
|
|
311
|
+
formName?: string
|
|
312
|
+
translations?: IFormTranslations
|
|
313
|
+
m2mCurrentFormFK?: string
|
|
314
|
+
m2mOtherFormFK?: string
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
/** -------------------------------------------------------------------------------------------- */
|
package/src/types/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
IFormDataLoadElement,
|
|
13
13
|
IFormDndLayoutRowHeader,
|
|
14
14
|
IFormSubmissionPdfElement,
|
|
15
|
+
IGalleryElement,
|
|
15
16
|
IGridContainerConfig,
|
|
16
17
|
IImageElement,
|
|
17
18
|
IInputElement,
|
|
@@ -77,6 +78,7 @@ export type IDndLayoutElement =
|
|
|
77
78
|
| IReadFieldDataElement
|
|
78
79
|
| IBooleanElement
|
|
79
80
|
| IFileUploadElement
|
|
81
|
+
| IGalleryElement
|
|
80
82
|
| IButtonElement
|
|
81
83
|
| ISignatureElement
|
|
82
84
|
| ITextElement
|