@5minds/node-red-dashboard-2-processcube-dynamic-form 2.2.1 → 2.3.0-develop-999cb6-mfglqyd6
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/nodes/ui-dynamic-form.html +331 -63
- package/package.json +1 -1
- package/resources/ui-dynamic-form.umd.js +36 -36
- package/ui/components/FooterActions.vue +13 -47
- package/ui/components/UIDynamicForm.vue +554 -113
- package/ui/stylesheets/ui-dynamic-form.css +119 -15
|
@@ -79,6 +79,53 @@
|
|
|
79
79
|
{{ field.customForm ? field.customForm.hint : undefined }}
|
|
80
80
|
</p>
|
|
81
81
|
</div>
|
|
82
|
+
<div v-else-if="createComponent(field).isFileField" class="ui-dynamic-form-file-wrapper">
|
|
83
|
+
<div v-if="getFilePreviewsForField(field.id).length > 0" class="ui-dynamic-form-image-previews">
|
|
84
|
+
<h4>Current files:</h4>
|
|
85
|
+
<div class="ui-dynamic-form-preview-grid">
|
|
86
|
+
<div
|
|
87
|
+
v-for="preview in getFilePreviewsForField(field.id)"
|
|
88
|
+
:key="preview.name"
|
|
89
|
+
class="ui-dynamic-form-preview-item"
|
|
90
|
+
>
|
|
91
|
+
<img
|
|
92
|
+
:src="preview.url"
|
|
93
|
+
:alt="preview.name"
|
|
94
|
+
class="ui-dynamic-form-preview-image"
|
|
95
|
+
@click="openImageModal(preview)"
|
|
96
|
+
/>
|
|
97
|
+
<div class="ui-dynamic-form-preview-info">
|
|
98
|
+
<span class="ui-dynamic-form-preview-name">{{ preview.name }}</span>
|
|
99
|
+
<span class="ui-dynamic-form-preview-size">{{ formatFileSize(preview.size) }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<FormKit
|
|
106
|
+
type="file"
|
|
107
|
+
:id="field.id"
|
|
108
|
+
:name="createComponent(field).name"
|
|
109
|
+
:label="field.label"
|
|
110
|
+
:required="field.required"
|
|
111
|
+
v-model="formData[field.id]"
|
|
112
|
+
:help="createComponent(field).hint"
|
|
113
|
+
innerClass="reset-background"
|
|
114
|
+
wrapperClass="$remove:formkit-wrapper"
|
|
115
|
+
labelClass="ui-dynamic-form-input-label"
|
|
116
|
+
:inputClass="`input-${theme}`"
|
|
117
|
+
:readonly="createComponent(field).isReadOnly"
|
|
118
|
+
:disabled="createComponent(field).isReadOnly"
|
|
119
|
+
:multiple="createComponent(field).multiple"
|
|
120
|
+
:validation="createComponent(field).validation"
|
|
121
|
+
validationVisibility="live"
|
|
122
|
+
:ref="
|
|
123
|
+
(el) => {
|
|
124
|
+
if (index === 0) firstFormFieldRef = el;
|
|
125
|
+
}
|
|
126
|
+
"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
82
129
|
<component
|
|
83
130
|
:is="createComponent(field).type"
|
|
84
131
|
v-else
|
|
@@ -121,6 +168,27 @@
|
|
|
121
168
|
<div v-if="!props.actions_inside_card && hasUserTask && actions.length > 0" style="padding-top: 32px">
|
|
122
169
|
<UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
|
|
123
170
|
</div>
|
|
171
|
+
|
|
172
|
+
<v-dialog v-model="imageModalOpen" max-width="800px">
|
|
173
|
+
<v-card v-if="modalImage">
|
|
174
|
+
<v-card-title class="headline">{{ modalImage.name }}</v-card-title>
|
|
175
|
+
<v-card-text>
|
|
176
|
+
<img
|
|
177
|
+
:src="modalImage.url"
|
|
178
|
+
:alt="modalImage.name"
|
|
179
|
+
style="width: 100%; max-height: 70vh; object-fit: contain"
|
|
180
|
+
/>
|
|
181
|
+
<div style="margin-top: 16px">
|
|
182
|
+
<strong>Size:</strong> {{ formatFileSize(modalImage.size) }}<br />
|
|
183
|
+
<strong>Type:</strong> {{ modalImage.type }}
|
|
184
|
+
</div>
|
|
185
|
+
</v-card-text>
|
|
186
|
+
<v-card-actions>
|
|
187
|
+
<v-spacer></v-spacer>
|
|
188
|
+
<v-btn color="primary" text @click="closeImageModal">Close</v-btn>
|
|
189
|
+
</v-card-actions>
|
|
190
|
+
</v-card>
|
|
191
|
+
</v-dialog>
|
|
124
192
|
</div>
|
|
125
193
|
</template>
|
|
126
194
|
|
|
@@ -215,6 +283,7 @@ export default {
|
|
|
215
283
|
return {
|
|
216
284
|
actions: [],
|
|
217
285
|
formData: {},
|
|
286
|
+
originalFileData: {},
|
|
218
287
|
userTask: null,
|
|
219
288
|
theme: '',
|
|
220
289
|
errorMsg: '',
|
|
@@ -222,6 +291,10 @@ export default {
|
|
|
222
291
|
msg: null,
|
|
223
292
|
collapsed: false,
|
|
224
293
|
firstFormFieldRef: null,
|
|
294
|
+
imageModalOpen: false,
|
|
295
|
+
modalImage: null,
|
|
296
|
+
objectUrlsByField: {},
|
|
297
|
+
previewCache: {},
|
|
225
298
|
};
|
|
226
299
|
},
|
|
227
300
|
computed: {
|
|
@@ -235,13 +308,12 @@ export default {
|
|
|
235
308
|
return !!this.userTask;
|
|
236
309
|
},
|
|
237
310
|
totalOutputs() {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
311
|
+
const confirmOutputs = this.props.handle_confirmation_dialogs
|
|
312
|
+
? this.props.confirm_actions
|
|
313
|
+
? this.props.confirm_actions.length
|
|
314
|
+
: 2
|
|
315
|
+
: 0;
|
|
316
|
+
return this.props.options.length + confirmOutputs + (this.props.trigger_on_change ? 1 : 0);
|
|
245
317
|
},
|
|
246
318
|
isConfirmDialog() {
|
|
247
319
|
return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
|
|
@@ -259,27 +331,55 @@ export default {
|
|
|
259
331
|
watch: {
|
|
260
332
|
formData: {
|
|
261
333
|
handler(newData, oldData) {
|
|
334
|
+
if (!oldData) return;
|
|
335
|
+
|
|
336
|
+
if (this.userTask && this.userTask.userTaskConfig && this.userTask.userTaskConfig.formFields) {
|
|
337
|
+
const fileFields = this.userTask.userTaskConfig.formFields.filter((field) => field.type === 'file');
|
|
338
|
+
|
|
339
|
+
fileFields.forEach((field) => {
|
|
340
|
+
const fieldId = field.id;
|
|
341
|
+
const newValue = newData[fieldId];
|
|
342
|
+
const oldValue = oldData[fieldId];
|
|
343
|
+
|
|
344
|
+
if (newValue !== oldValue && this.originalFileData[fieldId]) {
|
|
345
|
+
const hasNewFiles = this.hasActualFileObjects(newValue);
|
|
346
|
+
|
|
347
|
+
if (hasNewFiles) {
|
|
348
|
+
delete this.originalFileData[fieldId];
|
|
349
|
+
|
|
350
|
+
Object.keys(this.previewCache).forEach((key) => {
|
|
351
|
+
if (key.startsWith(fieldId + '_')) {
|
|
352
|
+
delete this.previewCache[key];
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
this.cleanupObjectUrls(fieldId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
262
362
|
if (this.props.trigger_on_change) {
|
|
263
363
|
const res = { payload: { formData: newData, userTask: this.userTask } };
|
|
264
364
|
this.send(res, this.totalOutputs - 1);
|
|
265
365
|
}
|
|
266
366
|
},
|
|
267
|
-
collapsed(newVal) {
|
|
268
|
-
if (!newVal && this.hasUserTask) {
|
|
269
|
-
nextTick(() => {
|
|
270
|
-
this.focusFirstFormField();
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
userTask(newVal) {
|
|
275
|
-
if (newVal && !this.collapsed) {
|
|
276
|
-
nextTick(() => {
|
|
277
|
-
this.focusFirstFormField();
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
367
|
deep: true,
|
|
282
368
|
},
|
|
369
|
+
collapsed(newVal) {
|
|
370
|
+
if (!newVal && this.hasUserTask) {
|
|
371
|
+
nextTick(() => {
|
|
372
|
+
this.focusFirstFormField();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
userTask(newVal) {
|
|
377
|
+
if (newVal && !this.collapsed) {
|
|
378
|
+
nextTick(() => {
|
|
379
|
+
this.focusFirstFormField();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
},
|
|
283
383
|
},
|
|
284
384
|
created() {
|
|
285
385
|
const currentPath = window.location.pathname;
|
|
@@ -315,6 +415,7 @@ export default {
|
|
|
315
415
|
},
|
|
316
416
|
unmounted() {
|
|
317
417
|
this.$socket?.off('msg-input:' + this.id);
|
|
418
|
+
this.cleanupObjectUrls();
|
|
318
419
|
},
|
|
319
420
|
methods: {
|
|
320
421
|
createComponent(field) {
|
|
@@ -496,25 +597,17 @@ export default {
|
|
|
496
597
|
case 'file':
|
|
497
598
|
const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
|
|
498
599
|
return {
|
|
499
|
-
type: '
|
|
600
|
+
type: 'div',
|
|
500
601
|
props: {
|
|
501
|
-
|
|
502
|
-
id: field.id,
|
|
503
|
-
name,
|
|
504
|
-
label: field.label,
|
|
505
|
-
required: field.required,
|
|
506
|
-
value: this.formData[field.id],
|
|
507
|
-
help: hint,
|
|
508
|
-
innerClass: 'reset-background',
|
|
509
|
-
wrapperClass: '$remove:formkit-wrapper',
|
|
510
|
-
labelClass: 'ui-dynamic-form-input-label',
|
|
511
|
-
inputClass: `input-${this.theme}`,
|
|
512
|
-
readonly: isReadOnly,
|
|
513
|
-
disabled: isReadOnly,
|
|
514
|
-
multiple,
|
|
515
|
-
validation,
|
|
516
|
-
validationVisibility: 'live',
|
|
602
|
+
class: 'ui-dynamic-form-file-wrapper',
|
|
517
603
|
},
|
|
604
|
+
isFileField: true,
|
|
605
|
+
field: field,
|
|
606
|
+
multiple: multiple,
|
|
607
|
+
isReadOnly: isReadOnly,
|
|
608
|
+
hint: hint,
|
|
609
|
+
validation: validation,
|
|
610
|
+
name: name,
|
|
518
611
|
};
|
|
519
612
|
case 'checkbox':
|
|
520
613
|
const options = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
|
|
@@ -875,6 +968,9 @@ export default {
|
|
|
875
968
|
return;
|
|
876
969
|
}
|
|
877
970
|
|
|
971
|
+
this.originalFileData = {};
|
|
972
|
+
this.previewCache = {};
|
|
973
|
+
this.cleanupObjectUrls();
|
|
878
974
|
this.actions = this.props.options;
|
|
879
975
|
|
|
880
976
|
const hasTask = msg.payload && msg.payload.userTask;
|
|
@@ -884,6 +980,8 @@ export default {
|
|
|
884
980
|
} else {
|
|
885
981
|
this.userTask = null;
|
|
886
982
|
this.formData = {};
|
|
983
|
+
this.originalFileData = {};
|
|
984
|
+
this.cleanupObjectUrls();
|
|
887
985
|
return;
|
|
888
986
|
}
|
|
889
987
|
|
|
@@ -896,6 +994,8 @@ export default {
|
|
|
896
994
|
this.collapsed = this.props.collapse_when_finished;
|
|
897
995
|
}
|
|
898
996
|
|
|
997
|
+
const hasConfirmField = formFields.some((field) => field.type === 'confirm');
|
|
998
|
+
|
|
899
999
|
if (formFields) {
|
|
900
1000
|
formFields.forEach((field) => {
|
|
901
1001
|
this.formData[field.id] = field.defaultValue;
|
|
@@ -904,31 +1004,63 @@ export default {
|
|
|
904
1004
|
const customForm = field.customForm ? JSON.parse(JSON.stringify(field.customForm)) : {};
|
|
905
1005
|
const confirmText = customForm.confirmButtonText ?? 'Confirm';
|
|
906
1006
|
const declineText = customForm.declineButtonText ?? 'Decline';
|
|
1007
|
+
|
|
907
1008
|
const confirmActions = [
|
|
908
1009
|
{
|
|
909
|
-
alignment: 'left',
|
|
910
|
-
primary: '
|
|
911
|
-
label:
|
|
1010
|
+
alignment: this.props.confirm_actions[1]?.alignment || 'left',
|
|
1011
|
+
primary: this.props.confirm_actions[1]?.primary || 'destructive',
|
|
1012
|
+
label: declineText,
|
|
912
1013
|
condition: '',
|
|
913
1014
|
isConfirmAction: true,
|
|
914
1015
|
confirmFieldId: field.id,
|
|
915
|
-
confirmValue:
|
|
1016
|
+
confirmValue: false,
|
|
916
1017
|
},
|
|
917
1018
|
{
|
|
918
|
-
alignment: '
|
|
919
|
-
primary: '
|
|
920
|
-
label:
|
|
1019
|
+
alignment: this.props.confirm_actions[0]?.alignment || 'right',
|
|
1020
|
+
primary: this.props.confirm_actions[0]?.primary || 'primary',
|
|
1021
|
+
label: confirmText,
|
|
921
1022
|
condition: '',
|
|
922
1023
|
isConfirmAction: true,
|
|
923
1024
|
confirmFieldId: field.id,
|
|
924
|
-
confirmValue:
|
|
1025
|
+
confirmValue: true,
|
|
925
1026
|
},
|
|
926
1027
|
];
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1028
|
+
|
|
1029
|
+
const filteredActions = this.props.options.filter((action) => {
|
|
1030
|
+
if (!action.condition) return true;
|
|
1031
|
+
try {
|
|
1032
|
+
const usertaskWithContext = {
|
|
1033
|
+
...this.userTask,
|
|
1034
|
+
isConfirmDialog: true,
|
|
1035
|
+
};
|
|
1036
|
+
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
1037
|
+
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
1038
|
+
return Boolean(result);
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
console.error('Error while evaluating condition: ' + err);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
this.actions = [...filteredActions, ...confirmActions];
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (!hasConfirmField && this.props.handle_confirmation_dialogs) {
|
|
1051
|
+
this.actions = this.props.options.filter((action) => {
|
|
1052
|
+
if (!action.condition) return true;
|
|
1053
|
+
try {
|
|
1054
|
+
const usertaskWithContext = {
|
|
1055
|
+
...this.userTask,
|
|
1056
|
+
isConfirmDialog: false,
|
|
1057
|
+
};
|
|
1058
|
+
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
1059
|
+
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
1060
|
+
return Boolean(result);
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
console.error('Error while evaluating condition: ' + err);
|
|
1063
|
+
return false;
|
|
932
1064
|
}
|
|
933
1065
|
});
|
|
934
1066
|
}
|
|
@@ -937,7 +1069,12 @@ export default {
|
|
|
937
1069
|
Object.keys(initialValues)
|
|
938
1070
|
.filter((key) => formFieldIds.includes(key))
|
|
939
1071
|
.forEach((key) => {
|
|
940
|
-
|
|
1072
|
+
const field = formFields.find((f) => f.id === key);
|
|
1073
|
+
if (field && field.type === 'file') {
|
|
1074
|
+
this.formData[key] = this.transformBase64ToFormKitFormat(initialValues[key], field);
|
|
1075
|
+
} else {
|
|
1076
|
+
this.formData[key] = initialValues[key];
|
|
1077
|
+
}
|
|
941
1078
|
});
|
|
942
1079
|
}
|
|
943
1080
|
|
|
@@ -945,7 +1082,12 @@ export default {
|
|
|
945
1082
|
Object.keys(finishedFormData)
|
|
946
1083
|
.filter((key) => formFieldIds.includes(key))
|
|
947
1084
|
.forEach((key) => {
|
|
948
|
-
|
|
1085
|
+
const field = formFields.find((f) => f.id === key);
|
|
1086
|
+
if (field && field.type === 'file') {
|
|
1087
|
+
this.formData[key] = this.transformBase64ToFormKitFormat(finishedFormData[key], field);
|
|
1088
|
+
} else {
|
|
1089
|
+
this.formData[key] = finishedFormData[key];
|
|
1090
|
+
}
|
|
949
1091
|
});
|
|
950
1092
|
}
|
|
951
1093
|
|
|
@@ -953,65 +1095,24 @@ export default {
|
|
|
953
1095
|
this.focusFirstFormField();
|
|
954
1096
|
});
|
|
955
1097
|
},
|
|
956
|
-
actionFn(action) {
|
|
957
|
-
if (action.isTerminate) {
|
|
958
|
-
this.showError('');
|
|
959
|
-
|
|
960
|
-
const msg = this.msg ?? {};
|
|
961
|
-
msg.payload = {
|
|
962
|
-
formData: this.formData,
|
|
963
|
-
userTask: this.userTask,
|
|
964
|
-
isTerminate: true,
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
const terminateOutputIndex = this.totalOutputs - 1;
|
|
968
|
-
|
|
969
|
-
this.send(msg, terminateOutputIndex);
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (action.isSuspend) {
|
|
974
|
-
this.showError('');
|
|
975
|
-
|
|
976
|
-
const msg = this.msg ?? {};
|
|
977
|
-
msg.payload = {
|
|
978
|
-
formData: this.formData,
|
|
979
|
-
userTask: this.userTask,
|
|
980
|
-
isSuspend: true,
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
const suspendOutputIndex = this.totalOutputs - 2;
|
|
984
|
-
|
|
985
|
-
this.send(msg, suspendOutputIndex);
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
1098
|
+
async actionFn(action) {
|
|
989
1099
|
if (action.isConfirmAction && action.confirmFieldId) {
|
|
990
1100
|
this.formData[action.confirmFieldId] = action.confirmValue;
|
|
991
1101
|
}
|
|
992
1102
|
|
|
993
|
-
if (action.
|
|
994
|
-
const formkitInputs = this.$refs.form.$el.querySelectorAll('.formkit-outer');
|
|
995
|
-
let allComplete = true;
|
|
996
|
-
|
|
997
|
-
formkitInputs.forEach((input) => {
|
|
998
|
-
const dataComplete = input.getAttribute('data-complete');
|
|
999
|
-
const dataInvalid = input.getAttribute('data-invalid');
|
|
1000
|
-
|
|
1001
|
-
if (dataComplete == null && dataInvalid === 'true') {
|
|
1002
|
-
allComplete = false;
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
if (!allComplete) return;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (this.checkCondition(action.condition)) {
|
|
1103
|
+
if (this.checkCondition(action.condition, { isConfirmDialog: this.isConfirmDialog })) {
|
|
1010
1104
|
this.showError('');
|
|
1011
1105
|
|
|
1012
|
-
|
|
1106
|
+
let processedFormData = { ...this.formData };
|
|
1013
1107
|
const formFields = this.userTask.userTaskConfig.formFields;
|
|
1014
1108
|
|
|
1109
|
+
try {
|
|
1110
|
+
processedFormData = await this.processFileFields(processedFormData, formFields);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
this.showError('Fehler beim Verarbeiten der Dateien');
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1015
1116
|
formFields.forEach((field) => {
|
|
1016
1117
|
const fieldValue = processedFormData[field.id];
|
|
1017
1118
|
|
|
@@ -1034,20 +1135,29 @@ export default {
|
|
|
1034
1135
|
|
|
1035
1136
|
const msg = this.msg ?? {};
|
|
1036
1137
|
msg.payload = { formData: processedFormData, userTask: this.userTask };
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1138
|
+
|
|
1139
|
+
let outputIndex;
|
|
1140
|
+
if (action.isConfirmAction) {
|
|
1141
|
+
const confirmActionIndex = action.confirmValue ? 1 : 0;
|
|
1142
|
+
outputIndex = this.props.options.length + confirmActionIndex;
|
|
1143
|
+
} else {
|
|
1144
|
+
outputIndex = this.props.options.findIndex((element) => element.label === action.label);
|
|
1145
|
+
}
|
|
1146
|
+
this.send(msg, outputIndex);
|
|
1042
1147
|
} else {
|
|
1043
1148
|
this.showError(action.errorMessage);
|
|
1044
1149
|
}
|
|
1045
1150
|
},
|
|
1046
|
-
checkCondition(condition) {
|
|
1151
|
+
checkCondition(condition, context = {}) {
|
|
1047
1152
|
if (condition === '') return true;
|
|
1048
1153
|
try {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1154
|
+
const usertaskWithContext = {
|
|
1155
|
+
...this.userTask,
|
|
1156
|
+
isConfirmDialog: context.isConfirmDialog || false,
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + condition + ')');
|
|
1160
|
+
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
1051
1161
|
return Boolean(result);
|
|
1052
1162
|
} catch (err) {
|
|
1053
1163
|
console.error('Error while evaluating condition: ' + err);
|
|
@@ -1082,6 +1192,338 @@ export default {
|
|
|
1082
1192
|
}
|
|
1083
1193
|
}
|
|
1084
1194
|
},
|
|
1195
|
+
/**
|
|
1196
|
+
* Transforms base64 file data to FormKit-compatible format for display.
|
|
1197
|
+
*
|
|
1198
|
+
* FormKit file inputs expect an array of objects with 'name' property for display.
|
|
1199
|
+
* This function converts the base64 format (with 'data' property) to the display format,
|
|
1200
|
+
* while preserving the original data in originalFileData for form submission.
|
|
1201
|
+
*
|
|
1202
|
+
* Expected input format from initialValues:
|
|
1203
|
+
* {
|
|
1204
|
+
* name: "filename.ext",
|
|
1205
|
+
* size: 12345,
|
|
1206
|
+
* type: "image/png",
|
|
1207
|
+
* data: "base64string..."
|
|
1208
|
+
* }
|
|
1209
|
+
*
|
|
1210
|
+
* Output format for FormKit:
|
|
1211
|
+
* [
|
|
1212
|
+
* { name: "filename.ext" }
|
|
1213
|
+
* ]
|
|
1214
|
+
*
|
|
1215
|
+
* @param {Object|Array} fileData - The file data from initial values
|
|
1216
|
+
* @param {Object} field - The form field configuration
|
|
1217
|
+
* @returns {Array} FormKit-compatible file array
|
|
1218
|
+
*/
|
|
1219
|
+
transformBase64ToFormKitFormat(fileData, field) {
|
|
1220
|
+
if (
|
|
1221
|
+
Array.isArray(fileData) &&
|
|
1222
|
+
fileData.length > 0 &&
|
|
1223
|
+
typeof fileData[0] === 'object' &&
|
|
1224
|
+
fileData[0].name &&
|
|
1225
|
+
!fileData[0].data
|
|
1226
|
+
) {
|
|
1227
|
+
return fileData;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const fieldId = field.id;
|
|
1231
|
+
const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
|
|
1232
|
+
|
|
1233
|
+
if (fileData && typeof fileData === 'object' && fileData.name && fileData.data) {
|
|
1234
|
+
this.originalFileData[fieldId] = fileData;
|
|
1235
|
+
|
|
1236
|
+
const formKitFile = {
|
|
1237
|
+
name: fileData.name,
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
return multiple ? [formKitFile] : [formKitFile];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (Array.isArray(fileData)) {
|
|
1244
|
+
this.originalFileData[fieldId] = fileData;
|
|
1245
|
+
|
|
1246
|
+
const formKitFiles = fileData
|
|
1247
|
+
.filter((file) => file && typeof file === 'object' && file.name)
|
|
1248
|
+
.map((file) => ({
|
|
1249
|
+
name: file.name,
|
|
1250
|
+
}));
|
|
1251
|
+
|
|
1252
|
+
return formKitFiles;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return [];
|
|
1256
|
+
},
|
|
1257
|
+
hasActualFileObjects(value) {
|
|
1258
|
+
if (!value) return false;
|
|
1259
|
+
|
|
1260
|
+
if (Array.isArray(value)) {
|
|
1261
|
+
return value.some((item) => {
|
|
1262
|
+
return (
|
|
1263
|
+
item instanceof File ||
|
|
1264
|
+
(item && item.file instanceof File) ||
|
|
1265
|
+
(item && item.file instanceof FileList && item.file.length > 0)
|
|
1266
|
+
);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return (
|
|
1271
|
+
value instanceof File ||
|
|
1272
|
+
(value instanceof FileList && value.length > 0) ||
|
|
1273
|
+
(value && value.file instanceof File) ||
|
|
1274
|
+
(value && value.file instanceof FileList && value.file.length > 0)
|
|
1275
|
+
);
|
|
1276
|
+
},
|
|
1277
|
+
async convertFileToBase64(file) {
|
|
1278
|
+
return new Promise((resolve, reject) => {
|
|
1279
|
+
const reader = new FileReader();
|
|
1280
|
+
reader.onload = () => {
|
|
1281
|
+
const base64 = reader.result.split(',')[1];
|
|
1282
|
+
resolve({
|
|
1283
|
+
name: file.name,
|
|
1284
|
+
size: file.size,
|
|
1285
|
+
type: file.type,
|
|
1286
|
+
lastModified: file.lastModified,
|
|
1287
|
+
data: base64,
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
reader.onerror = reject;
|
|
1291
|
+
reader.readAsDataURL(file);
|
|
1292
|
+
});
|
|
1293
|
+
},
|
|
1294
|
+
async processFileFields(formData, formFields) {
|
|
1295
|
+
const processedData = { ...formData };
|
|
1296
|
+
|
|
1297
|
+
for (const field of formFields) {
|
|
1298
|
+
if (field.type === 'file') {
|
|
1299
|
+
const fieldValue = processedData[field.id];
|
|
1300
|
+
const fieldId = field.id;
|
|
1301
|
+
|
|
1302
|
+
if (this.originalFileData[fieldId]) {
|
|
1303
|
+
processedData[fieldId] = this.originalFileData[fieldId];
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (fieldValue) {
|
|
1308
|
+
const multiple = field.customForm
|
|
1309
|
+
? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true'
|
|
1310
|
+
: false;
|
|
1311
|
+
|
|
1312
|
+
if (multiple && Array.isArray(fieldValue)) {
|
|
1313
|
+
const base64Files = [];
|
|
1314
|
+
for (const file of fieldValue) {
|
|
1315
|
+
const processedFile = await this.processIndividualFile(file);
|
|
1316
|
+
if (processedFile) {
|
|
1317
|
+
base64Files.push(processedFile);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
processedData[field.id] = base64Files;
|
|
1321
|
+
} else if (Array.isArray(fieldValue)) {
|
|
1322
|
+
const base64Files = [];
|
|
1323
|
+
for (const file of fieldValue) {
|
|
1324
|
+
const processedFile = await this.processIndividualFile(file);
|
|
1325
|
+
if (processedFile) {
|
|
1326
|
+
base64Files.push(processedFile);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
processedData[field.id] = multiple ? base64Files : base64Files[0] || null;
|
|
1330
|
+
} else if (fieldValue) {
|
|
1331
|
+
const processedFile = await this.processIndividualFile(fieldValue);
|
|
1332
|
+
if (processedFile) {
|
|
1333
|
+
processedData[field.id] = processedFile;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return processedData;
|
|
1341
|
+
},
|
|
1342
|
+
async processIndividualFile(fileData) {
|
|
1343
|
+
let actualFile = null;
|
|
1344
|
+
|
|
1345
|
+
if (fileData instanceof File) {
|
|
1346
|
+
actualFile = fileData;
|
|
1347
|
+
} else if (fileData instanceof FileList && fileData.length > 0) {
|
|
1348
|
+
actualFile = fileData[0];
|
|
1349
|
+
} else if (fileData && fileData.file instanceof File) {
|
|
1350
|
+
actualFile = fileData.file;
|
|
1351
|
+
} else if (fileData && fileData.file instanceof FileList && fileData.file.length > 0) {
|
|
1352
|
+
actualFile = fileData.file[0];
|
|
1353
|
+
} else if (Array.isArray(fileData) && fileData.length > 0) {
|
|
1354
|
+
if (fileData[0] instanceof File) {
|
|
1355
|
+
actualFile = fileData[0];
|
|
1356
|
+
} else if (fileData[0] && fileData[0].file instanceof File) {
|
|
1357
|
+
actualFile = fileData[0].file;
|
|
1358
|
+
} else if (fileData[0] && fileData[0].file instanceof FileList && fileData[0].file.length > 0) {
|
|
1359
|
+
actualFile = fileData[0].file[0];
|
|
1360
|
+
}
|
|
1361
|
+
} else if (typeof fileData === 'object' && fileData.data && fileData.name) {
|
|
1362
|
+
return fileData;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (actualFile instanceof File) {
|
|
1366
|
+
return await this.convertFileToBase64(actualFile);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (fileData && typeof fileData === 'object' && !fileData.data) {
|
|
1370
|
+
console.warn('Could not process file data:', fileData);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return fileData;
|
|
1374
|
+
},
|
|
1375
|
+
isImageFile(fileData) {
|
|
1376
|
+
if (!fileData || typeof fileData !== 'object') return false;
|
|
1377
|
+
|
|
1378
|
+
const mimeType = fileData.type;
|
|
1379
|
+
if (!mimeType) return false;
|
|
1380
|
+
|
|
1381
|
+
return mimeType.startsWith('image/');
|
|
1382
|
+
},
|
|
1383
|
+
generateImagePreviewUrl(fileData) {
|
|
1384
|
+
if (!fileData || !fileData.data || !fileData.type) return null;
|
|
1385
|
+
|
|
1386
|
+
return `data:${fileData.type};base64,${fileData.data}`;
|
|
1387
|
+
},
|
|
1388
|
+
getFilePreviewsForField(fieldId) {
|
|
1389
|
+
const currentData = this.formData[fieldId] || null;
|
|
1390
|
+
const cachedOriginalData = this.originalFileData[fieldId] || null;
|
|
1391
|
+
const cacheKey = `${fieldId}_${JSON.stringify(currentData)}_${JSON.stringify(cachedOriginalData)}`;
|
|
1392
|
+
|
|
1393
|
+
if (this.previewCache[cacheKey]) {
|
|
1394
|
+
return this.previewCache[cacheKey];
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const currentFormData = this.formData[fieldId];
|
|
1398
|
+
const originalFileData = this.originalFileData[fieldId];
|
|
1399
|
+
|
|
1400
|
+
if (!currentFormData && !originalFileData) {
|
|
1401
|
+
this.previewCache[cacheKey] = [];
|
|
1402
|
+
return [];
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
let previews = [];
|
|
1406
|
+
|
|
1407
|
+
const hasNewFileObjects =
|
|
1408
|
+
currentFormData &&
|
|
1409
|
+
Array.isArray(currentFormData) &&
|
|
1410
|
+
currentFormData.length > 0 &&
|
|
1411
|
+
this.hasActualFileObjects(currentFormData);
|
|
1412
|
+
|
|
1413
|
+
if (hasNewFileObjects) {
|
|
1414
|
+
previews = this.generatePreviewsFromCurrentFiles(currentFormData, fieldId);
|
|
1415
|
+
} else if (originalFileData) {
|
|
1416
|
+
const fileArray = Array.isArray(originalFileData) ? originalFileData : [originalFileData];
|
|
1417
|
+
previews = fileArray
|
|
1418
|
+
.filter((file) => this.isImageFile(file))
|
|
1419
|
+
.map((file) => ({
|
|
1420
|
+
name: file.name,
|
|
1421
|
+
url: this.generateImagePreviewUrl(file),
|
|
1422
|
+
size: file.size,
|
|
1423
|
+
type: file.type,
|
|
1424
|
+
}));
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
this.previewCache[cacheKey] = previews;
|
|
1428
|
+
|
|
1429
|
+
return previews;
|
|
1430
|
+
},
|
|
1431
|
+
generatePreviewsFromCurrentFiles(fileArray, fieldId = null) {
|
|
1432
|
+
if (fieldId && this.objectUrlsByField[fieldId]) {
|
|
1433
|
+
this.objectUrlsByField[fieldId].forEach((url) => URL.revokeObjectURL(url));
|
|
1434
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1435
|
+
} else if (fieldId) {
|
|
1436
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const previews = fileArray
|
|
1440
|
+
.filter((fileItem) => {
|
|
1441
|
+
let actualFile = null;
|
|
1442
|
+
|
|
1443
|
+
if (fileItem instanceof File) {
|
|
1444
|
+
actualFile = fileItem;
|
|
1445
|
+
} else if (fileItem && fileItem.file instanceof File) {
|
|
1446
|
+
actualFile = fileItem.file;
|
|
1447
|
+
} else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
|
|
1448
|
+
actualFile = fileItem[0];
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
return actualFile && actualFile.type && actualFile.type.startsWith('image/');
|
|
1452
|
+
})
|
|
1453
|
+
.map((fileItem) => {
|
|
1454
|
+
let actualFile = null;
|
|
1455
|
+
|
|
1456
|
+
if (fileItem instanceof File) {
|
|
1457
|
+
actualFile = fileItem;
|
|
1458
|
+
} else if (fileItem && fileItem.file instanceof File) {
|
|
1459
|
+
actualFile = fileItem.file;
|
|
1460
|
+
} else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
|
|
1461
|
+
actualFile = fileItem[0];
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const url = actualFile ? URL.createObjectURL(actualFile) : null;
|
|
1465
|
+
|
|
1466
|
+
if (url && fieldId) {
|
|
1467
|
+
if (!this.objectUrlsByField[fieldId]) {
|
|
1468
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1469
|
+
}
|
|
1470
|
+
this.objectUrlsByField[fieldId].push(url);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
return {
|
|
1474
|
+
name: actualFile.name,
|
|
1475
|
+
url: url,
|
|
1476
|
+
size: actualFile.size,
|
|
1477
|
+
type: actualFile.type,
|
|
1478
|
+
isObjectUrl: true,
|
|
1479
|
+
};
|
|
1480
|
+
})
|
|
1481
|
+
.filter((preview) => preview.url !== null);
|
|
1482
|
+
|
|
1483
|
+
return previews;
|
|
1484
|
+
},
|
|
1485
|
+
formatFileSize(bytes) {
|
|
1486
|
+
if (!bytes) return '0 B';
|
|
1487
|
+
|
|
1488
|
+
const k = 1024;
|
|
1489
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1490
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1491
|
+
|
|
1492
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1493
|
+
},
|
|
1494
|
+
openImageModal(preview) {
|
|
1495
|
+
this.modalImage = preview;
|
|
1496
|
+
this.imageModalOpen = true;
|
|
1497
|
+
},
|
|
1498
|
+
closeImageModal() {
|
|
1499
|
+
this.imageModalOpen = false;
|
|
1500
|
+
this.modalImage = null;
|
|
1501
|
+
},
|
|
1502
|
+
cleanupObjectUrls(fieldId = null) {
|
|
1503
|
+
if (fieldId) {
|
|
1504
|
+
if (this.objectUrlsByField[fieldId]) {
|
|
1505
|
+
this.objectUrlsByField[fieldId].forEach((url) => {
|
|
1506
|
+
URL.revokeObjectURL(url);
|
|
1507
|
+
});
|
|
1508
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
Object.keys(this.previewCache).forEach((key) => {
|
|
1512
|
+
if (key.startsWith(fieldId + '_')) {
|
|
1513
|
+
delete this.previewCache[key];
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
} else {
|
|
1517
|
+
Object.keys(this.objectUrlsByField).forEach((fieldId) => {
|
|
1518
|
+
this.objectUrlsByField[fieldId].forEach((url) => {
|
|
1519
|
+
URL.revokeObjectURL(url);
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
this.objectUrlsByField = {};
|
|
1523
|
+
|
|
1524
|
+
this.previewCache = {};
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1085
1527
|
},
|
|
1086
1528
|
};
|
|
1087
1529
|
|
|
@@ -1098,6 +1540,5 @@ function mapItems(type, field) {
|
|
|
1098
1540
|
</script>
|
|
1099
1541
|
|
|
1100
1542
|
<style>
|
|
1101
|
-
/* CSS is auto scoped, but using named classes is still recommended */
|
|
1102
1543
|
@import '../stylesheets/ui-dynamic-form.css';
|
|
1103
1544
|
</style>
|