@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.
@@ -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,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
- 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
- });
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
- this.actions = [...filteredActions, ...confirmActions];
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
- this.formData[key] = initialValues[key];
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
- this.formData[key] = finishedFormData[key];
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
- const processedFormData = { ...this.formData };
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
- const confirmActionIndex = action.confirmValue ? 1 : 0;
1044
- outputIndex = this.props.options.length + confirmActionIndex;
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>