@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.78",
3
+ "version": "0.4.80",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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.map((savedConfig) => { // foreach saved column, in the order it was saved...
1216
- const columnConfig = localColumnsConfig.find(localConfig => localConfig.id === savedConfig.id); // find the corresponding column in localColumnsConfig
1217
- _.assign(columnConfig, savedConfig);
1218
- return columnConfig;
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;
@@ -72,6 +72,10 @@ export default function withModal(WrappedComponent) {
72
72
  throw new Error('withModal: body is required for showModal');
73
73
  }
74
74
 
75
+ if (_.isFunction(body)) {
76
+ body = body();
77
+ }
78
+
75
79
  setTitle(title);
76
80
  setBody(body);
77
81
  setCanClose(canClose);
@@ -609,7 +609,7 @@ function TreeComponent(props) {
609
609
  let nodes = [];
610
610
  if (Repository) {
611
611
  if (!Repository.isDestroyed) {
612
- if (!Repository.areRootNodesLoaded) {
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 Minus from '../../Components/Icons/Minus.js';
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 = <Pressable
90
- onPress={() => {
91
- downloadInBackground(downloadUrl);
92
- }}
93
- className="Pressable px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
94
- >
95
- {isDownloading && <Spinner className="mr-2" />}
96
- {onSee && isPdf && <IconButton className="mr-1" icon={Eye} onPress={() => onSee(id)} />}
97
- <Text>{filename}</Text>
98
- {onDelete && <IconButton className="ml-1" icon={Xmark} onPress={() => onDelete(id)} />}
99
- </Pressable>;
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
- const files = _.map(Repository.entities, (entity) => {
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: entity.attachments__uri, // 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.
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 = Repository.getById(id);
361
+ file = Attachments.getById(id);
305
362
  if (file) {
306
363
  // if the file exists in the repository, delete it there
307
- Repository.deleteById(id);
308
- Repository.save();
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
- Repository.reload();
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
- findFile = (id) => {
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
- files = getFiles(),
410
- currentFile = findFile(id),
411
- currentIx = _.findIndex(files, currentFile);
412
- if (currentIx > 0) {
413
- return files[currentIx - 1];
414
- }
415
- return null;
416
- }
417
- findNextFile = (url, id) => {
418
- const
419
- files = getFiles(),
420
- currentFile = findFile(id),
421
- currentIx = _.findIndex(files, currentFile);
422
- if (currentIx < files.length - 1) {
423
- return files[currentIx + 1];
424
- }
425
- return null;
426
- },
427
- buildModalBody = (id) => {
428
- let isPdf = false,
429
- url = null,
430
- body = null,
431
- isPrevDisabled = false,
432
- isNextDisabled = false,
433
- onPrev,
434
- onNext;
435
- switch(viewMode) {
436
- case ATTACHMENTS_VIEW_MODES__ICON: {
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 = (id) => {
500
- if (!id) {
501
- alert('Cannot view lightbox until image is uploaded.');
502
- return;
503
- }
521
+ onViewLightbox = async (item) => {
522
+ cleanupModalBlobUrls();
523
+ const modalBody = await buildModalBody(item);
504
524
  showModal({
505
525
  title: 'Lightbox',
506
- body: buildModalBody(id),
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
- const attachmentDirectory = getTreeSelection()[0];
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
- Repository.on('beforeLoad', setTrue);
628
- Repository.on('load', setFalse);
629
- Repository.on('load', buildFiles);
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 = Repository.getBaseParamConditions() || {},
713
+ currentConditions = Attachments.getParamConditions() || {},
640
714
  newConditions = {
641
715
  'conditions[Attachments.model]': model,
642
716
  'conditions[Attachments.modelid]': modelid.current,
643
717
  },
644
- currentPageSize = Repository.pageSize,
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
- Repository.setBaseParams(newConditions);
743
+ Attachments.setParams(newConditions);
670
744
  doReload = true;
671
745
  }
672
746
 
673
747
  // figure out pageSize
674
748
  if (!_.isEqual(currentPageSize, newPageSize)) {
675
- Repository.setPageSize(newPageSize);
749
+ Attachments.setPageSize(newPageSize);
676
750
  doReload = true;
677
751
  }
678
752
  if (doReload) {
679
- await Repository.load();
753
+ await Attachments.load();
680
754
  }
681
755
  if (usesDirectories) {
682
756
  const
683
- wasAlreadyLoaded = AttachmentDirectories.areRootNodesLoaded,
684
- currentConditions = AttachmentDirectories.getBaseParamConditions() || {},
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.setBaseParams(newConditions);
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.reload();
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
- Repository.clear();
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
- Repository.off('beforeLoad', setTrue);
732
- Repository.off('load', setFalse);
733
- Repository.off('load', buildFiles);
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
- content = <VStack
759
- className={clsx(
760
- 'AttachmentsElement-icon-VStack1',
761
- 'h-full',
762
- 'flex-1',
763
- 'border',
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-HStack',
841
+ 'AttachmentsElement-icon-VStack1',
774
842
  'h-full',
775
843
  'flex-1',
776
- 'flex-wrap',
777
- files.length === 0 ? [
778
- // So the 'No files' text is centered
779
- 'justify-center',
780
- 'items-center',
844
+ 'border',
845
+ 'p-1',
846
+ isLoading ? [
847
+ 'border-t-4',
848
+ 'border-t-[#f00]',
781
849
  ] : null,
782
850
  )}
783
851
  >
784
- {files.length === 0 && <Text className="text-grey-600 italic">No files {usesDirectories ? 'in this directory' : ''}</Text>}
785
- {files.map((file) => {
786
- let eyeProps = {};
787
- if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
788
- eyeProps = {
789
- onSee: onViewLightbox,
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
- // Create drag source item for this file
794
- const fileEntity = Repository.getById(file.id);
795
- console.log('Drag setup for file:', file.id, 'Entity:', fileEntity, 'canCrud:', canCrud, 'usesDirectories:', usesDirectories);
796
- const dragSourceItem = {
797
- item: fileEntity, // Get the actual entity
798
- sourceComponentRef: null, // Could be set to a ref if needed
799
- getDragProxy: () => {
800
- // Custom drag preview for file items
801
- return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
802
- <Text className="font-semibold text-gray-800">{file.name}</Text>
803
- <Text className="text-sm text-gray-600">File</Text>
804
- </VStack>;
805
- }
806
- };
807
-
808
- return <Box
809
- key={file.id}
810
- className="mr-2"
811
- >
812
- {useFileMosaic &&
813
- <DraggableFileMosaic
814
- {...file}
815
- backgroundBlurImage={false}
816
- onDownload={onDownload}
817
- {..._fileMosaic}
818
- {...eyeProps}
819
- isDragSource={canCrud && usesDirectories}
820
- dragSourceType="Attachments"
821
- dragSourceItem={dragSourceItem}
822
- onDragStart={() => {
823
- setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
824
- }}
825
- onDragEnd={() => {
826
- setIsDragging(false);
827
- }}
828
- />}
829
- {!useFileMosaic &&
830
- <FileCardCustom
831
- {...file}
832
- backgroundBlurImage={false}
833
- {..._fileMosaic}
834
- {...eyeProps}
835
- isDragSource={canCrud && usesDirectories}
836
- dragSourceType="Attachments"
837
- dragSourceItem={dragSourceItem}
838
- item={Repository.getById(file.id)}
839
- onDragStart={() => {
840
- setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
841
- }}
842
- onDragEnd={() => {
843
- setIsDragging(false);
844
- }}
845
- />}
846
- </Box>;
847
- })}
848
- </HStack>
849
- {Repository.total <= collapsedMax ? null :
850
- <Button
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={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: 70,
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-[70px]"
979
+ className="w-[60px]"
896
980
  icon={Eye}
897
981
  _icon={{
898
982
  size: 'xl',
899
983
  }}
900
- onPress={() => onViewLightbox(item.id)}
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": 150
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": 200
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: Repository.api.baseURL + Repository.name + '/uploadAttachment',
1105
+ url: Attachments.api.baseURL + Attachments.schema.name + '/uploadAttachment',
1001
1106
  method: 'POST',
1002
- headers: Repository.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: 'Minus',
1230
+ key: 'Trash',
1126
1231
  text: 'Delete Directory',
1127
1232
  handler: onDeleteDirectory,
1128
- icon: Minus,
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
- AttachmentDirectories = usesDirectories ? oneHatData.getRepository('AttachmentDirectories', true) : null; // put this here; otherwise a new unique repository will be created on every render!
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
  };