@5minds/node-red-dashboard-2-processcube-dynamic-form 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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,14 +308,12 @@ export default {
|
|
|
235
308
|
return !!this.userTask;
|
|
236
309
|
},
|
|
237
310
|
totalOutputs() {
|
|
238
|
-
const confirmOutputs = this.props.handle_confirmation_dialogs
|
|
239
|
-
?
|
|
311
|
+
const confirmOutputs = this.props.handle_confirmation_dialogs
|
|
312
|
+
? this.props.confirm_actions
|
|
313
|
+
? this.props.confirm_actions.length
|
|
314
|
+
: 2
|
|
240
315
|
: 0;
|
|
241
|
-
return (
|
|
242
|
-
this.props.options.length +
|
|
243
|
-
confirmOutputs +
|
|
244
|
-
(this.props.trigger_on_change ? 1 : 0)
|
|
245
|
-
);
|
|
316
|
+
return this.props.options.length + confirmOutputs + (this.props.trigger_on_change ? 1 : 0);
|
|
246
317
|
},
|
|
247
318
|
isConfirmDialog() {
|
|
248
319
|
return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
|
|
@@ -260,27 +331,55 @@ export default {
|
|
|
260
331
|
watch: {
|
|
261
332
|
formData: {
|
|
262
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
|
+
|
|
263
362
|
if (this.props.trigger_on_change) {
|
|
264
363
|
const res = { payload: { formData: newData, userTask: this.userTask } };
|
|
265
364
|
this.send(res, this.totalOutputs - 1);
|
|
266
365
|
}
|
|
267
366
|
},
|
|
268
|
-
collapsed(newVal) {
|
|
269
|
-
if (!newVal && this.hasUserTask) {
|
|
270
|
-
nextTick(() => {
|
|
271
|
-
this.focusFirstFormField();
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
userTask(newVal) {
|
|
276
|
-
if (newVal && !this.collapsed) {
|
|
277
|
-
nextTick(() => {
|
|
278
|
-
this.focusFirstFormField();
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
367
|
deep: true,
|
|
283
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
|
+
},
|
|
284
383
|
},
|
|
285
384
|
created() {
|
|
286
385
|
const currentPath = window.location.pathname;
|
|
@@ -316,6 +415,7 @@ export default {
|
|
|
316
415
|
},
|
|
317
416
|
unmounted() {
|
|
318
417
|
this.$socket?.off('msg-input:' + this.id);
|
|
418
|
+
this.cleanupObjectUrls();
|
|
319
419
|
},
|
|
320
420
|
methods: {
|
|
321
421
|
createComponent(field) {
|
|
@@ -497,25 +597,17 @@ export default {
|
|
|
497
597
|
case 'file':
|
|
498
598
|
const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
|
|
499
599
|
return {
|
|
500
|
-
type: '
|
|
600
|
+
type: 'div',
|
|
501
601
|
props: {
|
|
502
|
-
|
|
503
|
-
id: field.id,
|
|
504
|
-
name,
|
|
505
|
-
label: field.label,
|
|
506
|
-
required: field.required,
|
|
507
|
-
value: this.formData[field.id],
|
|
508
|
-
help: hint,
|
|
509
|
-
innerClass: 'reset-background',
|
|
510
|
-
wrapperClass: '$remove:formkit-wrapper',
|
|
511
|
-
labelClass: 'ui-dynamic-form-input-label',
|
|
512
|
-
inputClass: `input-${this.theme}`,
|
|
513
|
-
readonly: isReadOnly,
|
|
514
|
-
disabled: isReadOnly,
|
|
515
|
-
multiple,
|
|
516
|
-
validation,
|
|
517
|
-
validationVisibility: 'live',
|
|
602
|
+
class: 'ui-dynamic-form-file-wrapper',
|
|
518
603
|
},
|
|
604
|
+
isFileField: true,
|
|
605
|
+
field: field,
|
|
606
|
+
multiple: multiple,
|
|
607
|
+
isReadOnly: isReadOnly,
|
|
608
|
+
hint: hint,
|
|
609
|
+
validation: validation,
|
|
610
|
+
name: name,
|
|
519
611
|
};
|
|
520
612
|
case 'checkbox':
|
|
521
613
|
const options = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
|
|
@@ -876,6 +968,9 @@ export default {
|
|
|
876
968
|
return;
|
|
877
969
|
}
|
|
878
970
|
|
|
971
|
+
this.originalFileData = {};
|
|
972
|
+
this.previewCache = {};
|
|
973
|
+
this.cleanupObjectUrls();
|
|
879
974
|
this.actions = this.props.options;
|
|
880
975
|
|
|
881
976
|
const hasTask = msg.payload && msg.payload.userTask;
|
|
@@ -885,6 +980,8 @@ export default {
|
|
|
885
980
|
} else {
|
|
886
981
|
this.userTask = null;
|
|
887
982
|
this.formData = {};
|
|
983
|
+
this.originalFileData = {};
|
|
984
|
+
this.cleanupObjectUrls();
|
|
888
985
|
return;
|
|
889
986
|
}
|
|
890
987
|
|
|
@@ -908,55 +1005,55 @@ export default {
|
|
|
908
1005
|
const confirmText = customForm.confirmButtonText ?? 'Confirm';
|
|
909
1006
|
const declineText = customForm.declineButtonText ?? 'Decline';
|
|
910
1007
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const filteredActions = this.props.options.filter(action => {
|
|
933
|
-
if (!action.condition) return true;
|
|
934
|
-
try {
|
|
935
|
-
const usertaskWithContext = {
|
|
936
|
-
...this.userTask,
|
|
937
|
-
isConfirmDialog: true
|
|
938
|
-
};
|
|
939
|
-
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
940
|
-
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
941
|
-
return Boolean(result);
|
|
942
|
-
} catch (err) {
|
|
943
|
-
console.error('Error while evaluating condition: ' + err);
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
});
|
|
1008
|
+
const confirmActions = [
|
|
1009
|
+
{
|
|
1010
|
+
alignment: this.props.confirm_actions[1]?.alignment || 'left',
|
|
1011
|
+
primary: this.props.confirm_actions[1]?.primary || 'destructive',
|
|
1012
|
+
label: declineText,
|
|
1013
|
+
condition: '',
|
|
1014
|
+
isConfirmAction: true,
|
|
1015
|
+
confirmFieldId: field.id,
|
|
1016
|
+
confirmValue: false,
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
alignment: this.props.confirm_actions[0]?.alignment || 'right',
|
|
1020
|
+
primary: this.props.confirm_actions[0]?.primary || 'primary',
|
|
1021
|
+
label: confirmText,
|
|
1022
|
+
condition: '',
|
|
1023
|
+
isConfirmAction: true,
|
|
1024
|
+
confirmFieldId: field.id,
|
|
1025
|
+
confirmValue: true,
|
|
1026
|
+
},
|
|
1027
|
+
];
|
|
947
1028
|
|
|
948
|
-
|
|
949
|
-
|
|
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
|
+
}
|
|
950
1047
|
});
|
|
951
1048
|
}
|
|
952
1049
|
|
|
953
1050
|
if (!hasConfirmField && this.props.handle_confirmation_dialogs) {
|
|
954
|
-
this.actions = this.props.options.filter(action => {
|
|
1051
|
+
this.actions = this.props.options.filter((action) => {
|
|
955
1052
|
if (!action.condition) return true;
|
|
956
1053
|
try {
|
|
957
1054
|
const usertaskWithContext = {
|
|
958
1055
|
...this.userTask,
|
|
959
|
-
isConfirmDialog: false
|
|
1056
|
+
isConfirmDialog: false,
|
|
960
1057
|
};
|
|
961
1058
|
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
962
1059
|
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
@@ -972,7 +1069,12 @@ export default {
|
|
|
972
1069
|
Object.keys(initialValues)
|
|
973
1070
|
.filter((key) => formFieldIds.includes(key))
|
|
974
1071
|
.forEach((key) => {
|
|
975
|
-
|
|
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
|
+
}
|
|
976
1078
|
});
|
|
977
1079
|
}
|
|
978
1080
|
|
|
@@ -980,7 +1082,12 @@ export default {
|
|
|
980
1082
|
Object.keys(finishedFormData)
|
|
981
1083
|
.filter((key) => formFieldIds.includes(key))
|
|
982
1084
|
.forEach((key) => {
|
|
983
|
-
|
|
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
|
+
}
|
|
984
1091
|
});
|
|
985
1092
|
}
|
|
986
1093
|
|
|
@@ -988,33 +1095,24 @@ export default {
|
|
|
988
1095
|
this.focusFirstFormField();
|
|
989
1096
|
});
|
|
990
1097
|
},
|
|
991
|
-
actionFn(action) {
|
|
1098
|
+
async actionFn(action) {
|
|
992
1099
|
if (action.isConfirmAction && action.confirmFieldId) {
|
|
993
1100
|
this.formData[action.confirmFieldId] = action.confirmValue;
|
|
994
1101
|
}
|
|
995
1102
|
|
|
996
|
-
if (action.label === 'Speichern' || action.label === 'Speichern und nächster') {
|
|
997
|
-
const formkitInputs = this.$refs.form.$el.querySelectorAll('.formkit-outer');
|
|
998
|
-
let allComplete = true;
|
|
999
|
-
|
|
1000
|
-
formkitInputs.forEach((input) => {
|
|
1001
|
-
const dataComplete = input.getAttribute('data-complete');
|
|
1002
|
-
const dataInvalid = input.getAttribute('data-invalid');
|
|
1003
|
-
|
|
1004
|
-
if (dataComplete == null && dataInvalid === 'true') {
|
|
1005
|
-
allComplete = false;
|
|
1006
|
-
}
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
if (!allComplete) return;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
1103
|
if (this.checkCondition(action.condition, { isConfirmDialog: this.isConfirmDialog })) {
|
|
1013
1104
|
this.showError('');
|
|
1014
1105
|
|
|
1015
|
-
|
|
1106
|
+
let processedFormData = { ...this.formData };
|
|
1016
1107
|
const formFields = this.userTask.userTaskConfig.formFields;
|
|
1017
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
|
+
|
|
1018
1116
|
formFields.forEach((field) => {
|
|
1019
1117
|
const fieldValue = processedFormData[field.id];
|
|
1020
1118
|
|
|
@@ -1040,8 +1138,8 @@ export default {
|
|
|
1040
1138
|
|
|
1041
1139
|
let outputIndex;
|
|
1042
1140
|
if (action.isConfirmAction) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1141
|
+
const confirmActionIndex = action.confirmValue ? 1 : 0;
|
|
1142
|
+
outputIndex = this.props.options.length + confirmActionIndex;
|
|
1045
1143
|
} else {
|
|
1046
1144
|
outputIndex = this.props.options.findIndex((element) => element.label === action.label);
|
|
1047
1145
|
}
|
|
@@ -1055,7 +1153,7 @@ export default {
|
|
|
1055
1153
|
try {
|
|
1056
1154
|
const usertaskWithContext = {
|
|
1057
1155
|
...this.userTask,
|
|
1058
|
-
isConfirmDialog: context.isConfirmDialog || false
|
|
1156
|
+
isConfirmDialog: context.isConfirmDialog || false,
|
|
1059
1157
|
};
|
|
1060
1158
|
|
|
1061
1159
|
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + condition + ')');
|
|
@@ -1094,6 +1192,338 @@ export default {
|
|
|
1094
1192
|
}
|
|
1095
1193
|
}
|
|
1096
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
|
+
},
|
|
1097
1527
|
},
|
|
1098
1528
|
};
|
|
1099
1529
|
|
|
@@ -1110,6 +1540,5 @@ function mapItems(type, field) {
|
|
|
1110
1540
|
</script>
|
|
1111
1541
|
|
|
1112
1542
|
<style>
|
|
1113
|
-
/* CSS is auto scoped, but using named classes is still recommended */
|
|
1114
1543
|
@import '../stylesheets/ui-dynamic-form.css';
|
|
1115
1544
|
</style>
|