@7365admin1/layer-common 3.2.1-staging.72 → 3.2.2
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/CHANGELOG.md +39 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/FeedbackMain.vue +7 -7
- package/components/HidAccessLogDashboard.vue +44 -119
- package/components/HidIdentityMapping.vue +975 -0
- package/components/HidIntercomManagement.vue +201 -808
- package/components/HidQrCodeConfiguration.vue +95 -907
- package/components/HidServiceSettingsPanel.vue +1 -20
- package/components/HidUserEnrollment.vue +81 -392
- package/components/Layout/NavigationDrawer.vue +2 -43
- package/components/NavigationItem.vue +0 -9
- package/components/Nfc/NFCPatrolRouteMain.vue +7 -52
- package/components/OnlineFormConfigurationForm.vue +224 -620
- package/components/OnlineFormFill.vue +8 -36
- package/components/OnlineFormsConfiguration.vue +2 -6
- package/components/QrTemplate/CreditCardLandscape.vue +12 -19
- package/components/QrTemplate/PrintDialog.vue +196 -209
- package/components/SiteSettings.vue +2 -4
- package/components/VisitorForm.vue +78 -201
- package/components/VisitorManagement.vue +2 -1
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +0 -127
- package/composables/useHidNavigation.ts +7 -0
- package/composables/useOnlineFormPages.ts +0 -2
- package/composables/useSiteSettings.ts +0 -50
- package/composables/useWebUsb.ts +37 -127
- package/middleware/member.ts +4 -0
- package/package.json +1 -2
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +56 -21
- package/plugins/vuetify.ts +9 -37
- package/types/site.d.ts +0 -71
- package/.changeset/camera-wall-gated-endpoint.md +0 -47
- package/.changeset/camera-wall-plain-english.md +0 -60
- package/.changeset/camera-wall-selection-and-guidance.md +0 -65
- package/.changeset/dark-primary-contrast.md +0 -39
- package/components/CameraWall.vue +0 -1034
- package/components/CameraWallTile.vue +0 -818
- package/components/HidCameraCaptureDialog.vue +0 -199
- package/composables/useSipWebPhone.ts +0 -311
- package/utils/camera-wall.test.ts +0 -571
- package/utils/camera-wall.ts +0 -926
- package/utils/theme.test.ts +0 -201
- package/utils/theme.ts +0 -136
|
@@ -1,427 +1,181 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-card width="100%"
|
|
2
|
+
<v-card width="100%">
|
|
3
3
|
<v-toolbar>
|
|
4
4
|
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
5
|
<span class="font-weight-bold text-h5">
|
|
6
|
-
{{ prop.mode === "edit" ? "Edit" : "
|
|
6
|
+
{{ prop.mode === "edit" ? "Edit" : "Add" }} Online Form
|
|
7
7
|
</span>
|
|
8
8
|
</v-row>
|
|
9
9
|
</v-toolbar>
|
|
10
10
|
|
|
11
|
-
<v-card-text style="
|
|
12
|
-
<v-form v-model="validForm">
|
|
13
|
-
<div class="pa-4 pb-0">
|
|
11
|
+
<v-card-text style="max-height: 76vh; overflow-y: auto" class="pa-0">
|
|
12
|
+
<v-form v-model="validForm" :disabled="disable">
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
<!-- Form Type -->
|
|
15
|
+
<v-col cols="12" class="px-6 pt-4 pb-0">
|
|
16
|
+
<InputLabel title="Form Type" />
|
|
17
|
+
|
|
18
|
+
<!-- Add mode: selectable list -->
|
|
19
|
+
<v-list
|
|
20
|
+
v-if="prop.mode !== 'edit'"
|
|
21
|
+
density="compact"
|
|
22
|
+
lines="one"
|
|
23
|
+
max-height="220"
|
|
24
|
+
style="overflow-y: auto; border: 1px solid rgba(0,0,0,0.18); border-radius: 4px;"
|
|
25
|
+
class="pa-0"
|
|
26
|
+
>
|
|
27
|
+
<v-list-item
|
|
28
|
+
v-if="availableFormTypes.length === 0"
|
|
29
|
+
disabled
|
|
30
|
+
title="All forms have already been added for this site"
|
|
31
|
+
class="text-caption text-medium-emphasis"
|
|
26
32
|
/>
|
|
27
|
-
<v-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
<v-list-item
|
|
34
|
+
v-for="name in availableFormTypes"
|
|
35
|
+
:key="name"
|
|
36
|
+
:title="name"
|
|
37
|
+
:active="selectedFormType === name"
|
|
38
|
+
active-color="primary"
|
|
39
|
+
class="text-caption"
|
|
40
|
+
@click="selectedFormType = name"
|
|
41
|
+
/>
|
|
42
|
+
</v-list>
|
|
31
43
|
|
|
32
|
-
<!--
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
color="primary"
|
|
38
|
-
class="text-none pa-0"
|
|
39
|
-
prepend-icon="mdi-file-outline"
|
|
40
|
-
@click="showTemplatePicker = true"
|
|
41
|
-
>
|
|
42
|
-
Start from existing template
|
|
43
|
-
</v-btn>
|
|
44
|
-
</div>
|
|
44
|
+
<!-- Edit mode: read-only label -->
|
|
45
|
+
<v-chip v-else color="primary" variant="tonal" size="small">
|
|
46
|
+
{{ prop.onlineForm.formType }}
|
|
47
|
+
</v-chip>
|
|
48
|
+
</v-col>
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
<div v-if="prop.mode === 'edit'" class="mb-3">
|
|
48
|
-
<InputLabel title="Status" />
|
|
49
|
-
<v-select
|
|
50
|
-
v-model="formStatus"
|
|
51
|
-
:items="['active', 'inactive']"
|
|
52
|
-
density="compact"
|
|
53
|
-
variant="outlined"
|
|
54
|
-
hide-details
|
|
55
|
-
/>
|
|
56
|
-
</div>
|
|
50
|
+
<v-divider class="mt-4" />
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
hide-details
|
|
66
|
-
color="primary"
|
|
67
|
-
inset
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
<v-text-field
|
|
71
|
-
v-if="requiresPayment"
|
|
72
|
-
v-model.number="paymentAmount"
|
|
73
|
-
density="compact"
|
|
74
|
-
variant="outlined"
|
|
75
|
-
type="number"
|
|
76
|
-
min="1"
|
|
77
|
-
label="Payment Amount (SGD)"
|
|
78
|
-
prefix="$"
|
|
79
|
-
class="mb-3"
|
|
80
|
-
hide-details="auto"
|
|
81
|
-
:rules="requiresPayment ? [v => (v > 0) || 'Amount must be greater than 0'] : []"
|
|
52
|
+
<!-- Status (edit mode only) -->
|
|
53
|
+
<v-col v-if="prop.mode === 'edit'" cols="12" class="px-6 pt-3 pb-0">
|
|
54
|
+
<InputLabel title="Status" />
|
|
55
|
+
<v-select
|
|
56
|
+
v-model="formStatus"
|
|
57
|
+
:items="['active', 'inactive']"
|
|
58
|
+
density="comfortable"
|
|
82
59
|
/>
|
|
60
|
+
</v-col>
|
|
83
61
|
|
|
84
|
-
|
|
85
|
-
|
|
62
|
+
<!-- Attachment -->
|
|
63
|
+
<!-- <v-col cols="12" class="px-6 pt-3 pb-4">
|
|
64
|
+
<InputLabel title="Attachment (Optional)" />
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<v-list-subheader>Input Fields</v-list-subheader>
|
|
113
|
-
<v-list-item title="Text / Number / Date Field" prepend-icon="mdi-form-textbox" @click="addBlock('field')" />
|
|
114
|
-
<v-list-item title="Dropdown (Select)" prepend-icon="mdi-chevron-down-box-outline" @click="addBlock('field-select')" />
|
|
115
|
-
<v-list-item title="Checkbox Group" prepend-icon="mdi-checkbox-multiple-outline" @click="addBlock('checkbox-group')" />
|
|
116
|
-
<v-list-item title="Signature" prepend-icon="mdi-draw" @click="addBlock('signature-row')" />
|
|
117
|
-
</v-list>
|
|
118
|
-
</v-menu>
|
|
66
|
+
<div v-if="attachmentId" class="d-flex align-center" style="gap: 8px">
|
|
67
|
+
<v-card
|
|
68
|
+
variant="outlined"
|
|
69
|
+
border="thin"
|
|
70
|
+
class="px-3 flex-grow-1 text-caption"
|
|
71
|
+
height="40"
|
|
72
|
+
:href="getFileUrl(attachmentId)"
|
|
73
|
+
target="_blank"
|
|
74
|
+
>
|
|
75
|
+
<v-row no-gutters class="fill-height" align="center" style="gap: 6px">
|
|
76
|
+
<v-icon size="15">mdi-file-document-outline</v-icon>
|
|
77
|
+
{{ attachmentFileName || "View attachment" }}
|
|
78
|
+
<v-icon size="13" class="ml-auto">mdi-open-in-new</v-icon>
|
|
79
|
+
</v-row>
|
|
80
|
+
</v-card>
|
|
81
|
+
<v-btn
|
|
82
|
+
icon
|
|
83
|
+
variant="text"
|
|
84
|
+
size="small"
|
|
85
|
+
density="comfortable"
|
|
86
|
+
:disabled="disable"
|
|
87
|
+
@click="clearAttachment"
|
|
88
|
+
>
|
|
89
|
+
<v-icon size="17">mdi-close</v-icon>
|
|
90
|
+
</v-btn>
|
|
119
91
|
</div>
|
|
120
92
|
|
|
121
|
-
<!-- Empty state -->
|
|
122
93
|
<div
|
|
123
|
-
v-
|
|
124
|
-
class="
|
|
125
|
-
|
|
94
|
+
v-else
|
|
95
|
+
class="upload-drop-zone"
|
|
96
|
+
:class="{ 'drag-over': isDragOver, 'is-uploading': isUploading }"
|
|
97
|
+
@dragover.prevent="isDragOver = true"
|
|
98
|
+
@dragleave.prevent="isDragOver = false"
|
|
99
|
+
@drop.prevent="handleDrop"
|
|
100
|
+
@click="fileInputRef?.click()"
|
|
126
101
|
>
|
|
127
|
-
|
|
102
|
+
<v-progress-circular
|
|
103
|
+
v-if="isUploading"
|
|
104
|
+
indeterminate
|
|
105
|
+
size="24"
|
|
106
|
+
width="2"
|
|
107
|
+
color="primary"
|
|
108
|
+
class="mb-1"
|
|
109
|
+
/>
|
|
110
|
+
<v-icon
|
|
111
|
+
v-else
|
|
112
|
+
size="26"
|
|
113
|
+
:color="isDragOver ? 'primary' : undefined"
|
|
114
|
+
class="mb-1 text-medium-emphasis"
|
|
115
|
+
>
|
|
116
|
+
mdi-cloud-upload-outline
|
|
117
|
+
</v-icon>
|
|
118
|
+
<span class="text-caption text-medium-emphasis">
|
|
119
|
+
{{ isUploading ? "Uploading…" : "Drop file here or click to browse" }}
|
|
120
|
+
</span>
|
|
121
|
+
<input
|
|
122
|
+
ref="fileInputRef"
|
|
123
|
+
type="file"
|
|
124
|
+
accept=".pdf,application/pdf,image/*"
|
|
125
|
+
style="display: none"
|
|
126
|
+
@change="handleFileInputChange"
|
|
127
|
+
/>
|
|
128
128
|
</div>
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class="mb-2"
|
|
135
|
-
>
|
|
136
|
-
<v-card variant="outlined" class="rounded-lg">
|
|
137
|
-
<!-- Header row -->
|
|
138
|
-
<div class="d-flex align-center px-3 py-2 ga-2">
|
|
139
|
-
<v-chip
|
|
140
|
-
size="x-small"
|
|
141
|
-
:color="blockTypeColor(block.type)"
|
|
142
|
-
variant="flat"
|
|
143
|
-
label
|
|
144
|
-
style="min-width: 110px; justify-content: center; flex-shrink: 0"
|
|
145
|
-
>
|
|
146
|
-
{{ blockTypeLabel(block.type) }}
|
|
147
|
-
</v-chip>
|
|
148
|
-
<span
|
|
149
|
-
class="text-caption text-medium-emphasis flex-grow-1"
|
|
150
|
-
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
|
|
151
|
-
>
|
|
152
|
-
{{ blockSummary(block) }}
|
|
153
|
-
</span>
|
|
154
|
-
<div class="d-flex align-center" style="gap: 2px; flex-shrink: 0">
|
|
155
|
-
<v-btn icon size="x-small" variant="text" :disabled="idx === 0" @click="moveBlock(idx, -1)">
|
|
156
|
-
<v-icon size="15">mdi-arrow-up</v-icon>
|
|
157
|
-
</v-btn>
|
|
158
|
-
<v-btn icon size="x-small" variant="text" :disabled="idx === blocks.length - 1" @click="moveBlock(idx, 1)">
|
|
159
|
-
<v-icon size="15">mdi-arrow-down</v-icon>
|
|
160
|
-
</v-btn>
|
|
161
|
-
<v-btn icon size="x-small" variant="text" :color="editingIdx === idx ? 'primary' : undefined" @click="toggleEdit(idx)">
|
|
162
|
-
<v-icon size="15">mdi-pencil</v-icon>
|
|
163
|
-
</v-btn>
|
|
164
|
-
<v-btn icon size="x-small" variant="text" color="error" @click="deleteBlock(idx)">
|
|
165
|
-
<v-icon size="15">mdi-delete</v-icon>
|
|
166
|
-
</v-btn>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<!-- Inline editor -->
|
|
171
|
-
<div v-if="editingIdx === idx">
|
|
172
|
-
<v-divider />
|
|
173
|
-
<div class="pa-3">
|
|
130
|
+
<span v-if="attachmentError" class="text-caption text-error d-block mt-1">
|
|
131
|
+
{{ attachmentError }}
|
|
132
|
+
</span>
|
|
133
|
+
</v-col> -->
|
|
174
134
|
|
|
175
|
-
|
|
176
|
-
<template v-if="block.type === 'section-header'">
|
|
177
|
-
<v-text-field v-model="block.text" label="Heading Text" density="compact" variant="outlined" hide-details />
|
|
178
|
-
</template>
|
|
179
|
-
|
|
180
|
-
<!-- paragraph -->
|
|
181
|
-
<template v-else-if="block.type === 'paragraph'">
|
|
182
|
-
<v-textarea v-model="block.text" label="Paragraph Text" density="compact" variant="outlined" rows="3" auto-grow hide-details />
|
|
183
|
-
</template>
|
|
184
|
-
|
|
185
|
-
<!-- ordered-list / unordered-list -->
|
|
186
|
-
<template v-else-if="block.type === 'ordered-list' || block.type === 'unordered-list'">
|
|
187
|
-
<v-text-field v-model="block.title" label="Section Title (optional)" density="compact" variant="outlined" class="mb-3" hide-details />
|
|
188
|
-
<div class="text-caption font-weight-medium mb-2">Items:</div>
|
|
189
|
-
<div
|
|
190
|
-
v-for="(item, itemIdx) in block.items"
|
|
191
|
-
:key="itemIdx"
|
|
192
|
-
class="d-flex align-center ga-2 mb-2"
|
|
193
|
-
>
|
|
194
|
-
<span class="text-caption text-medium-emphasis" style="min-width: 20px; text-align: right">
|
|
195
|
-
{{ block.type === 'ordered-list' ? `${itemIdx + 1}.` : '•' }}
|
|
196
|
-
</span>
|
|
197
|
-
<v-text-field
|
|
198
|
-
:model-value="item"
|
|
199
|
-
@update:model-value="block.items[itemIdx] = $event"
|
|
200
|
-
density="compact"
|
|
201
|
-
variant="outlined"
|
|
202
|
-
hide-details
|
|
203
|
-
class="flex-grow-1"
|
|
204
|
-
/>
|
|
205
|
-
<v-btn icon size="x-small" variant="text" color="error" @click="block.items.splice(itemIdx, 1)">
|
|
206
|
-
<v-icon size="13">mdi-close</v-icon>
|
|
207
|
-
</v-btn>
|
|
208
|
-
</div>
|
|
209
|
-
<v-btn size="x-small" variant="text" color="primary" class="text-none" prepend-icon="mdi-plus" @click="block.items.push('')">
|
|
210
|
-
Add item
|
|
211
|
-
</v-btn>
|
|
212
|
-
</template>
|
|
213
|
-
|
|
214
|
-
<!-- divider -->
|
|
215
|
-
<template v-else-if="block.type === 'divider'">
|
|
216
|
-
<span class="text-caption text-medium-emphasis">Horizontal divider — no settings needed.</span>
|
|
217
|
-
</template>
|
|
218
|
-
|
|
219
|
-
<!-- field -->
|
|
220
|
-
<template v-else-if="block.type === 'field'">
|
|
221
|
-
<v-row dense>
|
|
222
|
-
<v-col cols="12">
|
|
223
|
-
<v-text-field
|
|
224
|
-
v-model="block.field.label"
|
|
225
|
-
label="Field Label"
|
|
226
|
-
density="compact"
|
|
227
|
-
variant="outlined"
|
|
228
|
-
hide-details
|
|
229
|
-
@update:model-value="onLabelChange(block, $event)"
|
|
230
|
-
/>
|
|
231
|
-
</v-col>
|
|
232
|
-
<v-col cols="7">
|
|
233
|
-
<v-text-field
|
|
234
|
-
v-model="block.field.key"
|
|
235
|
-
label="Field Key"
|
|
236
|
-
density="compact"
|
|
237
|
-
variant="outlined"
|
|
238
|
-
hint="Auto-generated · editable"
|
|
239
|
-
persistent-hint
|
|
240
|
-
/>
|
|
241
|
-
</v-col>
|
|
242
|
-
<v-col cols="5">
|
|
243
|
-
<v-select
|
|
244
|
-
v-model="block.field.fieldType"
|
|
245
|
-
:items="fieldTypeOptions"
|
|
246
|
-
label="Type"
|
|
247
|
-
density="compact"
|
|
248
|
-
variant="outlined"
|
|
249
|
-
hide-details
|
|
250
|
-
@update:model-value="onFieldTypeChange(block, $event)"
|
|
251
|
-
/>
|
|
252
|
-
</v-col>
|
|
253
|
-
<v-col cols="12">
|
|
254
|
-
<v-switch
|
|
255
|
-
v-model="block.field.required"
|
|
256
|
-
label="Required"
|
|
257
|
-
density="compact"
|
|
258
|
-
hide-details
|
|
259
|
-
color="primary"
|
|
260
|
-
inset
|
|
261
|
-
/>
|
|
262
|
-
</v-col>
|
|
263
|
-
<!-- Options for select type -->
|
|
264
|
-
<v-col v-if="block.field.fieldType === 'select'" cols="12">
|
|
265
|
-
<div class="text-caption font-weight-medium mb-2">Dropdown Options:</div>
|
|
266
|
-
<div
|
|
267
|
-
v-for="(opt, optIdx) in (block.field.options ?? [])"
|
|
268
|
-
:key="optIdx"
|
|
269
|
-
class="d-flex align-center ga-2 mb-2"
|
|
270
|
-
>
|
|
271
|
-
<v-text-field
|
|
272
|
-
:model-value="opt"
|
|
273
|
-
@update:model-value="block.field.options[optIdx] = $event"
|
|
274
|
-
density="compact"
|
|
275
|
-
variant="outlined"
|
|
276
|
-
hide-details
|
|
277
|
-
class="flex-grow-1"
|
|
278
|
-
:label="`Option ${optIdx + 1}`"
|
|
279
|
-
/>
|
|
280
|
-
<v-btn icon size="x-small" variant="text" color="error" @click="block.field.options.splice(optIdx, 1)">
|
|
281
|
-
<v-icon size="13">mdi-close</v-icon>
|
|
282
|
-
</v-btn>
|
|
283
|
-
</div>
|
|
284
|
-
<v-btn size="x-small" variant="text" color="primary" class="text-none" prepend-icon="mdi-plus" @click="(block.field.options ??= []).push('')">
|
|
285
|
-
Add option
|
|
286
|
-
</v-btn>
|
|
287
|
-
</v-col>
|
|
288
|
-
</v-row>
|
|
289
|
-
</template>
|
|
290
|
-
|
|
291
|
-
<!-- checkbox-group -->
|
|
292
|
-
<template v-else-if="block.type === 'checkbox-group'">
|
|
293
|
-
<v-text-field v-model="block.title" label="Section Title (optional)" density="compact" variant="outlined" class="mb-3" hide-details />
|
|
294
|
-
<v-switch v-model="block.single" label="Single selection (radio behavior)" density="compact" hide-details color="primary" inset class="mb-3" />
|
|
295
|
-
<div class="text-caption font-weight-medium mb-2">Checkboxes:</div>
|
|
296
|
-
<div
|
|
297
|
-
v-for="(item, itemIdx) in block.items"
|
|
298
|
-
:key="itemIdx"
|
|
299
|
-
class="d-flex align-center ga-2 mb-2"
|
|
300
|
-
>
|
|
301
|
-
<v-text-field
|
|
302
|
-
v-model="item.label"
|
|
303
|
-
density="compact"
|
|
304
|
-
variant="outlined"
|
|
305
|
-
hide-details
|
|
306
|
-
label="Label"
|
|
307
|
-
class="flex-grow-1"
|
|
308
|
-
@update:model-value="onCheckboxLabelChange(item, $event)"
|
|
309
|
-
/>
|
|
310
|
-
<v-text-field
|
|
311
|
-
v-model="item.key"
|
|
312
|
-
density="compact"
|
|
313
|
-
variant="outlined"
|
|
314
|
-
hide-details
|
|
315
|
-
label="Key"
|
|
316
|
-
style="max-width: 130px"
|
|
317
|
-
/>
|
|
318
|
-
<v-btn icon size="x-small" variant="text" color="error" @click="block.items.splice(itemIdx, 1)">
|
|
319
|
-
<v-icon size="13">mdi-close</v-icon>
|
|
320
|
-
</v-btn>
|
|
321
|
-
</div>
|
|
322
|
-
<v-btn size="x-small" variant="text" color="primary" class="text-none" prepend-icon="mdi-plus" @click="block.items.push({ key: '', label: '' })">
|
|
323
|
-
Add checkbox
|
|
324
|
-
</v-btn>
|
|
325
|
-
</template>
|
|
326
|
-
|
|
327
|
-
<!-- signature-row -->
|
|
328
|
-
<template v-else-if="block.type === 'signature-row'">
|
|
329
|
-
<v-row dense>
|
|
330
|
-
<v-col cols="12">
|
|
331
|
-
<v-text-field
|
|
332
|
-
v-model="block.label"
|
|
333
|
-
label="Signature Label"
|
|
334
|
-
density="compact"
|
|
335
|
-
variant="outlined"
|
|
336
|
-
hide-details
|
|
337
|
-
@update:model-value="block.key = labelToKey($event)"
|
|
338
|
-
/>
|
|
339
|
-
</v-col>
|
|
340
|
-
<v-col cols="12">
|
|
341
|
-
<v-text-field
|
|
342
|
-
v-model="block.key"
|
|
343
|
-
label="Key"
|
|
344
|
-
density="compact"
|
|
345
|
-
variant="outlined"
|
|
346
|
-
hint="Unique identifier"
|
|
347
|
-
persistent-hint
|
|
348
|
-
/>
|
|
349
|
-
</v-col>
|
|
350
|
-
</v-row>
|
|
351
|
-
</template>
|
|
352
|
-
|
|
353
|
-
</div>
|
|
354
|
-
</div>
|
|
355
|
-
</v-card>
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
<!-- Error -->
|
|
360
|
-
<div v-if="message" class="px-4 pb-3">
|
|
135
|
+
<v-col v-if="message" cols="12" class="px-6">
|
|
361
136
|
<span class="text-caption text-error">{{ message }}</span>
|
|
362
|
-
</
|
|
137
|
+
</v-col>
|
|
363
138
|
</v-form>
|
|
364
139
|
</v-card-text>
|
|
365
140
|
|
|
366
|
-
<!-- Action bar -->
|
|
367
141
|
<v-toolbar density="compact">
|
|
368
142
|
<v-row no-gutters>
|
|
369
143
|
<v-col cols="6">
|
|
370
|
-
<v-btn
|
|
144
|
+
<v-btn
|
|
145
|
+
tile
|
|
146
|
+
block
|
|
147
|
+
variant="text"
|
|
148
|
+
class="text-none"
|
|
149
|
+
size="48"
|
|
150
|
+
@click="emit('cancel')"
|
|
151
|
+
>
|
|
152
|
+
Cancel
|
|
153
|
+
</v-btn>
|
|
371
154
|
</v-col>
|
|
372
155
|
<v-col cols="6">
|
|
373
156
|
<v-btn
|
|
374
|
-
tile
|
|
375
|
-
|
|
376
|
-
|
|
157
|
+
tile
|
|
158
|
+
block
|
|
159
|
+
variant="flat"
|
|
160
|
+
color="black"
|
|
161
|
+
class="text-none"
|
|
162
|
+
size="48"
|
|
163
|
+
:disabled="!canSubmit || disable"
|
|
164
|
+
:loading="disable"
|
|
377
165
|
@click="submit"
|
|
378
166
|
>
|
|
379
|
-
{{ prop.mode === "edit" ? "Update" : "
|
|
167
|
+
{{ prop.mode === "edit" ? "Update" : "Submit" }}
|
|
380
168
|
</v-btn>
|
|
381
169
|
</v-col>
|
|
382
170
|
</v-row>
|
|
383
171
|
</v-toolbar>
|
|
384
|
-
|
|
385
|
-
<!-- Template picker dialog -->
|
|
386
|
-
<v-dialog v-model="showTemplatePicker" width="520" scrollable>
|
|
387
|
-
<v-card>
|
|
388
|
-
<v-toolbar density="compact">
|
|
389
|
-
<span class="pl-4 font-weight-medium text-subtitle-1">Load Template</span>
|
|
390
|
-
</v-toolbar>
|
|
391
|
-
<div class="pt-3 px-4 pb-2 text-caption text-medium-emphasis" style="white-space: normal">
|
|
392
|
-
Select a template to pre-populate form blocks. You can customise them after loading.
|
|
393
|
-
</div>
|
|
394
|
-
<v-card-text style="max-height: 55vh; overflow-y: auto; padding: 0">
|
|
395
|
-
<v-list density="compact">
|
|
396
|
-
<v-list-item
|
|
397
|
-
v-if="sortedTemplateNames.length === 0"
|
|
398
|
-
disabled
|
|
399
|
-
title="All templates have already been added for this site"
|
|
400
|
-
class="text-caption text-medium-emphasis"
|
|
401
|
-
/>
|
|
402
|
-
<v-list-item
|
|
403
|
-
v-for="name in sortedTemplateNames"
|
|
404
|
-
:key="name"
|
|
405
|
-
:title="name"
|
|
406
|
-
class="text-caption"
|
|
407
|
-
@click="loadTemplate(name)"
|
|
408
|
-
/>
|
|
409
|
-
</v-list>
|
|
410
|
-
</v-card-text>
|
|
411
|
-
<v-toolbar density="compact">
|
|
412
|
-
<v-btn tile block variant="text" class="text-none" size="48" @click="showTemplatePicker = false">Close</v-btn>
|
|
413
|
-
</v-toolbar>
|
|
414
|
-
</v-card>
|
|
415
|
-
</v-dialog>
|
|
416
172
|
</v-card>
|
|
417
173
|
</template>
|
|
418
|
-
|
|
419
174
|
<script setup lang="ts">
|
|
175
|
+
import useFile from "../composables/useFile";
|
|
420
176
|
import useOnlineFormConfiguration from "../composables/useOnlineFormConfiguration";
|
|
421
177
|
import useOnlineFormPages from "../composables/useOnlineFormPages";
|
|
422
178
|
|
|
423
|
-
type AnyBlock = Record<string, any>;
|
|
424
|
-
|
|
425
179
|
const prop = defineProps({
|
|
426
180
|
mode: { type: String, default: "add" },
|
|
427
181
|
onlineForm: {
|
|
@@ -434,289 +188,139 @@ const emit = defineEmits(["cancel", "success"]);
|
|
|
434
188
|
|
|
435
189
|
const { formPages } = useOnlineFormPages();
|
|
436
190
|
const { create: _add, updateById: _update, getAll: _getAll } = useOnlineFormConfiguration();
|
|
191
|
+
const { addFile, getFileUrl } = useFile();
|
|
437
192
|
|
|
438
193
|
const route = useRoute();
|
|
439
194
|
const siteId = route.params.site as string;
|
|
440
195
|
const orgId = route.params.org as string;
|
|
441
196
|
|
|
442
|
-
//
|
|
443
|
-
const
|
|
444
|
-
const formStatus = ref<string>(prop.onlineForm?.status || "active");
|
|
445
|
-
const requiresPayment = ref(false);
|
|
446
|
-
const paymentAmount = ref<number>(0);
|
|
447
|
-
const validForm = ref(false);
|
|
448
|
-
const loading = ref(false);
|
|
449
|
-
const message = ref("");
|
|
450
|
-
|
|
451
|
-
// ── Blocks ───────────────────────────────────────────────────────────────────
|
|
452
|
-
let _uid = 0;
|
|
453
|
-
function nextUid() { return `b${++_uid}`; }
|
|
197
|
+
// --- form type selection (add mode) ---
|
|
198
|
+
const selectedFormType = ref("");
|
|
454
199
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// ── Template picker ───────────────────────────────────────────────────────────
|
|
459
|
-
const showTemplatePicker = ref(false);
|
|
460
|
-
const existingFormTypes = ref<string[]>([]);
|
|
461
|
-
const sortedTemplateNames = computed(() =>
|
|
462
|
-
Object.keys(formPages)
|
|
463
|
-
.filter((name) => !existingFormTypes.value.includes(name))
|
|
464
|
-
.sort((a, b) => a.localeCompare(b))
|
|
200
|
+
const { data: existingFormsData } = useLazyAsyncData(
|
|
201
|
+
`online-form-config-existing-${siteId}`,
|
|
202
|
+
() => _getAll({ site: siteId, org: orgId, status: "active", limit: 100 })
|
|
465
203
|
);
|
|
466
204
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
existingFormTypes.value = (res?.items ?? []).map((c: any) => c.formType);
|
|
471
|
-
} catch (error) {
|
|
472
|
-
console.error("Failed to load existing form types for template filtering", error);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
477
|
-
onMounted(() => {
|
|
478
|
-
if (prop.mode === "edit") {
|
|
479
|
-
if (prop.onlineForm?.fields?.blocks) {
|
|
480
|
-
try {
|
|
481
|
-
const parsed = JSON.parse(prop.onlineForm.fields.blocks);
|
|
482
|
-
blocks.value = (parsed as any[]).map((b) => ({ ...b, _uid: nextUid() }));
|
|
483
|
-
} catch {}
|
|
484
|
-
}
|
|
485
|
-
const pa = Number(prop.onlineForm?.fields?.paymentAmount ?? 0);
|
|
486
|
-
if (pa > 0) { requiresPayment.value = true; paymentAmount.value = pa; }
|
|
487
|
-
} else {
|
|
488
|
-
loadExistingFormTypes();
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
493
|
-
function labelToKey(label: string): string {
|
|
494
|
-
const words = label
|
|
495
|
-
.replace(/[^a-zA-Z0-9 ]/g, "")
|
|
496
|
-
.trim()
|
|
497
|
-
.split(/\s+/)
|
|
498
|
-
.filter(Boolean);
|
|
499
|
-
if (!words.length) return "";
|
|
500
|
-
return (
|
|
501
|
-
words[0].toLowerCase() +
|
|
502
|
-
words
|
|
503
|
-
.slice(1)
|
|
504
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
505
|
-
.join("")
|
|
205
|
+
const availableFormTypes = computed(() => {
|
|
206
|
+
const existing = new Set(
|
|
207
|
+
existingFormsData.value?.items?.map((f: any) => f.formType) ?? []
|
|
506
208
|
);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
{ title: "Textarea (multi-line)", value: "textarea" },
|
|
512
|
-
{ title: "Number", value: "number" },
|
|
513
|
-
{ title: "Date", value: "date" },
|
|
514
|
-
{ title: "Dropdown (Select)", value: "select" },
|
|
515
|
-
];
|
|
516
|
-
|
|
517
|
-
function blockTypeLabel(type: string): string {
|
|
518
|
-
const map: Record<string, string> = {
|
|
519
|
-
"section-header": "Heading",
|
|
520
|
-
paragraph: "Paragraph",
|
|
521
|
-
"ordered-list": "Numbered List",
|
|
522
|
-
"unordered-list": "Bullet List",
|
|
523
|
-
divider: "Divider",
|
|
524
|
-
field: "Field",
|
|
525
|
-
"checkbox-group": "Checkboxes",
|
|
526
|
-
"signature-row": "Signature",
|
|
527
|
-
};
|
|
528
|
-
return map[type] ?? type;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function blockTypeColor(type: string): string {
|
|
532
|
-
const map: Record<string, string> = {
|
|
533
|
-
"section-header": "blue-grey",
|
|
534
|
-
paragraph: "grey",
|
|
535
|
-
"ordered-list": "indigo",
|
|
536
|
-
"unordered-list": "indigo",
|
|
537
|
-
divider: "grey",
|
|
538
|
-
field: "teal",
|
|
539
|
-
"checkbox-group": "purple",
|
|
540
|
-
"signature-row": "orange",
|
|
541
|
-
};
|
|
542
|
-
return map[type] ?? "grey";
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function blockSummary(block: AnyBlock): string {
|
|
546
|
-
switch (block.type) {
|
|
547
|
-
case "section-header":
|
|
548
|
-
case "paragraph":
|
|
549
|
-
return (block.text ?? "").slice(0, 70) || "(empty)";
|
|
550
|
-
case "ordered-list":
|
|
551
|
-
case "unordered-list":
|
|
552
|
-
return block.title || `${block.items?.length ?? 0} item${block.items?.length === 1 ? "" : "s"}`;
|
|
553
|
-
case "divider":
|
|
554
|
-
return "──────────────";
|
|
555
|
-
case "field":
|
|
556
|
-
return block.field?.label
|
|
557
|
-
? `${block.field.label} · ${block.field.fieldType}`
|
|
558
|
-
: "(no label)";
|
|
559
|
-
case "checkbox-group":
|
|
560
|
-
return block.title || `${block.items?.length ?? 0} checkbox${block.items?.length === 1 ? "" : "es"}`;
|
|
561
|
-
case "signature-row":
|
|
562
|
-
return block.label || "(no label)";
|
|
563
|
-
default:
|
|
564
|
-
return "";
|
|
565
|
-
}
|
|
566
|
-
}
|
|
209
|
+
return Object.keys(formPages)
|
|
210
|
+
.filter((name) => !existing.has(name))
|
|
211
|
+
.sort((a, b) => a.localeCompare(b));
|
|
212
|
+
});
|
|
567
213
|
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
editingIdx.value = editingIdx.value === idx ? null : idx;
|
|
571
|
-
}
|
|
214
|
+
// --- status (edit mode) ---
|
|
215
|
+
const formStatus = ref<string>(prop.onlineForm?.status || "active");
|
|
572
216
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
217
|
+
// --- attachment ---
|
|
218
|
+
const attachmentId = ref<string>(prop.onlineForm?.attachment || prop.onlineForm?.fields?.attachment || "");
|
|
219
|
+
const attachmentFileName = ref("");
|
|
220
|
+
const attachmentError = ref("");
|
|
221
|
+
const isDragOver = ref(false);
|
|
222
|
+
const isUploading = ref(false);
|
|
223
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
224
|
+
|
|
225
|
+
function handleDrop(e: DragEvent) {
|
|
226
|
+
isDragOver.value = false;
|
|
227
|
+
const file = e.dataTransfer?.files[0];
|
|
228
|
+
if (file) uploadAttachment(file);
|
|
580
229
|
}
|
|
581
230
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
else if (editingIdx.value !== null && editingIdx.value > idx) editingIdx.value--;
|
|
231
|
+
function handleFileInputChange(e: Event) {
|
|
232
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
233
|
+
if (file) uploadAttachment(file);
|
|
586
234
|
}
|
|
587
235
|
|
|
588
|
-
function
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
break;
|
|
600
|
-
case "unordered-list":
|
|
601
|
-
b = { _uid: nextUid(), type: "unordered-list", title: "", items: [""] };
|
|
602
|
-
break;
|
|
603
|
-
case "divider":
|
|
604
|
-
b = { _uid: nextUid(), type: "divider" };
|
|
605
|
-
break;
|
|
606
|
-
case "field":
|
|
607
|
-
b = { _uid: nextUid(), type: "field", field: { key: "", label: "", fieldType: "text", required: false } };
|
|
608
|
-
break;
|
|
609
|
-
case "field-select":
|
|
610
|
-
b = { _uid: nextUid(), type: "field", field: { key: "", label: "", fieldType: "select", required: false, options: [] } };
|
|
611
|
-
break;
|
|
612
|
-
case "checkbox-group":
|
|
613
|
-
b = { _uid: nextUid(), type: "checkbox-group", title: "", single: false, items: [{ key: "", label: "" }] };
|
|
614
|
-
break;
|
|
615
|
-
case "signature-row":
|
|
616
|
-
b = { _uid: nextUid(), type: "signature-row", label: "", key: "" };
|
|
617
|
-
break;
|
|
618
|
-
default:
|
|
619
|
-
return;
|
|
236
|
+
async function uploadAttachment(file: File) {
|
|
237
|
+
isUploading.value = true;
|
|
238
|
+
attachmentError.value = "";
|
|
239
|
+
try {
|
|
240
|
+
const res = await addFile(file);
|
|
241
|
+
attachmentId.value = res.id;
|
|
242
|
+
attachmentFileName.value = file.name;
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
attachmentError.value = e?.message || "Upload failed. Please try again.";
|
|
245
|
+
} finally {
|
|
246
|
+
isUploading.value = false;
|
|
620
247
|
}
|
|
621
|
-
blocks.value.push(b);
|
|
622
|
-
editingIdx.value = blocks.value.length - 1;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Auto-generate key from label (field blocks)
|
|
626
|
-
function onLabelChange(block: AnyBlock, newLabel: string) {
|
|
627
|
-
if (!block._keyEdited) block.field.key = labelToKey(newLabel);
|
|
628
248
|
}
|
|
629
249
|
|
|
630
|
-
function
|
|
631
|
-
|
|
250
|
+
function clearAttachment() {
|
|
251
|
+
attachmentId.value = "";
|
|
252
|
+
attachmentFileName.value = "";
|
|
253
|
+
attachmentError.value = "";
|
|
254
|
+
if (fileInputRef.value) fileInputRef.value.value = "";
|
|
632
255
|
}
|
|
633
256
|
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// ── Template loading ──────────────────────────────────────────────────────────
|
|
640
|
-
function loadTemplate(name: string) {
|
|
641
|
-
const def = formPages[name];
|
|
642
|
-
if (!def) return;
|
|
643
|
-
formName.value = name;
|
|
644
|
-
blocks.value = def.blocks.map((b) => ({
|
|
645
|
-
...JSON.parse(JSON.stringify(b)),
|
|
646
|
-
_uid: nextUid(),
|
|
647
|
-
}));
|
|
648
|
-
showTemplatePicker.value = false;
|
|
649
|
-
editingIdx.value = null;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// ── Computed ──────────────────────────────────────────────────────────────────
|
|
653
|
-
const canSubmit = computed(() => {
|
|
654
|
-
const nameOk = prop.mode === "edit" || !!formName.value.trim();
|
|
655
|
-
const payOk = !requiresPayment.value || paymentAmount.value > 0;
|
|
656
|
-
return nameOk && payOk && !loading.value;
|
|
657
|
-
});
|
|
257
|
+
// --- submit ---
|
|
258
|
+
const validForm = ref(false);
|
|
259
|
+
const disable = ref(false);
|
|
260
|
+
const message = ref("");
|
|
658
261
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
663
|
-
const { _uid, _keyEdited, ...rest } = b;
|
|
664
|
-
if (rest.field) {
|
|
665
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
666
|
-
const { _keyEdited: _fke, ...fieldRest } = rest.field;
|
|
667
|
-
rest.field = fieldRest;
|
|
668
|
-
}
|
|
669
|
-
if (Array.isArray(rest.items)) {
|
|
670
|
-
rest.items = rest.items.map((item: any) => {
|
|
671
|
-
if (item && typeof item === "object") {
|
|
672
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
673
|
-
const { _keyEdited: _ike, ...itemRest } = item;
|
|
674
|
-
return itemRest;
|
|
675
|
-
}
|
|
676
|
-
return item;
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
return rest;
|
|
680
|
-
});
|
|
681
|
-
}
|
|
262
|
+
const canSubmit = computed(() =>
|
|
263
|
+
prop.mode === "edit" ? true : selectedFormType.value !== ""
|
|
264
|
+
);
|
|
682
265
|
|
|
683
|
-
function buildFields(): Record<string, string> {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
fields.paymentAmount = String(paymentAmount.value);
|
|
689
|
-
}
|
|
266
|
+
function buildFields(formType: string): Record<string, string> {
|
|
267
|
+
const def = formPages[formType];
|
|
268
|
+
const fields: Record<string, string> = {};
|
|
269
|
+
if (def) fields.blocks = JSON.stringify(def.blocks);
|
|
270
|
+
if (attachmentId.value) fields.attachment = attachmentId.value;
|
|
690
271
|
return fields;
|
|
691
272
|
}
|
|
692
273
|
|
|
693
|
-
// ── Submit ────────────────────────────────────────────────────────────────────
|
|
694
274
|
async function submit() {
|
|
275
|
+
disable.value = true;
|
|
695
276
|
message.value = "";
|
|
696
|
-
loading.value = true;
|
|
697
277
|
try {
|
|
698
278
|
if (prop.mode === "add") {
|
|
699
|
-
const name = formName.value.trim();
|
|
700
279
|
await _add({
|
|
701
|
-
formType:
|
|
702
|
-
fields: buildFields(),
|
|
280
|
+
formType: selectedFormType.value,
|
|
281
|
+
fields: buildFields(selectedFormType.value),
|
|
703
282
|
status: "active",
|
|
704
283
|
org: orgId,
|
|
705
284
|
site: siteId,
|
|
706
285
|
});
|
|
707
286
|
} else {
|
|
708
|
-
const formType = prop.onlineForm
|
|
709
|
-
await _update(prop.onlineForm
|
|
287
|
+
const formType = prop.onlineForm.formType as string;
|
|
288
|
+
await _update(prop.onlineForm._id ?? "", {
|
|
710
289
|
formType,
|
|
711
|
-
fields: buildFields(),
|
|
290
|
+
fields: buildFields(formType),
|
|
712
291
|
status: formStatus.value,
|
|
713
292
|
});
|
|
714
293
|
}
|
|
715
294
|
emit("success");
|
|
716
295
|
} catch (error: any) {
|
|
717
|
-
message.value = error?.response?._data?.message || "Failed to save form
|
|
296
|
+
message.value = error?.response?._data?.message || "Failed to save form";
|
|
718
297
|
} finally {
|
|
719
|
-
|
|
298
|
+
disable.value = false;
|
|
720
299
|
}
|
|
721
300
|
}
|
|
722
301
|
</script>
|
|
302
|
+
|
|
303
|
+
<style scoped>
|
|
304
|
+
.upload-drop-zone {
|
|
305
|
+
border: 2px dashed rgba(0, 0, 0, 0.2);
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
padding: 18px 12px;
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
align-items: center;
|
|
311
|
+
justify-content: center;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
min-height: 80px;
|
|
314
|
+
transition: border-color 0.2s, background-color 0.2s;
|
|
315
|
+
user-select: none;
|
|
316
|
+
}
|
|
317
|
+
.upload-drop-zone:hover,
|
|
318
|
+
.upload-drop-zone.drag-over {
|
|
319
|
+
border-color: rgb(var(--v-theme-primary));
|
|
320
|
+
background-color: rgba(var(--v-theme-primary), 0.04);
|
|
321
|
+
}
|
|
322
|
+
.upload-drop-zone.is-uploading {
|
|
323
|
+
pointer-events: none;
|
|
324
|
+
opacity: 0.65;
|
|
325
|
+
}
|
|
326
|
+
</style>
|