@5minds/node-red-dashboard-2-processcube-dynamic-form 2.3.0 → 2.4.0-develop-eade06-mfgp8vwu

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
- ? (this.props.confirm_actions ? this.props.confirm_actions.length : 2)
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: 'FormKit',
600
+ type: 'div',
501
601
  props: {
502
- type: 'file',
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
- const confirmActions = [
912
- {
913
- alignment: this.props.confirm_actions[1]?.alignment || 'left',
914
- primary: this.props.confirm_actions[1]?.primary || 'destructive',
915
- label: declineText,
916
- condition: '',
917
- isConfirmAction: true,
918
- confirmFieldId: field.id,
919
- confirmValue: false,
920
- },
921
- {
922
- alignment: this.props.confirm_actions[0]?.alignment || 'right',
923
- primary: this.props.confirm_actions[0]?.primary || 'primary',
924
- label: confirmText,
925
- condition: '',
926
- isConfirmAction: true,
927
- confirmFieldId: field.id,
928
- confirmValue: true,
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
- this.actions = [...filteredActions, ...confirmActions];
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
- 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
+ }
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
- 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
+ }
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
- const processedFormData = { ...this.formData };
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
- const confirmActionIndex = action.confirmValue ? 1 : 0;
1044
- outputIndex = this.props.options.length + confirmActionIndex;
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>