@5minds/node-red-dashboard-2-processcube-dynamic-form 2.2.1-file-preview-2-247fbf-mfdx0ry5 → 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: {
|
|
@@ -258,27 +331,55 @@ export default {
|
|
|
258
331
|
watch: {
|
|
259
332
|
formData: {
|
|
260
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
|
+
|
|
261
362
|
if (this.props.trigger_on_change) {
|
|
262
363
|
const res = { payload: { formData: newData, userTask: this.userTask } };
|
|
263
364
|
this.send(res, this.totalOutputs - 1);
|
|
264
365
|
}
|
|
265
366
|
},
|
|
266
|
-
collapsed(newVal) {
|
|
267
|
-
if (!newVal && this.hasUserTask) {
|
|
268
|
-
nextTick(() => {
|
|
269
|
-
this.focusFirstFormField();
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
userTask(newVal) {
|
|
274
|
-
if (newVal && !this.collapsed) {
|
|
275
|
-
nextTick(() => {
|
|
276
|
-
this.focusFirstFormField();
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
367
|
deep: true,
|
|
281
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
|
+
},
|
|
282
383
|
},
|
|
283
384
|
created() {
|
|
284
385
|
const currentPath = window.location.pathname;
|
|
@@ -314,6 +415,7 @@ export default {
|
|
|
314
415
|
},
|
|
315
416
|
unmounted() {
|
|
316
417
|
this.$socket?.off('msg-input:' + this.id);
|
|
418
|
+
this.cleanupObjectUrls();
|
|
317
419
|
},
|
|
318
420
|
methods: {
|
|
319
421
|
createComponent(field) {
|
|
@@ -495,25 +597,17 @@ export default {
|
|
|
495
597
|
case 'file':
|
|
496
598
|
const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
|
|
497
599
|
return {
|
|
498
|
-
type: '
|
|
600
|
+
type: 'div',
|
|
499
601
|
props: {
|
|
500
|
-
|
|
501
|
-
id: field.id,
|
|
502
|
-
name,
|
|
503
|
-
label: field.label,
|
|
504
|
-
required: field.required,
|
|
505
|
-
value: this.formData[field.id],
|
|
506
|
-
help: hint,
|
|
507
|
-
innerClass: 'reset-background',
|
|
508
|
-
wrapperClass: '$remove:formkit-wrapper',
|
|
509
|
-
labelClass: 'ui-dynamic-form-input-label',
|
|
510
|
-
inputClass: `input-${this.theme}`,
|
|
511
|
-
readonly: isReadOnly,
|
|
512
|
-
disabled: isReadOnly,
|
|
513
|
-
multiple,
|
|
514
|
-
validation,
|
|
515
|
-
validationVisibility: 'live',
|
|
602
|
+
class: 'ui-dynamic-form-file-wrapper',
|
|
516
603
|
},
|
|
604
|
+
isFileField: true,
|
|
605
|
+
field: field,
|
|
606
|
+
multiple: multiple,
|
|
607
|
+
isReadOnly: isReadOnly,
|
|
608
|
+
hint: hint,
|
|
609
|
+
validation: validation,
|
|
610
|
+
name: name,
|
|
517
611
|
};
|
|
518
612
|
case 'checkbox':
|
|
519
613
|
const options = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
|
|
@@ -874,6 +968,9 @@ export default {
|
|
|
874
968
|
return;
|
|
875
969
|
}
|
|
876
970
|
|
|
971
|
+
this.originalFileData = {};
|
|
972
|
+
this.previewCache = {};
|
|
973
|
+
this.cleanupObjectUrls();
|
|
877
974
|
this.actions = this.props.options;
|
|
878
975
|
|
|
879
976
|
const hasTask = msg.payload && msg.payload.userTask;
|
|
@@ -883,6 +980,8 @@ export default {
|
|
|
883
980
|
} else {
|
|
884
981
|
this.userTask = null;
|
|
885
982
|
this.formData = {};
|
|
983
|
+
this.originalFileData = {};
|
|
984
|
+
this.cleanupObjectUrls();
|
|
886
985
|
return;
|
|
887
986
|
}
|
|
888
987
|
|
|
@@ -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
|
|
|
@@ -1091,6 +1200,88 @@ export default {
|
|
|
1091
1200
|
}
|
|
1092
1201
|
}
|
|
1093
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
|
+
},
|
|
1094
1285
|
async convertFileToBase64(file) {
|
|
1095
1286
|
return new Promise((resolve, reject) => {
|
|
1096
1287
|
const reader = new FileReader();
|
|
@@ -1114,6 +1305,13 @@ export default {
|
|
|
1114
1305
|
for (const field of formFields) {
|
|
1115
1306
|
if (field.type === 'file') {
|
|
1116
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
|
+
}
|
|
1117
1315
|
|
|
1118
1316
|
if (fieldValue) {
|
|
1119
1317
|
const multiple = field.customForm
|
|
@@ -1121,7 +1319,6 @@ export default {
|
|
|
1121
1319
|
: false;
|
|
1122
1320
|
|
|
1123
1321
|
if (multiple && Array.isArray(fieldValue)) {
|
|
1124
|
-
// Handle multiple files
|
|
1125
1322
|
const base64Files = [];
|
|
1126
1323
|
for (const file of fieldValue) {
|
|
1127
1324
|
const processedFile = await this.processIndividualFile(file);
|
|
@@ -1184,6 +1381,158 @@ export default {
|
|
|
1184
1381
|
|
|
1185
1382
|
return fileData;
|
|
1186
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
|
+
},
|
|
1187
1536
|
},
|
|
1188
1537
|
};
|
|
1189
1538
|
|
|
@@ -68,7 +68,7 @@ code {
|
|
|
68
68
|
.ui-dynamic-form-title-default {
|
|
69
69
|
background: linear-gradient(131deg, #18181a 26.76%, #242326 100.16%);
|
|
70
70
|
padding: 32px;
|
|
71
|
-
color: #
|
|
71
|
+
color: #fff;
|
|
72
72
|
font-size: 36px;
|
|
73
73
|
font-weight: 700;
|
|
74
74
|
border-radius: 8px 8px 0px 0px;
|
|
@@ -593,3 +593,111 @@ code {
|
|
|
593
593
|
margin: revert;
|
|
594
594
|
padding: revert;
|
|
595
595
|
}
|
|
596
|
+
|
|
597
|
+
.ui-dynamic-form-image-previews {
|
|
598
|
+
margin-bottom: 16px;
|
|
599
|
+
padding: 16px;
|
|
600
|
+
border: 1px solid #e0e0e0;
|
|
601
|
+
border-radius: 8px;
|
|
602
|
+
background-color: #f9f9f9;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.ui-dynamic-form-image-previews h4 {
|
|
606
|
+
margin: 0 0 12px 0;
|
|
607
|
+
color: #333;
|
|
608
|
+
font-size: 14px;
|
|
609
|
+
font-weight: 600;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.ui-dynamic-form-preview-grid {
|
|
613
|
+
display: flex;
|
|
614
|
+
flex-wrap: wrap;
|
|
615
|
+
gap: 12px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.ui-dynamic-form-preview-item {
|
|
619
|
+
display: flex;
|
|
620
|
+
flex-direction: column;
|
|
621
|
+
align-items: center;
|
|
622
|
+
padding: 8px;
|
|
623
|
+
border: 1px solid #ddd;
|
|
624
|
+
border-radius: 6px;
|
|
625
|
+
background-color: white;
|
|
626
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
627
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
628
|
+
max-width: 150px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.ui-dynamic-form-preview-item:hover {
|
|
632
|
+
transform: translateY(-2px);
|
|
633
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.ui-dynamic-form-preview-image {
|
|
637
|
+
width: 120px;
|
|
638
|
+
height: 80px;
|
|
639
|
+
object-fit: cover;
|
|
640
|
+
border-radius: 4px;
|
|
641
|
+
cursor: pointer;
|
|
642
|
+
border: 2px solid transparent;
|
|
643
|
+
transition: border-color 0.2s ease;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.ui-dynamic-form-preview-image:hover {
|
|
647
|
+
border-color: #1976d2;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.ui-dynamic-form-preview-info {
|
|
651
|
+
display: flex;
|
|
652
|
+
flex-direction: column;
|
|
653
|
+
align-items: center;
|
|
654
|
+
margin-top: 8px;
|
|
655
|
+
text-align: center;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.ui-dynamic-form-preview-name {
|
|
659
|
+
font-size: 12px;
|
|
660
|
+
font-weight: 500;
|
|
661
|
+
color: #333;
|
|
662
|
+
word-break: break-word;
|
|
663
|
+
max-width: 120px;
|
|
664
|
+
overflow: hidden;
|
|
665
|
+
text-overflow: ellipsis;
|
|
666
|
+
display: -webkit-box;
|
|
667
|
+
-webkit-line-clamp: 2;
|
|
668
|
+
line-clamp: 2;
|
|
669
|
+
-webkit-box-orient: vertical;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.ui-dynamic-form-preview-size {
|
|
673
|
+
font-size: 10px;
|
|
674
|
+
color: #666;
|
|
675
|
+
margin-top: 4px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.ui-dynamic-form-file-wrapper {
|
|
679
|
+
width: 100%;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* Dark theme adjustments */
|
|
683
|
+
.ui-dynamic-form-dark .ui-dynamic-form-image-previews {
|
|
684
|
+
background-color: #2d2d2d;
|
|
685
|
+
border-color: #555;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.ui-dynamic-form-dark .ui-dynamic-form-image-previews h4 {
|
|
689
|
+
color: #fff;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.ui-dynamic-form-dark .ui-dynamic-form-preview-item {
|
|
693
|
+
background-color: #3d3d3d;
|
|
694
|
+
border-color: #555;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.ui-dynamic-form-dark .ui-dynamic-form-preview-name {
|
|
698
|
+
color: #fff;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.ui-dynamic-form-dark .ui-dynamic-form-preview-size {
|
|
702
|
+
color: #ccc;
|
|
703
|
+
}
|