@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.
@@ -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 outputsConfirmTerminate = 2;
239
- return (
240
- this.props.options.length +
241
- (this.props.handle_confirmation_dialogs ? 2 : 0) +
242
- (this.props.trigger_on_change ? 1 : 0) +
243
- outputsConfirmTerminate
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: 'FormKit',
600
+ type: 'div',
500
601
  props: {
501
- type: 'file',
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: 'true',
911
- label: confirmText,
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: true,
1016
+ confirmValue: false,
916
1017
  },
917
1018
  {
918
- alignment: 'left',
919
- primary: 'true',
920
- label: declineText,
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: false,
1025
+ confirmValue: true,
925
1026
  },
926
1027
  ];
927
- if (this.props.handle_confirmation_dialogs) {
928
- this.actions = confirmActions;
929
- } else {
930
- this.actions = [...this.actions, ...confirmActions];
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
- this.formData[key] = initialValues[key];
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
- this.formData[key] = finishedFormData[key];
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.label === 'Speichern' || action.label === 'Speichern und nächster') {
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
- const processedFormData = { ...this.formData };
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
- this.send(
1038
- msg,
1039
- this.actions.findIndex((element) => element.label === action.label) +
1040
- (this.isConfirmDialog ? this.props.options.length : 0)
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 func = Function('fields', 'userTask', 'msg', '"use strict"; return (' + condition + ')');
1050
- const result = func(this.formData, this.userTask, this.msg);
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>