@onehat/ui 0.4.78 → 0.4.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1212,11 +1212,19 @@ function GridComponent(props) {
|
|
|
1212
1212
|
} else {
|
|
1213
1213
|
// Conform the calculated localColumnsConfig to the saved config.
|
|
1214
1214
|
// This should allow us to continue using non-serializable configurations after a refresh
|
|
1215
|
-
const reconstructedLocalColumnsConfig = savedLocalColumnsConfig
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1215
|
+
const reconstructedLocalColumnsConfig = savedLocalColumnsConfig
|
|
1216
|
+
.map((savedConfig) => {
|
|
1217
|
+
const columnConfig = localColumnsConfig.find(localConfig => localConfig.id === savedConfig.id);
|
|
1218
|
+
if (!columnConfig) {
|
|
1219
|
+
console.warn(`Column with id '${savedConfig.id}' not found in current config, skipping`);
|
|
1220
|
+
return null; // Return null for missing columns
|
|
1221
|
+
}
|
|
1222
|
+
_.assign(columnConfig, savedConfig);
|
|
1223
|
+
return columnConfig;
|
|
1224
|
+
})
|
|
1225
|
+
.filter(Boolean); // Remove null entries
|
|
1226
|
+
|
|
1227
|
+
|
|
1220
1228
|
localColumnsConfig = reconstructedLocalColumnsConfig;
|
|
1221
1229
|
}
|
|
1222
1230
|
}
|
|
@@ -1277,7 +1285,7 @@ function GridComponent(props) {
|
|
|
1277
1285
|
applySelectorSelected();
|
|
1278
1286
|
Repository.resumeEvents();
|
|
1279
1287
|
|
|
1280
|
-
if (((Repository.isRemote && !Repository.isLoaded) || forceLoadOnRender) && !disableLoadOnRender) { // default remote repositories to load on render, optionally force or disable load on render
|
|
1288
|
+
if (((Repository.isRemote && !Repository.isLoaded && !Repository.isLoading) || forceLoadOnRender) && !disableLoadOnRender) { // default remote repositories to load on render, optionally force or disable load on render
|
|
1281
1289
|
Repository.load();
|
|
1282
1290
|
}
|
|
1283
1291
|
|
|
@@ -45,6 +45,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
45
45
|
editorType,
|
|
46
46
|
onAdd,
|
|
47
47
|
onChange, // any kind of crud change
|
|
48
|
+
onBeforeDelete,
|
|
48
49
|
onDelete,
|
|
49
50
|
onSave, // this could also be called 'onEdit'
|
|
50
51
|
onEditorClose,
|
|
@@ -289,7 +290,15 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
289
290
|
if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
|
|
290
291
|
return;
|
|
291
292
|
}
|
|
293
|
+
if (onBeforeDelete) {
|
|
294
|
+
// This listener is set by parent components using a prop
|
|
295
|
+
const listenerResult = await onBeforeDelete(selection);
|
|
296
|
+
if (listenerResult === false) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
292
300
|
if (getListeners().onBeforeDelete) {
|
|
301
|
+
// This listener is set by child components using setWithEditListeners()
|
|
293
302
|
const listenerResult = await getListeners().onBeforeDelete();
|
|
294
303
|
if (listenerResult === false) {
|
|
295
304
|
return;
|
|
@@ -345,7 +354,15 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
345
354
|
return;
|
|
346
355
|
}
|
|
347
356
|
const selection = getSelection();
|
|
357
|
+
if (onBeforeDelete) {
|
|
358
|
+
// This listener is set by parent components using a prop
|
|
359
|
+
const listenerResult = await onBeforeDelete(selection);
|
|
360
|
+
if (listenerResult === false) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
348
364
|
if (getListeners().onBeforeDelete) {
|
|
365
|
+
// This listener is set by child components using setWithEditListeners()
|
|
349
366
|
const listenerResult = await getListeners().onBeforeDelete(selection);
|
|
350
367
|
if (listenerResult === false) {
|
|
351
368
|
return;
|
|
@@ -609,7 +609,7 @@ function TreeComponent(props) {
|
|
|
609
609
|
let nodes = [];
|
|
610
610
|
if (Repository) {
|
|
611
611
|
if (!Repository.isDestroyed) {
|
|
612
|
-
if (!Repository.
|
|
612
|
+
if (!Repository.isLoaded) {
|
|
613
613
|
nodes = await Repository.loadRootNodes(1);
|
|
614
614
|
} else {
|
|
615
615
|
nodes = Repository.getRootNodes();
|
|
@@ -58,8 +58,10 @@ import getSaved from '../../Functions/getSaved.js';
|
|
|
58
58
|
import setSaved from '../../Functions/setSaved.js';
|
|
59
59
|
import Folder from '../../Components/Icons/Folder.js';
|
|
60
60
|
import Plus from '../../Components/Icons/Plus.js';
|
|
61
|
-
import
|
|
61
|
+
import Trash from '../../Components/Icons/Trash.js';
|
|
62
62
|
import Edit from '../../Components/Icons/Edit.js';
|
|
63
|
+
import Rotate from '../../Components/Icons/Rotate.js';
|
|
64
|
+
import Download from '../../Components/Icons/Download.js';
|
|
63
65
|
import delay from '../../Functions/delay.js';
|
|
64
66
|
import _ from 'lodash';
|
|
65
67
|
|
|
@@ -86,17 +88,29 @@ function FileCardCustom(props) {
|
|
|
86
88
|
isDownloading = uploadStatus && inArray(uploadStatus, ['preparing', 'uploading', 'success']),
|
|
87
89
|
isPdf = mimetype === 'application/pdf';
|
|
88
90
|
|
|
89
|
-
let cardContent =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
let cardContent =
|
|
92
|
+
<Pressable
|
|
93
|
+
onPress={() => {
|
|
94
|
+
downloadInBackground(downloadUrl);
|
|
95
|
+
}}
|
|
96
|
+
className="Pressable px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
|
|
97
|
+
>
|
|
98
|
+
{isDownloading &&
|
|
99
|
+
<Spinner className="mr-2" />}
|
|
100
|
+
{onSee && isPdf &&
|
|
101
|
+
<IconButton
|
|
102
|
+
className="mr-1"
|
|
103
|
+
icon={Eye}
|
|
104
|
+
onPress={() => onSee(id)}
|
|
105
|
+
/>}
|
|
106
|
+
<Text>{filename}</Text>
|
|
107
|
+
{onDelete &&
|
|
108
|
+
<IconButton
|
|
109
|
+
className="ml-1"
|
|
110
|
+
icon={Xmark}
|
|
111
|
+
onPress={() => onDelete(id)}
|
|
112
|
+
/>}
|
|
113
|
+
</Pressable>;
|
|
100
114
|
|
|
101
115
|
// Wrap with drag source if needed
|
|
102
116
|
if (isDragSource) {
|
|
@@ -123,8 +137,6 @@ function DraggableFileMosaic(props) {
|
|
|
123
137
|
...fileMosaicProps
|
|
124
138
|
} = props;
|
|
125
139
|
|
|
126
|
-
console.log('DraggableFileMosaic render:', { isDragSource, dragSourceType, hasItem: !!dragSourceItem.item });
|
|
127
|
-
|
|
128
140
|
// If not a drag source, just return the regular FileMosaic
|
|
129
141
|
if (!isDragSource) {
|
|
130
142
|
return <FileMosaic {...fileMosaicProps} />;
|
|
@@ -132,7 +144,6 @@ function DraggableFileMosaic(props) {
|
|
|
132
144
|
|
|
133
145
|
// Create a completely separate draggable container
|
|
134
146
|
const DragSourceContainer = withDragSource(({ dragSourceRef, ...dragProps }) => {
|
|
135
|
-
console.log('DragSourceContainer render with props:', dragProps);
|
|
136
147
|
return (
|
|
137
148
|
<div
|
|
138
149
|
ref={dragSourceRef}
|
|
@@ -211,7 +222,7 @@ function AttachmentsElement(props) {
|
|
|
211
222
|
selectorSelectedField = 'id',
|
|
212
223
|
|
|
213
224
|
// withData
|
|
214
|
-
Repository,
|
|
225
|
+
Repository: Attachments,
|
|
215
226
|
|
|
216
227
|
// withAlert
|
|
217
228
|
showModal,
|
|
@@ -227,6 +238,9 @@ function AttachmentsElement(props) {
|
|
|
227
238
|
modelid = useRef(modelidCalc),
|
|
228
239
|
id = props.id || (model && modelid.current ? `attachments-${model}-${modelid.current}` : 'attachments'),
|
|
229
240
|
forceUpdate = useForceUpdate(),
|
|
241
|
+
iconBlobUrlsRef = useRef(new Set()), // to track created blob URLs for cleanup
|
|
242
|
+
modalBlobUrlsRef = useRef(new Set()), // For modal images
|
|
243
|
+
[areBlobUrlsReady, setAreBlobUrlsReady] = useState(false),
|
|
230
244
|
[isReady, setIsReady] = useState(false),
|
|
231
245
|
[isUploading, setIsUploading] = useState(false),
|
|
232
246
|
[isLoading, setIsLoading] = useState(false),
|
|
@@ -259,8 +273,32 @@ function AttachmentsElement(props) {
|
|
|
259
273
|
getFiles = () => {
|
|
260
274
|
return setFilesRaw.current;
|
|
261
275
|
},
|
|
262
|
-
buildFiles = () => {
|
|
263
|
-
|
|
276
|
+
buildFiles = async () => {
|
|
277
|
+
setAreBlobUrlsReady(false);
|
|
278
|
+
cleanupIconBlobUrls();
|
|
279
|
+
|
|
280
|
+
// FilesUI doesn't allow headers to be passed with URLs,
|
|
281
|
+
// but these URLs require authentication.
|
|
282
|
+
// So we need to fetch the files ourselves, create blob URLs,
|
|
283
|
+
// and pass those to FilesUI.
|
|
284
|
+
const files = await Promise.all(_.map(Attachments.entities, async (entity) => {
|
|
285
|
+
let imageUrl = entity.attachments__uri;
|
|
286
|
+
|
|
287
|
+
// create authenticated blob URLs
|
|
288
|
+
try {
|
|
289
|
+
const response = await fetch(entity.attachments__uri, {
|
|
290
|
+
headers: Attachments.headers // Use your repository's headers
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (response.ok) {
|
|
294
|
+
const blob = await response.blob();
|
|
295
|
+
imageUrl = URL.createObjectURL(blob);
|
|
296
|
+
iconBlobUrlsRef.current.add(imageUrl);
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.warn('Failed to fetch authenticated image:', error);
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
return {
|
|
265
303
|
id: entity.id, // string | number The identifier of the file
|
|
266
304
|
// file: null, // File The file object obtained from client drop or selection
|
|
@@ -271,20 +309,39 @@ function AttachmentsElement(props) {
|
|
|
271
309
|
// errors: null, // string[] The list of errors according to the validation criteria or the result of the given custom validation function.
|
|
272
310
|
// uploadStatus: null, // UPLOADSTATUS The current upload status. (e.g. "uploading").
|
|
273
311
|
// uploadMessage: null, // string A message that shows the result of the upload process.
|
|
274
|
-
imageUrl:
|
|
312
|
+
imageUrl: imageUrl, // string A string representation or web url of the image that will be set to the "src" prop of an <img/> tag. If given, the component will use this image source instead of reading the image file.
|
|
275
313
|
downloadUrl: entity.attachments__uri, // string The url to be used to perform a GET request in order to download the file. If defined, the download icon will be shown.
|
|
276
314
|
// progress: null, // number The current percentage of upload progress. This value will have a higher priority over the upload progress value calculated inside the component.
|
|
277
315
|
// extraUploadData: null, // Record<string, any> The additional data that will be sent to the server when files are uploaded individually
|
|
278
316
|
// extraData: null, // Object Any kind of extra data that could be needed.
|
|
279
317
|
// serverResponse: null, // ServerResponse The upload response from server.
|
|
280
318
|
// xhr: null, // XMLHttpRequest A reference to the XHR object that allows the upload, progress and abort events.
|
|
319
|
+
|
|
281
320
|
};
|
|
282
|
-
});
|
|
321
|
+
}));
|
|
283
322
|
setFiles(files);
|
|
323
|
+
setAreBlobUrlsReady(true);
|
|
284
324
|
},
|
|
285
325
|
clearFiles = () => {
|
|
326
|
+
cleanupIconBlobUrls();
|
|
286
327
|
setFiles([]);
|
|
287
328
|
},
|
|
329
|
+
cleanupIconBlobUrls = () => {
|
|
330
|
+
iconBlobUrlsRef.current.forEach((url) => {
|
|
331
|
+
if (url.startsWith('blob:')) {
|
|
332
|
+
URL.revokeObjectURL(url);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
iconBlobUrlsRef.current.clear();
|
|
336
|
+
},
|
|
337
|
+
cleanupModalBlobUrls = () => {
|
|
338
|
+
modalBlobUrlsRef.current.forEach((url) => {
|
|
339
|
+
if (url.startsWith('blob:')) {
|
|
340
|
+
URL.revokeObjectURL(url);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
modalBlobUrlsRef.current.clear();
|
|
344
|
+
},
|
|
288
345
|
onFileDelete = (id) => {
|
|
289
346
|
const
|
|
290
347
|
files = getFiles(),
|
|
@@ -301,11 +358,11 @@ function AttachmentsElement(props) {
|
|
|
301
358
|
doDelete = (id) => {
|
|
302
359
|
const
|
|
303
360
|
files = getFiles(),
|
|
304
|
-
file =
|
|
361
|
+
file = Attachments.getById(id);
|
|
305
362
|
if (file) {
|
|
306
363
|
// if the file exists in the repository, delete it there
|
|
307
|
-
|
|
308
|
-
|
|
364
|
+
Attachments.deleteById(id);
|
|
365
|
+
Attachments.save();
|
|
309
366
|
|
|
310
367
|
} else {
|
|
311
368
|
// simply remove it from the files array
|
|
@@ -388,7 +445,7 @@ function AttachmentsElement(props) {
|
|
|
388
445
|
});
|
|
389
446
|
if (!isError) {
|
|
390
447
|
setIsUploading(false);
|
|
391
|
-
|
|
448
|
+
Attachments.reload();
|
|
392
449
|
if (onUpload) {
|
|
393
450
|
onUpload(files);
|
|
394
451
|
}
|
|
@@ -397,77 +454,42 @@ function AttachmentsElement(props) {
|
|
|
397
454
|
},
|
|
398
455
|
|
|
399
456
|
// Lightbox
|
|
400
|
-
|
|
401
|
-
const files = getFiles();
|
|
402
|
-
if (useFileMosaic) {
|
|
403
|
-
return _.find(files, (file) => file.id === id);
|
|
404
|
-
}
|
|
405
|
-
return _.find(files, { id });
|
|
406
|
-
},
|
|
407
|
-
findPrevFile = (id) => {
|
|
457
|
+
buildModalBody = async (item) => {
|
|
408
458
|
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
currentFile = findFile(id),
|
|
439
|
-
prevFile = findPrevFile(id),
|
|
440
|
-
nextFile = findNextFile(id);
|
|
441
|
-
isPrevDisabled = !prevFile;
|
|
442
|
-
isNextDisabled = !nextFile;
|
|
443
|
-
onPrev = () => {
|
|
444
|
-
updateModalBody(buildModalBody(prevFile.id));
|
|
445
|
-
};
|
|
446
|
-
onNext = () => {
|
|
447
|
-
updateModalBody(buildModalBody(nextFile.id));
|
|
448
|
-
};
|
|
449
|
-
url = currentFile.imageUrl;
|
|
450
|
-
isPdf = url?.match(/\.pdf$/);
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
case ATTACHMENTS_VIEW_MODES__LIST: {
|
|
454
|
-
const
|
|
455
|
-
currentFile = Repository.getById(id),
|
|
456
|
-
currentIx = Repository.getIxById(id),
|
|
457
|
-
prevFile = Repository.getByIx(currentIx - 1),
|
|
458
|
-
nextFile = Repository.getByIx(currentIx + 1);
|
|
459
|
-
isPrevDisabled = !prevFile;
|
|
460
|
-
isNextDisabled = !nextFile;
|
|
461
|
-
onPrev = () => {
|
|
462
|
-
updateModalBody(buildModalBody(prevFile.id));
|
|
463
|
-
};
|
|
464
|
-
onNext = () => {
|
|
465
|
-
updateModalBody(buildModalBody(nextFile.id));
|
|
466
|
-
};
|
|
467
|
-
url = currentFile.attachments__uri;
|
|
468
|
-
isPdf = currentFile.attachments__mimetype === 'application/pdf';
|
|
459
|
+
currentFile = item,
|
|
460
|
+
currentIx = Attachments.getIxById(item.id),
|
|
461
|
+
prevFile = Attachments.getByIx(currentIx - 1),
|
|
462
|
+
nextFile = Attachments.getByIx(currentIx + 1),
|
|
463
|
+
isPrevDisabled = !prevFile,
|
|
464
|
+
isNextDisabled = !nextFile,
|
|
465
|
+
onPrev = async () => {
|
|
466
|
+
cleanupModalBlobUrls();
|
|
467
|
+
const modalBody = await buildModalBody(prevFile);
|
|
468
|
+
updateModalBody(modalBody);
|
|
469
|
+
},
|
|
470
|
+
onNext = async () => {
|
|
471
|
+
cleanupModalBlobUrls();
|
|
472
|
+
const modalBody = await buildModalBody(nextFile);
|
|
473
|
+
updateModalBody(modalBody);
|
|
474
|
+
},
|
|
475
|
+
isPdf = currentFile.attachments__mimetype === 'application/pdf';
|
|
476
|
+
|
|
477
|
+
let url = currentFile.attachments__uri;
|
|
478
|
+
try {
|
|
479
|
+
const response = await fetch(currentFile.attachments__uri, {
|
|
480
|
+
headers: Attachments.headers // Use your repository's headers
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (response.ok) {
|
|
484
|
+
const blob = await response.blob();
|
|
485
|
+
url = URL.createObjectURL(blob);
|
|
486
|
+
modalBlobUrlsRef.current.add(url);
|
|
469
487
|
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.warn('Failed to fetch authenticated file for modal:', error);
|
|
470
490
|
}
|
|
491
|
+
|
|
492
|
+
let body = null;
|
|
471
493
|
if (isPdf) {
|
|
472
494
|
body = <iframe
|
|
473
495
|
src={url}
|
|
@@ -496,18 +518,17 @@ function AttachmentsElement(props) {
|
|
|
496
518
|
/>
|
|
497
519
|
</HStack>;
|
|
498
520
|
},
|
|
499
|
-
onViewLightbox = (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
521
|
+
onViewLightbox = async (item) => {
|
|
522
|
+
cleanupModalBlobUrls();
|
|
523
|
+
const modalBody = await buildModalBody(item);
|
|
504
524
|
showModal({
|
|
505
525
|
title: 'Lightbox',
|
|
506
|
-
body:
|
|
526
|
+
body: modalBody,
|
|
507
527
|
canClose: true,
|
|
508
528
|
includeCancel: true,
|
|
509
529
|
w: 1920,
|
|
510
530
|
h: 1080,
|
|
531
|
+
onClose: cleanupModalBlobUrls,
|
|
511
532
|
});
|
|
512
533
|
},
|
|
513
534
|
|
|
@@ -555,9 +576,54 @@ function AttachmentsElement(props) {
|
|
|
555
576
|
});
|
|
556
577
|
},
|
|
557
578
|
onDeleteDirectory = async () => {
|
|
558
|
-
|
|
579
|
+
|
|
580
|
+
const
|
|
581
|
+
attachmentDirectory = getTreeSelection()[0],
|
|
582
|
+
isRoot = attachmentDirectory.isRoot;
|
|
583
|
+
if (isRoot) {
|
|
584
|
+
alert('Cannot delete the root directory.');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
// check if there are any attachments in this directory or its subdirectories
|
|
590
|
+
const
|
|
591
|
+
url = AttachmentDirectories.api.baseURL + 'AttachmentDirectories/hasAttachments',
|
|
592
|
+
data = {
|
|
593
|
+
attachment_directory_id: treeSelection[0].id,
|
|
594
|
+
},
|
|
595
|
+
result = await AttachmentDirectories._send('POST', url, data);
|
|
596
|
+
|
|
597
|
+
const {
|
|
598
|
+
root,
|
|
599
|
+
success,
|
|
600
|
+
total,
|
|
601
|
+
message
|
|
602
|
+
} = AttachmentDirectories._processServerResponse(result);
|
|
603
|
+
|
|
604
|
+
if (!success) {
|
|
605
|
+
alert(message);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (root.hasAttachments) {
|
|
610
|
+
alert('Cannot delete a directory that contains attachments somewhere down its hierarchy. Please move or delete the attachments first.');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
// transfer selection to the parent node
|
|
616
|
+
const
|
|
617
|
+
parentNode = attachmentDirectory.getParent(),
|
|
618
|
+
newSelection = [parentNode];
|
|
619
|
+
setTreeSelection(newSelection);
|
|
620
|
+
self.children.tree.setSelection(newSelection);
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
// now delete it
|
|
559
624
|
await attachmentDirectory.delete();
|
|
560
625
|
self.children.tree.buildAndSetTreeNodeData();
|
|
626
|
+
|
|
561
627
|
},
|
|
562
628
|
onRenameDirectory = () => {
|
|
563
629
|
const attachmentDirectory = getTreeSelection()[0];
|
|
@@ -606,6 +672,14 @@ function AttachmentsElement(props) {
|
|
|
606
672
|
}}
|
|
607
673
|
/>,
|
|
608
674
|
});
|
|
675
|
+
},
|
|
676
|
+
onReloadDirectories = async () => {
|
|
677
|
+
await AttachmentDirectories.loadRootNodes(2);
|
|
678
|
+
const rootNodes = AttachmentDirectories.getRootNodes();
|
|
679
|
+
if (rootNodes) {
|
|
680
|
+
setTreeSelection(rootNodes);
|
|
681
|
+
self.children.tree.setSelection(rootNodes);
|
|
682
|
+
}
|
|
609
683
|
};
|
|
610
684
|
|
|
611
685
|
if (!_.isEqual(modelidCalc, modelid.current)) {
|
|
@@ -624,9 +698,9 @@ function AttachmentsElement(props) {
|
|
|
624
698
|
setDirectoriesTrue = () => setIsDirectoriesLoading(true),
|
|
625
699
|
setDirectoriesFalse = () => setIsDirectoriesLoading(false);
|
|
626
700
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
701
|
+
Attachments.on('beforeLoad', setTrue);
|
|
702
|
+
Attachments.on('load', setFalse);
|
|
703
|
+
Attachments.on('load', buildFiles);
|
|
630
704
|
if (usesDirectories) {
|
|
631
705
|
AttachmentDirectories.on('beforeLoad', setDirectoriesTrue);
|
|
632
706
|
AttachmentDirectories.on('loadRootNodes', setDirectoriesFalse);
|
|
@@ -636,12 +710,12 @@ function AttachmentsElement(props) {
|
|
|
636
710
|
|
|
637
711
|
if (modelid.current && !_.isArray(modelid.current)) {
|
|
638
712
|
const
|
|
639
|
-
currentConditions =
|
|
713
|
+
currentConditions = Attachments.getParamConditions() || {},
|
|
640
714
|
newConditions = {
|
|
641
715
|
'conditions[Attachments.model]': model,
|
|
642
716
|
'conditions[Attachments.modelid]': modelid.current,
|
|
643
717
|
},
|
|
644
|
-
currentPageSize =
|
|
718
|
+
currentPageSize = Attachments.pageSize,
|
|
645
719
|
newPageSize = showAll ? expandedMax : collapsedMax;
|
|
646
720
|
|
|
647
721
|
// figure out conditions
|
|
@@ -666,34 +740,34 @@ function AttachmentsElement(props) {
|
|
|
666
740
|
}
|
|
667
741
|
let doReload = false;
|
|
668
742
|
if (!_.isEqual(currentConditions, newConditions)) {
|
|
669
|
-
|
|
743
|
+
Attachments.setParams(newConditions);
|
|
670
744
|
doReload = true;
|
|
671
745
|
}
|
|
672
746
|
|
|
673
747
|
// figure out pageSize
|
|
674
748
|
if (!_.isEqual(currentPageSize, newPageSize)) {
|
|
675
|
-
|
|
749
|
+
Attachments.setPageSize(newPageSize);
|
|
676
750
|
doReload = true;
|
|
677
751
|
}
|
|
678
752
|
if (doReload) {
|
|
679
|
-
await
|
|
753
|
+
await Attachments.load();
|
|
680
754
|
}
|
|
681
755
|
if (usesDirectories) {
|
|
682
756
|
const
|
|
683
|
-
wasAlreadyLoaded = AttachmentDirectories.
|
|
684
|
-
currentConditions = AttachmentDirectories.
|
|
757
|
+
wasAlreadyLoaded = AttachmentDirectories.isLoaded,
|
|
758
|
+
currentConditions = AttachmentDirectories.getParamConditions() || {},
|
|
685
759
|
newConditions = {
|
|
686
760
|
'conditions[AttachmentDirectories.model]': selectorSelected.repository.name,
|
|
687
761
|
'conditions[AttachmentDirectories.modelid]': selectorSelected[selectorSelectedField],
|
|
688
762
|
};
|
|
689
763
|
let doReload = false;
|
|
690
764
|
if (!_.isEqual(currentConditions, newConditions)) {
|
|
691
|
-
AttachmentDirectories.
|
|
765
|
+
AttachmentDirectories.setParams(newConditions);
|
|
692
766
|
doReload = true;
|
|
693
767
|
}
|
|
694
768
|
if (doReload) {
|
|
695
769
|
// setTreeSelection([]); // clear it; otherwise we get stale nodes after reloading AttachmentDirectories
|
|
696
|
-
await AttachmentDirectories.
|
|
770
|
+
await AttachmentDirectories.loadRootNodes(2);
|
|
697
771
|
if (wasAlreadyLoaded) {
|
|
698
772
|
const rootNodes = AttachmentDirectories.getRootNodes();
|
|
699
773
|
if (rootNodes) {
|
|
@@ -703,9 +777,9 @@ function AttachmentsElement(props) {
|
|
|
703
777
|
}
|
|
704
778
|
}
|
|
705
779
|
|
|
706
|
-
buildFiles();
|
|
780
|
+
await buildFiles();
|
|
707
781
|
} else {
|
|
708
|
-
|
|
782
|
+
Attachments.clear();
|
|
709
783
|
if (usesDirectories) {
|
|
710
784
|
AttachmentDirectories.clear();
|
|
711
785
|
}
|
|
@@ -728,13 +802,15 @@ function AttachmentsElement(props) {
|
|
|
728
802
|
})();
|
|
729
803
|
|
|
730
804
|
return () => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
805
|
+
Attachments.off('beforeLoad', setTrue);
|
|
806
|
+
Attachments.off('load', setFalse);
|
|
807
|
+
Attachments.off('load', buildFiles);
|
|
734
808
|
if (usesDirectories) {
|
|
735
809
|
AttachmentDirectories.off('beforeLoad', setDirectoriesTrue);
|
|
736
810
|
AttachmentDirectories.off('loadRootNodes', setDirectoriesFalse);
|
|
737
811
|
}
|
|
812
|
+
cleanupIconBlobUrls();
|
|
813
|
+
cleanupModalBlobUrls();
|
|
738
814
|
};
|
|
739
815
|
}, [model, modelid.current, showAll, getTreeSelection()]);
|
|
740
816
|
|
|
@@ -754,117 +830,125 @@ function AttachmentsElement(props) {
|
|
|
754
830
|
const files = getFiles();
|
|
755
831
|
let content = null;
|
|
756
832
|
// icon or list view
|
|
757
|
-
if (viewMode === ATTACHMENTS_VIEW_MODES__ICON) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
'p-1',
|
|
765
|
-
isLoading ? [
|
|
766
|
-
'border-t-4',
|
|
767
|
-
'border-t-[#f00]',
|
|
768
|
-
] : null,
|
|
769
|
-
)}
|
|
770
|
-
>
|
|
771
|
-
<HStack
|
|
833
|
+
if (viewMode === ATTACHMENTS_VIEW_MODES__ICON || isUploading) {
|
|
834
|
+
if (isLoading || !areBlobUrlsReady) {
|
|
835
|
+
content = <VStack className="AttachmentsElement-icon-VStack1 h-full flex-1 border p-1 justify-center items-center">
|
|
836
|
+
<Spinner />
|
|
837
|
+
</VStack>;
|
|
838
|
+
} else {
|
|
839
|
+
content = <VStack
|
|
772
840
|
className={clsx(
|
|
773
|
-
'AttachmentsElement-
|
|
841
|
+
'AttachmentsElement-icon-VStack1',
|
|
774
842
|
'h-full',
|
|
775
843
|
'flex-1',
|
|
776
|
-
'
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
'
|
|
780
|
-
'
|
|
844
|
+
'border',
|
|
845
|
+
'p-1',
|
|
846
|
+
isLoading ? [
|
|
847
|
+
'border-t-4',
|
|
848
|
+
'border-t-[#f00]',
|
|
781
849
|
] : null,
|
|
782
850
|
)}
|
|
783
851
|
>
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
852
|
+
<HStack
|
|
853
|
+
className={clsx(
|
|
854
|
+
'AttachmentsElement-HStack',
|
|
855
|
+
'gap-2',
|
|
856
|
+
'flex-wrap',
|
|
857
|
+
'items-start',
|
|
858
|
+
files.length === 0 ? [
|
|
859
|
+
// So the 'No files' text is centered
|
|
860
|
+
'justify-center',
|
|
861
|
+
'items-center',
|
|
862
|
+
'h-full',
|
|
863
|
+
] : null,
|
|
864
|
+
)}
|
|
865
|
+
>
|
|
866
|
+
{files.length === 0 && <Text className="text-grey-600 italic">No files {usesDirectories ? 'in this directory' : ''}</Text>}
|
|
867
|
+
{files.map((file) => {
|
|
868
|
+
const fileEntity = Attachments.getById(file.id);
|
|
869
|
+
let eyeProps = {};
|
|
870
|
+
if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
|
|
871
|
+
eyeProps = {
|
|
872
|
+
onSee: () => {
|
|
873
|
+
onViewLightbox(fileEntity);
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Create drag source item for this file
|
|
879
|
+
const dragSourceItem = {
|
|
880
|
+
item: fileEntity, // Get the actual entity
|
|
881
|
+
sourceComponentRef: null, // Could be set to a ref if needed
|
|
882
|
+
getDragProxy: () => {
|
|
883
|
+
// Custom drag preview for file items
|
|
884
|
+
return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
|
|
885
|
+
<Text className="font-semibold text-gray-800">{file.name}</Text>
|
|
886
|
+
<Text className="text-sm text-gray-600">File</Text>
|
|
887
|
+
</VStack>;
|
|
888
|
+
}
|
|
790
889
|
};
|
|
791
|
-
}
|
|
792
890
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
onPress={toggleShowAll}
|
|
852
|
-
className="AttachmentsElement-toggleShowAll mt-2"
|
|
853
|
-
text={'Show ' + (showAll ? ' Less' : ' All ' + Repository.total)}
|
|
854
|
-
_text={{
|
|
855
|
-
className: `
|
|
856
|
-
text-grey-600
|
|
857
|
-
italic
|
|
858
|
-
text-left
|
|
859
|
-
w-full
|
|
860
|
-
`,
|
|
861
|
-
}}
|
|
862
|
-
variant="outline"
|
|
863
|
-
/>}
|
|
864
|
-
</VStack>;
|
|
891
|
+
return <Box
|
|
892
|
+
key={file.id}
|
|
893
|
+
className="mr-2"
|
|
894
|
+
>
|
|
895
|
+
{useFileMosaic &&
|
|
896
|
+
<DraggableFileMosaic
|
|
897
|
+
{...file}
|
|
898
|
+
backgroundBlurImage={false}
|
|
899
|
+
onDownload={onDownload}
|
|
900
|
+
{..._fileMosaic}
|
|
901
|
+
{...eyeProps}
|
|
902
|
+
isDragSource={canCrud && usesDirectories}
|
|
903
|
+
dragSourceType="Attachments"
|
|
904
|
+
dragSourceItem={dragSourceItem}
|
|
905
|
+
onDragStart={() => {
|
|
906
|
+
setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
|
|
907
|
+
}}
|
|
908
|
+
onDragEnd={() => {
|
|
909
|
+
setIsDragging(false);
|
|
910
|
+
}}
|
|
911
|
+
/>}
|
|
912
|
+
{!useFileMosaic &&
|
|
913
|
+
<FileCardCustom
|
|
914
|
+
{...file}
|
|
915
|
+
backgroundBlurImage={false}
|
|
916
|
+
{..._fileMosaic}
|
|
917
|
+
{...eyeProps}
|
|
918
|
+
isDragSource={canCrud && usesDirectories}
|
|
919
|
+
dragSourceType="Attachments"
|
|
920
|
+
dragSourceItem={dragSourceItem}
|
|
921
|
+
item={Attachments.getById(file.id)}
|
|
922
|
+
onDragStart={() => {
|
|
923
|
+
setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
|
|
924
|
+
}}
|
|
925
|
+
onDragEnd={() => {
|
|
926
|
+
setIsDragging(false);
|
|
927
|
+
}}
|
|
928
|
+
/>}
|
|
929
|
+
</Box>;
|
|
930
|
+
})}
|
|
931
|
+
</HStack>
|
|
932
|
+
{Attachments.total <= collapsedMax ? null :
|
|
933
|
+
<Button
|
|
934
|
+
onPress={toggleShowAll}
|
|
935
|
+
className="AttachmentsElement-toggleShowAll mt-2"
|
|
936
|
+
text={'Show ' + (showAll ? ' Less' : ' All ' + Attachments.total)}
|
|
937
|
+
_text={{
|
|
938
|
+
className: `
|
|
939
|
+
text-grey-600
|
|
940
|
+
italic
|
|
941
|
+
text-left
|
|
942
|
+
w-full
|
|
943
|
+
`,
|
|
944
|
+
}}
|
|
945
|
+
variant="outline"
|
|
946
|
+
/>}
|
|
947
|
+
</VStack>;
|
|
948
|
+
}
|
|
865
949
|
} else if (viewMode === ATTACHMENTS_VIEW_MODES__LIST) {
|
|
866
950
|
content = <AttachmentsGridEditor
|
|
867
|
-
Repository={
|
|
951
|
+
Repository={Attachments}
|
|
868
952
|
selectionMode={SELECTION_MODE_MULTI}
|
|
869
953
|
showSelectHandle={false}
|
|
870
954
|
disableAdd={true}
|
|
@@ -884,7 +968,7 @@ function AttachmentsElement(props) {
|
|
|
884
968
|
{
|
|
885
969
|
id: 'view',
|
|
886
970
|
header: 'View',
|
|
887
|
-
w:
|
|
971
|
+
w: 60,
|
|
888
972
|
isSortable: false,
|
|
889
973
|
isEditable: false,
|
|
890
974
|
isReorderable: false,
|
|
@@ -892,16 +976,37 @@ function AttachmentsElement(props) {
|
|
|
892
976
|
isHidable: false,
|
|
893
977
|
renderer: (item) => {
|
|
894
978
|
return <IconButton
|
|
895
|
-
className="w-[
|
|
979
|
+
className="w-[60px]"
|
|
896
980
|
icon={Eye}
|
|
897
981
|
_icon={{
|
|
898
982
|
size: 'xl',
|
|
899
983
|
}}
|
|
900
|
-
onPress={() => onViewLightbox(item
|
|
984
|
+
onPress={() => onViewLightbox(item)}
|
|
901
985
|
tooltip="View"
|
|
902
986
|
/>;
|
|
903
987
|
},
|
|
904
988
|
},
|
|
989
|
+
// {
|
|
990
|
+
// id: 'download',
|
|
991
|
+
// header: 'Get',
|
|
992
|
+
// w: 60,
|
|
993
|
+
// isSortable: false,
|
|
994
|
+
// isEditable: false,
|
|
995
|
+
// isReorderable: false,
|
|
996
|
+
// isResizable: false,
|
|
997
|
+
// isHidable: false,
|
|
998
|
+
// renderer: (item) => {
|
|
999
|
+
// return <IconButton
|
|
1000
|
+
// className="w-[60px]"
|
|
1001
|
+
// icon={Download}
|
|
1002
|
+
// _icon={{
|
|
1003
|
+
// size: 'xl',
|
|
1004
|
+
// }}
|
|
1005
|
+
// onPress={() => onDownload(item.id)}
|
|
1006
|
+
// tooltip="Download"
|
|
1007
|
+
// />;
|
|
1008
|
+
// },
|
|
1009
|
+
// },
|
|
905
1010
|
{
|
|
906
1011
|
"id": "attachments__filename",
|
|
907
1012
|
"header": "Filename",
|
|
@@ -910,7 +1015,7 @@ function AttachmentsElement(props) {
|
|
|
910
1015
|
"isEditable": true,
|
|
911
1016
|
"isReorderable": true,
|
|
912
1017
|
"isResizable": true,
|
|
913
|
-
"w":
|
|
1018
|
+
"w": 250
|
|
914
1019
|
},
|
|
915
1020
|
{
|
|
916
1021
|
"id": "attachments__size_formatted",
|
|
@@ -920,7 +1025,7 @@ function AttachmentsElement(props) {
|
|
|
920
1025
|
"isEditable": false,
|
|
921
1026
|
"isReorderable": true,
|
|
922
1027
|
"isResizable": true,
|
|
923
|
-
"w":
|
|
1028
|
+
"w": 100
|
|
924
1029
|
},
|
|
925
1030
|
]}
|
|
926
1031
|
areRowsDragSource={canCrud}
|
|
@@ -988,7 +1093,7 @@ function AttachmentsElement(props) {
|
|
|
988
1093
|
</VStack>;
|
|
989
1094
|
|
|
990
1095
|
// Always wrap content in dropzone when canCrud is true, but conditionally disable functionality
|
|
991
|
-
if (canCrud) {
|
|
1096
|
+
if (canCrud && !isDragging) {
|
|
992
1097
|
content = <Dropzone
|
|
993
1098
|
value={files}
|
|
994
1099
|
onChange={isDragging ? () => {} : onDropzoneChange} // Disable onChange when dragging
|
|
@@ -997,9 +1102,9 @@ function AttachmentsElement(props) {
|
|
|
997
1102
|
maxFileSize={styles.ATTACHMENTS_MAX_FILESIZE}
|
|
998
1103
|
autoClean={true}
|
|
999
1104
|
uploadConfig={{
|
|
1000
|
-
url:
|
|
1105
|
+
url: Attachments.api.baseURL + Attachments.schema.name + '/uploadAttachment',
|
|
1001
1106
|
method: 'POST',
|
|
1002
|
-
headers:
|
|
1107
|
+
headers: Attachments.headers,
|
|
1003
1108
|
autoUpload,
|
|
1004
1109
|
}}
|
|
1005
1110
|
headerConfig={{
|
|
@@ -1122,12 +1227,18 @@ function AttachmentsElement(props) {
|
|
|
1122
1227
|
isDisabled: !treeSelection.length, // disabled if no selection
|
|
1123
1228
|
},
|
|
1124
1229
|
{
|
|
1125
|
-
key: '
|
|
1230
|
+
key: 'Trash',
|
|
1126
1231
|
text: 'Delete Directory',
|
|
1127
1232
|
handler: onDeleteDirectory,
|
|
1128
|
-
icon:
|
|
1233
|
+
icon: Trash,
|
|
1129
1234
|
isDisabled: !treeSelection.length || !treeSelection[0].parentId, // disabled if selection is root or none
|
|
1130
1235
|
},
|
|
1236
|
+
{
|
|
1237
|
+
key: 'Reload',
|
|
1238
|
+
text: 'Reload Directories',
|
|
1239
|
+
handler: onReloadDirectories,
|
|
1240
|
+
icon: Rotate,
|
|
1241
|
+
},
|
|
1131
1242
|
] : [],
|
|
1132
1243
|
}}
|
|
1133
1244
|
/>
|
|
@@ -1158,13 +1269,27 @@ function withAdditionalProps(WrappedComponent) {
|
|
|
1158
1269
|
const {
|
|
1159
1270
|
usesDirectories = false,
|
|
1160
1271
|
} = props,
|
|
1161
|
-
|
|
1272
|
+
[isReady, setIsReady] = useState(false),
|
|
1273
|
+
[AttachmentDirectories] = useState(() => (usesDirectories ? oneHatData.getRepository('AttachmentDirectories', true) : null)), // lazy instantiator, so getRepository is called only once (it's unique, so otherwise, every time this renders, we'd get a new Repository!)
|
|
1274
|
+
[Attachments] = useState(() => oneHatData.getRepository('Attachments', true)); // same
|
|
1162
1275
|
|
|
1276
|
+
useEffect(() => {
|
|
1277
|
+
(async () => {
|
|
1278
|
+
Attachments.setBaseParams(props.baseParams || {}); // have to add the baseParams here, because we're bypassing withData
|
|
1279
|
+
if (!isReady) {
|
|
1280
|
+
setIsReady(true);
|
|
1281
|
+
}
|
|
1282
|
+
})();
|
|
1283
|
+
}, []);
|
|
1284
|
+
|
|
1285
|
+
if (!isReady) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1163
1289
|
return <WrappedComponent
|
|
1164
|
-
model="Attachments"
|
|
1165
|
-
uniqueRepository={true}
|
|
1166
1290
|
reference="attachments"
|
|
1167
1291
|
{...props}
|
|
1292
|
+
Repository={Attachments}
|
|
1168
1293
|
AttachmentDirectories={AttachmentDirectories}
|
|
1169
1294
|
/>;
|
|
1170
1295
|
};
|