@onehat/ui 0.4.79 → 0.4.81

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.79",
3
+ "version": "0.4.81",
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
  }
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useEffect, } from 'react';
2
2
  import {
3
3
  Box,
4
+ HStack,
4
5
  ScrollView,
5
6
  Text,
6
7
  VStack,
@@ -61,6 +61,7 @@ import Plus from '../../Components/Icons/Plus.js';
61
61
  import Trash from '../../Components/Icons/Trash.js';
62
62
  import Edit from '../../Components/Icons/Edit.js';
63
63
  import Rotate from '../../Components/Icons/Rotate.js';
64
+ import Download from '../../Components/Icons/Download.js';
64
65
  import delay from '../../Functions/delay.js';
65
66
  import _ from 'lodash';
66
67
 
@@ -136,8 +137,6 @@ function DraggableFileMosaic(props) {
136
137
  ...fileMosaicProps
137
138
  } = props;
138
139
 
139
- console.log('DraggableFileMosaic render:', { isDragSource, dragSourceType, hasItem: !!dragSourceItem.item });
140
-
141
140
  // If not a drag source, just return the regular FileMosaic
142
141
  if (!isDragSource) {
143
142
  return <FileMosaic {...fileMosaicProps} />;
@@ -145,7 +144,6 @@ function DraggableFileMosaic(props) {
145
144
 
146
145
  // Create a completely separate draggable container
147
146
  const DragSourceContainer = withDragSource(({ dragSourceRef, ...dragProps }) => {
148
- console.log('DragSourceContainer render with props:', dragProps);
149
147
  return (
150
148
  <div
151
149
  ref={dragSourceRef}
@@ -240,6 +238,9 @@ function AttachmentsElement(props) {
240
238
  modelid = useRef(modelidCalc),
241
239
  id = props.id || (model && modelid.current ? `attachments-${model}-${modelid.current}` : 'attachments'),
242
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),
243
244
  [isReady, setIsReady] = useState(false),
244
245
  [isUploading, setIsUploading] = useState(false),
245
246
  [isLoading, setIsLoading] = useState(false),
@@ -272,8 +273,32 @@ function AttachmentsElement(props) {
272
273
  getFiles = () => {
273
274
  return setFilesRaw.current;
274
275
  },
275
- buildFiles = () => {
276
- const files = _.map(Attachments.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
+
277
302
  return {
278
303
  id: entity.id, // string | number The identifier of the file
279
304
  // file: null, // File The file object obtained from client drop or selection
@@ -284,20 +309,39 @@ function AttachmentsElement(props) {
284
309
  // errors: null, // string[] The list of errors according to the validation criteria or the result of the given custom validation function.
285
310
  // uploadStatus: null, // UPLOADSTATUS The current upload status. (e.g. "uploading").
286
311
  // uploadMessage: null, // string A message that shows the result of the upload process.
287
- 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.
288
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.
289
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.
290
315
  // extraUploadData: null, // Record<string, any> The additional data that will be sent to the server when files are uploaded individually
291
316
  // extraData: null, // Object Any kind of extra data that could be needed.
292
317
  // serverResponse: null, // ServerResponse The upload response from server.
293
318
  // xhr: null, // XMLHttpRequest A reference to the XHR object that allows the upload, progress and abort events.
319
+
294
320
  };
295
- });
321
+ }));
296
322
  setFiles(files);
323
+ setAreBlobUrlsReady(true);
297
324
  },
298
325
  clearFiles = () => {
326
+ cleanupIconBlobUrls();
299
327
  setFiles([]);
300
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
+ },
301
345
  onFileDelete = (id) => {
302
346
  const
303
347
  files = getFiles(),
@@ -410,22 +454,41 @@ function AttachmentsElement(props) {
410
454
  },
411
455
 
412
456
  // Lightbox
413
- buildModalBody = (id) => {
457
+ buildModalBody = async (item) => {
414
458
  const
415
- currentFile = Attachments.getById(id),
416
- currentIx = Attachments.getIxById(id),
459
+ currentFile = item,
460
+ currentIx = Attachments.getIxById(item.id),
417
461
  prevFile = Attachments.getByIx(currentIx - 1),
418
462
  nextFile = Attachments.getByIx(currentIx + 1),
419
463
  isPrevDisabled = !prevFile,
420
464
  isNextDisabled = !nextFile,
421
- onPrev = () => {
422
- updateModalBody(buildModalBody(prevFile.id));
465
+ onPrev = async () => {
466
+ cleanupModalBlobUrls();
467
+ const modalBody = await buildModalBody(prevFile);
468
+ updateModalBody(modalBody);
423
469
  },
424
- onNext = () => {
425
- updateModalBody(buildModalBody(nextFile.id));
470
+ onNext = async () => {
471
+ cleanupModalBlobUrls();
472
+ const modalBody = await buildModalBody(nextFile);
473
+ updateModalBody(modalBody);
426
474
  },
427
- url = currentFile.attachments__uri,
428
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);
487
+ }
488
+ } catch (error) {
489
+ console.warn('Failed to fetch authenticated file for modal:', error);
490
+ }
491
+
429
492
  let body = null;
430
493
  if (isPdf) {
431
494
  body = <iframe
@@ -455,18 +518,17 @@ function AttachmentsElement(props) {
455
518
  />
456
519
  </HStack>;
457
520
  },
458
- onViewLightbox = (id) => {
459
- if (!id) {
460
- alert('Cannot view lightbox until image is uploaded.');
461
- return;
462
- }
521
+ onViewLightbox = async (item) => {
522
+ cleanupModalBlobUrls();
523
+ const modalBody = await buildModalBody(item);
463
524
  showModal({
464
525
  title: 'Lightbox',
465
- body: buildModalBody(id),
526
+ body: modalBody,
466
527
  canClose: true,
467
528
  includeCancel: true,
468
529
  w: 1920,
469
530
  h: 1080,
531
+ onClose: cleanupModalBlobUrls,
470
532
  });
471
533
  },
472
534
 
@@ -692,7 +754,7 @@ function AttachmentsElement(props) {
692
754
  }
693
755
  if (usesDirectories) {
694
756
  const
695
- wasAlreadyLoaded = AttachmentDirectories.areRootNodesLoaded,
757
+ wasAlreadyLoaded = AttachmentDirectories.isLoaded,
696
758
  currentConditions = AttachmentDirectories.getParamConditions() || {},
697
759
  newConditions = {
698
760
  'conditions[AttachmentDirectories.model]': selectorSelected.repository.name,
@@ -715,7 +777,7 @@ function AttachmentsElement(props) {
715
777
  }
716
778
  }
717
779
 
718
- buildFiles();
780
+ await buildFiles();
719
781
  } else {
720
782
  Attachments.clear();
721
783
  if (usesDirectories) {
@@ -747,6 +809,8 @@ function AttachmentsElement(props) {
747
809
  AttachmentDirectories.off('beforeLoad', setDirectoriesTrue);
748
810
  AttachmentDirectories.off('loadRootNodes', setDirectoriesFalse);
749
811
  }
812
+ cleanupIconBlobUrls();
813
+ cleanupModalBlobUrls();
750
814
  };
751
815
  }, [model, modelid.current, showAll, getTreeSelection()]);
752
816
 
@@ -767,48 +831,52 @@ function AttachmentsElement(props) {
767
831
  let content = null;
768
832
  // icon or list view
769
833
  if (viewMode === ATTACHMENTS_VIEW_MODES__ICON || isUploading) {
770
- content = <VStack
771
- className={clsx(
772
- 'AttachmentsElement-icon-VStack1',
773
- 'h-full',
774
- 'flex-1',
775
- 'border',
776
- 'p-1',
777
- isLoading ? [
778
- 'border-t-4',
779
- 'border-t-[#f00]',
780
- ] : null,
781
- )}
782
- >
783
- <HStack
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
784
840
  className={clsx(
785
- 'AttachmentsElement-HStack',
786
- 'gap-2',
787
- 'flex-wrap',
788
- 'items-start',
789
- files.length === 0 ? [
790
- // So the 'No files' text is centered
791
- 'justify-center',
792
- 'items-center',
793
- 'h-full',
841
+ 'AttachmentsElement-icon-VStack1',
842
+ 'h-full',
843
+ 'flex-1',
844
+ 'border',
845
+ 'p-1',
846
+ isLoading ? [
847
+ 'border-t-4',
848
+ 'border-t-[#f00]',
794
849
  ] : null,
795
850
  )}
796
851
  >
797
- {files.length === 0 && <Text className="text-grey-600 italic">No files {usesDirectories ? 'in this directory' : ''}</Text>}
798
- {files.map((file) => {
799
- let eyeProps = {};
800
- if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
801
- eyeProps = {
802
- onSee: () => {
803
- onViewLightbox(file.id);
804
- },
805
- };
806
- }
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
+ }
807
877
 
808
- // Create drag source item for this file
809
- const
810
- fileEntity = Attachments.getById(file.id),
811
- dragSourceItem = {
878
+ // Create drag source item for this file
879
+ const dragSourceItem = {
812
880
  item: fileEntity, // Get the actual entity
813
881
  sourceComponentRef: null, // Could be set to a ref if needed
814
882
  getDragProxy: () => {
@@ -820,63 +888,64 @@ function AttachmentsElement(props) {
820
888
  }
821
889
  };
822
890
 
823
- return <Box
824
- key={file.id}
825
- className="BoxHERE mr-2"
826
- >
827
- {useFileMosaic &&
828
- <DraggableFileMosaic
829
- {...file}
830
- backgroundBlurImage={false}
831
- onDownload={onDownload}
832
- {..._fileMosaic}
833
- {...eyeProps}
834
- isDragSource={canCrud && usesDirectories}
835
- dragSourceType="Attachments"
836
- dragSourceItem={dragSourceItem}
837
- onDragStart={() => {
838
- setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
839
- }}
840
- onDragEnd={() => {
841
- setIsDragging(false);
842
- }}
843
- />}
844
- {!useFileMosaic &&
845
- <FileCardCustom
846
- {...file}
847
- backgroundBlurImage={false}
848
- {..._fileMosaic}
849
- {...eyeProps}
850
- isDragSource={canCrud && usesDirectories}
851
- dragSourceType="Attachments"
852
- dragSourceItem={dragSourceItem}
853
- item={Attachments.getById(file.id)}
854
- onDragStart={() => {
855
- setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
856
- }}
857
- onDragEnd={() => {
858
- setIsDragging(false);
859
- }}
860
- />}
861
- </Box>;
862
- })}
863
- </HStack>
864
- {Attachments.total <= collapsedMax ? null :
865
- <Button
866
- onPress={toggleShowAll}
867
- className="AttachmentsElement-toggleShowAll mt-2"
868
- text={'Show ' + (showAll ? ' Less' : ' All ' + Attachments.total)}
869
- _text={{
870
- className: `
871
- text-grey-600
872
- italic
873
- text-left
874
- w-full
875
- `,
876
- }}
877
- variant="outline"
878
- />}
879
- </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
+ }
880
949
  } else if (viewMode === ATTACHMENTS_VIEW_MODES__LIST) {
881
950
  content = <AttachmentsGridEditor
882
951
  Repository={Attachments}
@@ -912,32 +981,32 @@ function AttachmentsElement(props) {
912
981
  _icon={{
913
982
  size: 'xl',
914
983
  }}
915
- onPress={() => onViewLightbox(item.id)}
984
+ onPress={() => onViewLightbox(item)}
916
985
  tooltip="View"
917
986
  />;
918
987
  },
919
988
  },
920
- {
921
- id: 'download',
922
- header: 'Get',
923
- w: 60,
924
- isSortable: false,
925
- isEditable: false,
926
- isReorderable: false,
927
- isResizable: false,
928
- isHidable: false,
929
- renderer: (item) => {
930
- return <IconButton
931
- className="w-[60px]"
932
- icon={Download}
933
- _icon={{
934
- size: 'xl',
935
- }}
936
- onPress={() => onDownload(item.id)}
937
- tooltip="Download"
938
- />;
939
- },
940
- },
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
+ // },
941
1010
  {
942
1011
  "id": "attachments__filename",
943
1012
  "header": "Filename",
@@ -1024,7 +1093,7 @@ function AttachmentsElement(props) {
1024
1093
  </VStack>;
1025
1094
 
1026
1095
  // Always wrap content in dropzone when canCrud is true, but conditionally disable functionality
1027
- if (canCrud) {
1096
+ if (canCrud && !isDragging) {
1028
1097
  content = <Dropzone
1029
1098
  value={files}
1030
1099
  onChange={isDragging ? () => {} : onDropzoneChange} // Disable onChange when dragging