@5minds/node-red-dashboard-2-processcube-dynamic-form 2.2.1-feature-f3865e-mf6maz40 → 2.2.1-file-preview-2-7f33a3-mfferizd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/resources/ui-dynamic-form.umd.js +36 -36
- package/ui/components/UIDynamicForm.vue +540 -102
- 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,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,12 +980,16 @@ 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
|
|
|
891
988
|
const formFields = this.userTask.userTaskConfig.formFields;
|
|
892
989
|
const formFieldIds = formFields.map((ff) => ff.id);
|
|
893
990
|
const initialValues = this.userTask.startToken;
|
|
991
|
+
console.log('Initial Values:', initialValues);
|
|
992
|
+
console.log('Form Fields:', formFields);
|
|
894
993
|
const finishedFormData = msg.payload.formData;
|
|
895
994
|
this.formIsFinished = !!msg.payload.formData;
|
|
896
995
|
if (this.formIsFinished) {
|
|
@@ -908,55 +1007,55 @@ export default {
|
|
|
908
1007
|
const confirmText = customForm.confirmButtonText ?? 'Confirm';
|
|
909
1008
|
const declineText = customForm.declineButtonText ?? 'Decline';
|
|
910
1009
|
|
|
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
|
-
});
|
|
1010
|
+
const confirmActions = [
|
|
1011
|
+
{
|
|
1012
|
+
alignment: this.props.confirm_actions[1]?.alignment || 'left',
|
|
1013
|
+
primary: this.props.confirm_actions[1]?.primary || 'destructive',
|
|
1014
|
+
label: declineText,
|
|
1015
|
+
condition: '',
|
|
1016
|
+
isConfirmAction: true,
|
|
1017
|
+
confirmFieldId: field.id,
|
|
1018
|
+
confirmValue: false,
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
alignment: this.props.confirm_actions[0]?.alignment || 'right',
|
|
1022
|
+
primary: this.props.confirm_actions[0]?.primary || 'primary',
|
|
1023
|
+
label: confirmText,
|
|
1024
|
+
condition: '',
|
|
1025
|
+
isConfirmAction: true,
|
|
1026
|
+
confirmFieldId: field.id,
|
|
1027
|
+
confirmValue: true,
|
|
1028
|
+
},
|
|
1029
|
+
];
|
|
947
1030
|
|
|
948
|
-
|
|
949
|
-
|
|
1031
|
+
const filteredActions = this.props.options.filter((action) => {
|
|
1032
|
+
if (!action.condition) return true;
|
|
1033
|
+
try {
|
|
1034
|
+
const usertaskWithContext = {
|
|
1035
|
+
...this.userTask,
|
|
1036
|
+
isConfirmDialog: true,
|
|
1037
|
+
};
|
|
1038
|
+
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
1039
|
+
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
1040
|
+
return Boolean(result);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
console.error('Error while evaluating condition: ' + err);
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
this.actions = [...filteredActions, ...confirmActions];
|
|
1048
|
+
}
|
|
950
1049
|
});
|
|
951
1050
|
}
|
|
952
1051
|
|
|
953
1052
|
if (!hasConfirmField && this.props.handle_confirmation_dialogs) {
|
|
954
|
-
this.actions = this.props.options.filter(action => {
|
|
1053
|
+
this.actions = this.props.options.filter((action) => {
|
|
955
1054
|
if (!action.condition) return true;
|
|
956
1055
|
try {
|
|
957
1056
|
const usertaskWithContext = {
|
|
958
1057
|
...this.userTask,
|
|
959
|
-
isConfirmDialog: false
|
|
1058
|
+
isConfirmDialog: false,
|
|
960
1059
|
};
|
|
961
1060
|
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + action.condition + ')');
|
|
962
1061
|
const result = func(this.formData, usertaskWithContext, this.msg);
|
|
@@ -972,7 +1071,12 @@ export default {
|
|
|
972
1071
|
Object.keys(initialValues)
|
|
973
1072
|
.filter((key) => formFieldIds.includes(key))
|
|
974
1073
|
.forEach((key) => {
|
|
975
|
-
|
|
1074
|
+
const field = formFields.find((f) => f.id === key);
|
|
1075
|
+
if (field && field.type === 'file') {
|
|
1076
|
+
this.formData[key] = this.transformBase64ToFormKitFormat(initialValues[key], field);
|
|
1077
|
+
} else {
|
|
1078
|
+
this.formData[key] = initialValues[key];
|
|
1079
|
+
}
|
|
976
1080
|
});
|
|
977
1081
|
}
|
|
978
1082
|
|
|
@@ -980,7 +1084,12 @@ export default {
|
|
|
980
1084
|
Object.keys(finishedFormData)
|
|
981
1085
|
.filter((key) => formFieldIds.includes(key))
|
|
982
1086
|
.forEach((key) => {
|
|
983
|
-
|
|
1087
|
+
const field = formFields.find((f) => f.id === key);
|
|
1088
|
+
if (field && field.type === 'file') {
|
|
1089
|
+
this.formData[key] = this.transformBase64ToFormKitFormat(finishedFormData[key], field);
|
|
1090
|
+
} else {
|
|
1091
|
+
this.formData[key] = finishedFormData[key];
|
|
1092
|
+
}
|
|
984
1093
|
});
|
|
985
1094
|
}
|
|
986
1095
|
|
|
@@ -988,33 +1097,30 @@ export default {
|
|
|
988
1097
|
this.focusFirstFormField();
|
|
989
1098
|
});
|
|
990
1099
|
},
|
|
991
|
-
actionFn(action) {
|
|
1100
|
+
async actionFn(action) {
|
|
992
1101
|
if (action.isConfirmAction && action.confirmFieldId) {
|
|
993
1102
|
this.formData[action.confirmFieldId] = action.confirmValue;
|
|
994
1103
|
}
|
|
995
1104
|
|
|
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
1105
|
if (this.checkCondition(action.condition, { isConfirmDialog: this.isConfirmDialog })) {
|
|
1013
1106
|
this.showError('');
|
|
1014
1107
|
|
|
1015
|
-
|
|
1108
|
+
let processedFormData = { ...this.formData };
|
|
1016
1109
|
const formFields = this.userTask.userTaskConfig.formFields;
|
|
1017
1110
|
|
|
1111
|
+
try {
|
|
1112
|
+
console.log(
|
|
1113
|
+
'Processing file fields:',
|
|
1114
|
+
formFields.filter((f) => f.type === 'file')
|
|
1115
|
+
);
|
|
1116
|
+
processedFormData = await this.processFileFields(processedFormData, formFields);
|
|
1117
|
+
console.log('File processing completed successfully');
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
console.error('Error processing file fields:', error);
|
|
1120
|
+
this.showError('Fehler beim Verarbeiten der Dateien');
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1018
1124
|
formFields.forEach((field) => {
|
|
1019
1125
|
const fieldValue = processedFormData[field.id];
|
|
1020
1126
|
|
|
@@ -1040,8 +1146,8 @@ export default {
|
|
|
1040
1146
|
|
|
1041
1147
|
let outputIndex;
|
|
1042
1148
|
if (action.isConfirmAction) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1149
|
+
const confirmActionIndex = action.confirmValue ? 1 : 0;
|
|
1150
|
+
outputIndex = this.props.options.length + confirmActionIndex;
|
|
1045
1151
|
} else {
|
|
1046
1152
|
outputIndex = this.props.options.findIndex((element) => element.label === action.label);
|
|
1047
1153
|
}
|
|
@@ -1055,7 +1161,7 @@ export default {
|
|
|
1055
1161
|
try {
|
|
1056
1162
|
const usertaskWithContext = {
|
|
1057
1163
|
...this.userTask,
|
|
1058
|
-
isConfirmDialog: context.isConfirmDialog || false
|
|
1164
|
+
isConfirmDialog: context.isConfirmDialog || false,
|
|
1059
1165
|
};
|
|
1060
1166
|
|
|
1061
1167
|
const func = Function('fields', 'usertask', 'msg', '"use strict"; return (' + condition + ')');
|
|
@@ -1094,6 +1200,339 @@ export default {
|
|
|
1094
1200
|
}
|
|
1095
1201
|
}
|
|
1096
1202
|
},
|
|
1203
|
+
/**
|
|
1204
|
+
* Transforms base64 file data to FormKit-compatible format for display.
|
|
1205
|
+
*
|
|
1206
|
+
* FormKit file inputs expect an array of objects with 'name' property for display.
|
|
1207
|
+
* This function converts the base64 format (with 'data' property) to the display format,
|
|
1208
|
+
* while preserving the original data in originalFileData for form submission.
|
|
1209
|
+
*
|
|
1210
|
+
* Expected input format from initialValues:
|
|
1211
|
+
* {
|
|
1212
|
+
* name: "filename.ext",
|
|
1213
|
+
* size: 12345,
|
|
1214
|
+
* type: "image/png",
|
|
1215
|
+
* data: "base64string..."
|
|
1216
|
+
* }
|
|
1217
|
+
*
|
|
1218
|
+
* Output format for FormKit:
|
|
1219
|
+
* [
|
|
1220
|
+
* { name: "filename.ext" }
|
|
1221
|
+
* ]
|
|
1222
|
+
*
|
|
1223
|
+
* @param {Object|Array} fileData - The file data from initial values
|
|
1224
|
+
* @param {Object} field - The form field configuration
|
|
1225
|
+
* @returns {Array} FormKit-compatible file array
|
|
1226
|
+
*/
|
|
1227
|
+
transformBase64ToFormKitFormat(fileData, field) {
|
|
1228
|
+
if (
|
|
1229
|
+
Array.isArray(fileData) &&
|
|
1230
|
+
fileData.length > 0 &&
|
|
1231
|
+
typeof fileData[0] === 'object' &&
|
|
1232
|
+
fileData[0].name &&
|
|
1233
|
+
!fileData[0].data
|
|
1234
|
+
) {
|
|
1235
|
+
return fileData;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const fieldId = field.id;
|
|
1239
|
+
const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
|
|
1240
|
+
|
|
1241
|
+
if (fileData && typeof fileData === 'object' && fileData.name && fileData.data) {
|
|
1242
|
+
this.originalFileData[fieldId] = fileData;
|
|
1243
|
+
|
|
1244
|
+
const formKitFile = {
|
|
1245
|
+
name: fileData.name,
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
return multiple ? [formKitFile] : [formKitFile];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (Array.isArray(fileData)) {
|
|
1252
|
+
this.originalFileData[fieldId] = fileData;
|
|
1253
|
+
|
|
1254
|
+
const formKitFiles = fileData
|
|
1255
|
+
.filter((file) => file && typeof file === 'object' && file.name)
|
|
1256
|
+
.map((file) => ({
|
|
1257
|
+
name: file.name,
|
|
1258
|
+
}));
|
|
1259
|
+
|
|
1260
|
+
return formKitFiles;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return [];
|
|
1264
|
+
},
|
|
1265
|
+
hasActualFileObjects(value) {
|
|
1266
|
+
if (!value) return false;
|
|
1267
|
+
|
|
1268
|
+
if (Array.isArray(value)) {
|
|
1269
|
+
return value.some((item) => {
|
|
1270
|
+
return (
|
|
1271
|
+
item instanceof File ||
|
|
1272
|
+
(item && item.file instanceof File) ||
|
|
1273
|
+
(item && item.file instanceof FileList && item.file.length > 0)
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return (
|
|
1279
|
+
value instanceof File ||
|
|
1280
|
+
(value instanceof FileList && value.length > 0) ||
|
|
1281
|
+
(value && value.file instanceof File) ||
|
|
1282
|
+
(value && value.file instanceof FileList && value.file.length > 0)
|
|
1283
|
+
);
|
|
1284
|
+
},
|
|
1285
|
+
async convertFileToBase64(file) {
|
|
1286
|
+
return new Promise((resolve, reject) => {
|
|
1287
|
+
const reader = new FileReader();
|
|
1288
|
+
reader.onload = () => {
|
|
1289
|
+
const base64 = reader.result.split(',')[1];
|
|
1290
|
+
resolve({
|
|
1291
|
+
name: file.name,
|
|
1292
|
+
size: file.size,
|
|
1293
|
+
type: file.type,
|
|
1294
|
+
lastModified: file.lastModified,
|
|
1295
|
+
data: base64,
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1298
|
+
reader.onerror = reject;
|
|
1299
|
+
reader.readAsDataURL(file);
|
|
1300
|
+
});
|
|
1301
|
+
},
|
|
1302
|
+
async processFileFields(formData, formFields) {
|
|
1303
|
+
const processedData = { ...formData };
|
|
1304
|
+
|
|
1305
|
+
for (const field of formFields) {
|
|
1306
|
+
if (field.type === 'file') {
|
|
1307
|
+
const fieldValue = processedData[field.id];
|
|
1308
|
+
const fieldId = field.id;
|
|
1309
|
+
|
|
1310
|
+
if (this.originalFileData[fieldId]) {
|
|
1311
|
+
processedData[fieldId] = this.originalFileData[fieldId];
|
|
1312
|
+
console.log(`Using original file data for field ${fieldId}:`, this.originalFileData[fieldId]);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (fieldValue) {
|
|
1317
|
+
const multiple = field.customForm
|
|
1318
|
+
? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true'
|
|
1319
|
+
: false;
|
|
1320
|
+
|
|
1321
|
+
if (multiple && 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] = base64Files;
|
|
1330
|
+
} else if (Array.isArray(fieldValue)) {
|
|
1331
|
+
const base64Files = [];
|
|
1332
|
+
for (const file of fieldValue) {
|
|
1333
|
+
const processedFile = await this.processIndividualFile(file);
|
|
1334
|
+
if (processedFile) {
|
|
1335
|
+
base64Files.push(processedFile);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
processedData[field.id] = multiple ? base64Files : base64Files[0] || null;
|
|
1339
|
+
} else if (fieldValue) {
|
|
1340
|
+
const processedFile = await this.processIndividualFile(fieldValue);
|
|
1341
|
+
if (processedFile) {
|
|
1342
|
+
processedData[field.id] = processedFile;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return processedData;
|
|
1350
|
+
},
|
|
1351
|
+
async processIndividualFile(fileData) {
|
|
1352
|
+
let actualFile = null;
|
|
1353
|
+
|
|
1354
|
+
if (fileData instanceof File) {
|
|
1355
|
+
actualFile = fileData;
|
|
1356
|
+
} else if (fileData instanceof FileList && fileData.length > 0) {
|
|
1357
|
+
actualFile = fileData[0];
|
|
1358
|
+
} else if (fileData && fileData.file instanceof File) {
|
|
1359
|
+
actualFile = fileData.file;
|
|
1360
|
+
} else if (fileData && fileData.file instanceof FileList && fileData.file.length > 0) {
|
|
1361
|
+
actualFile = fileData.file[0];
|
|
1362
|
+
} else if (Array.isArray(fileData) && fileData.length > 0) {
|
|
1363
|
+
if (fileData[0] instanceof File) {
|
|
1364
|
+
actualFile = fileData[0];
|
|
1365
|
+
} else if (fileData[0] && fileData[0].file instanceof File) {
|
|
1366
|
+
actualFile = fileData[0].file;
|
|
1367
|
+
} else if (fileData[0] && fileData[0].file instanceof FileList && fileData[0].file.length > 0) {
|
|
1368
|
+
actualFile = fileData[0].file[0];
|
|
1369
|
+
}
|
|
1370
|
+
} else if (typeof fileData === 'object' && fileData.data && fileData.name) {
|
|
1371
|
+
return fileData;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (actualFile instanceof File) {
|
|
1375
|
+
return await this.convertFileToBase64(actualFile);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (fileData && typeof fileData === 'object' && !fileData.data) {
|
|
1379
|
+
console.warn('Could not process file data:', fileData);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return fileData;
|
|
1383
|
+
},
|
|
1384
|
+
isImageFile(fileData) {
|
|
1385
|
+
if (!fileData || typeof fileData !== 'object') return false;
|
|
1386
|
+
|
|
1387
|
+
const mimeType = fileData.type;
|
|
1388
|
+
if (!mimeType) return false;
|
|
1389
|
+
|
|
1390
|
+
return mimeType.startsWith('image/');
|
|
1391
|
+
},
|
|
1392
|
+
generateImagePreviewUrl(fileData) {
|
|
1393
|
+
if (!fileData || !fileData.data || !fileData.type) return null;
|
|
1394
|
+
|
|
1395
|
+
return `data:${fileData.type};base64,${fileData.data}`;
|
|
1396
|
+
},
|
|
1397
|
+
getFilePreviewsForField(fieldId) {
|
|
1398
|
+
const currentData = this.formData[fieldId] || null;
|
|
1399
|
+
const cachedOriginalData = this.originalFileData[fieldId] || null;
|
|
1400
|
+
const cacheKey = `${fieldId}_${JSON.stringify(currentData)}_${JSON.stringify(cachedOriginalData)}`;
|
|
1401
|
+
|
|
1402
|
+
if (this.previewCache[cacheKey]) {
|
|
1403
|
+
return this.previewCache[cacheKey];
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const currentFormData = this.formData[fieldId];
|
|
1407
|
+
const originalFileData = this.originalFileData[fieldId];
|
|
1408
|
+
|
|
1409
|
+
if (!currentFormData && !originalFileData) {
|
|
1410
|
+
this.previewCache[cacheKey] = [];
|
|
1411
|
+
return [];
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
let previews = [];
|
|
1415
|
+
|
|
1416
|
+
const hasNewFileObjects =
|
|
1417
|
+
currentFormData &&
|
|
1418
|
+
Array.isArray(currentFormData) &&
|
|
1419
|
+
currentFormData.length > 0 &&
|
|
1420
|
+
this.hasActualFileObjects(currentFormData);
|
|
1421
|
+
|
|
1422
|
+
if (hasNewFileObjects) {
|
|
1423
|
+
previews = this.generatePreviewsFromCurrentFiles(currentFormData, fieldId);
|
|
1424
|
+
} else if (originalFileData) {
|
|
1425
|
+
const fileArray = Array.isArray(originalFileData) ? originalFileData : [originalFileData];
|
|
1426
|
+
previews = fileArray
|
|
1427
|
+
.filter((file) => this.isImageFile(file))
|
|
1428
|
+
.map((file) => ({
|
|
1429
|
+
name: file.name,
|
|
1430
|
+
url: this.generateImagePreviewUrl(file),
|
|
1431
|
+
size: file.size,
|
|
1432
|
+
type: file.type,
|
|
1433
|
+
}));
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
this.previewCache[cacheKey] = previews;
|
|
1437
|
+
|
|
1438
|
+
return previews;
|
|
1439
|
+
},
|
|
1440
|
+
generatePreviewsFromCurrentFiles(fileArray, fieldId = null) {
|
|
1441
|
+
if (fieldId && this.objectUrlsByField[fieldId]) {
|
|
1442
|
+
this.objectUrlsByField[fieldId].forEach((url) => URL.revokeObjectURL(url));
|
|
1443
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1444
|
+
} else if (fieldId) {
|
|
1445
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const previews = fileArray
|
|
1449
|
+
.filter((fileItem) => {
|
|
1450
|
+
let actualFile = null;
|
|
1451
|
+
|
|
1452
|
+
if (fileItem instanceof File) {
|
|
1453
|
+
actualFile = fileItem;
|
|
1454
|
+
} else if (fileItem && fileItem.file instanceof File) {
|
|
1455
|
+
actualFile = fileItem.file;
|
|
1456
|
+
} else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
|
|
1457
|
+
actualFile = fileItem[0];
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return actualFile && actualFile.type && actualFile.type.startsWith('image/');
|
|
1461
|
+
})
|
|
1462
|
+
.map((fileItem) => {
|
|
1463
|
+
let actualFile = null;
|
|
1464
|
+
|
|
1465
|
+
if (fileItem instanceof File) {
|
|
1466
|
+
actualFile = fileItem;
|
|
1467
|
+
} else if (fileItem && fileItem.file instanceof File) {
|
|
1468
|
+
actualFile = fileItem.file;
|
|
1469
|
+
} else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
|
|
1470
|
+
actualFile = fileItem[0];
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const url = actualFile ? URL.createObjectURL(actualFile) : null;
|
|
1474
|
+
|
|
1475
|
+
if (url && fieldId) {
|
|
1476
|
+
if (!this.objectUrlsByField[fieldId]) {
|
|
1477
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1478
|
+
}
|
|
1479
|
+
this.objectUrlsByField[fieldId].push(url);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return {
|
|
1483
|
+
name: actualFile.name,
|
|
1484
|
+
url: url,
|
|
1485
|
+
size: actualFile.size,
|
|
1486
|
+
type: actualFile.type,
|
|
1487
|
+
isObjectUrl: true,
|
|
1488
|
+
};
|
|
1489
|
+
})
|
|
1490
|
+
.filter((preview) => preview.url !== null);
|
|
1491
|
+
|
|
1492
|
+
return previews;
|
|
1493
|
+
},
|
|
1494
|
+
formatFileSize(bytes) {
|
|
1495
|
+
if (!bytes) return '0 B';
|
|
1496
|
+
|
|
1497
|
+
const k = 1024;
|
|
1498
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1499
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1500
|
+
|
|
1501
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1502
|
+
},
|
|
1503
|
+
openImageModal(preview) {
|
|
1504
|
+
this.modalImage = preview;
|
|
1505
|
+
this.imageModalOpen = true;
|
|
1506
|
+
},
|
|
1507
|
+
closeImageModal() {
|
|
1508
|
+
this.imageModalOpen = false;
|
|
1509
|
+
this.modalImage = null;
|
|
1510
|
+
},
|
|
1511
|
+
cleanupObjectUrls(fieldId = null) {
|
|
1512
|
+
if (fieldId) {
|
|
1513
|
+
if (this.objectUrlsByField[fieldId]) {
|
|
1514
|
+
this.objectUrlsByField[fieldId].forEach((url) => {
|
|
1515
|
+
URL.revokeObjectURL(url);
|
|
1516
|
+
});
|
|
1517
|
+
this.objectUrlsByField[fieldId] = [];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
Object.keys(this.previewCache).forEach((key) => {
|
|
1521
|
+
if (key.startsWith(fieldId + '_')) {
|
|
1522
|
+
delete this.previewCache[key];
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
} else {
|
|
1526
|
+
Object.keys(this.objectUrlsByField).forEach((fieldId) => {
|
|
1527
|
+
this.objectUrlsByField[fieldId].forEach((url) => {
|
|
1528
|
+
URL.revokeObjectURL(url);
|
|
1529
|
+
});
|
|
1530
|
+
});
|
|
1531
|
+
this.objectUrlsByField = {};
|
|
1532
|
+
|
|
1533
|
+
this.previewCache = {};
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1097
1536
|
},
|
|
1098
1537
|
};
|
|
1099
1538
|
|
|
@@ -1110,6 +1549,5 @@ function mapItems(type, field) {
|
|
|
1110
1549
|
</script>
|
|
1111
1550
|
|
|
1112
1551
|
<style>
|
|
1113
|
-
/* CSS is auto scoped, but using named classes is still recommended */
|
|
1114
1552
|
@import '../stylesheets/ui-dynamic-form.css';
|
|
1115
1553
|
</style>
|