@onehat/ui 0.4.77 → 0.4.79

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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/Components/Editor/AttachmentDirectoriesEditor.js +51 -0
  3. package/src/Components/Editor/AttachmentsEditor.js +81 -0
  4. package/src/Components/Form/Field/Combo/AttachmentDirectoriesCombo.js +20 -0
  5. package/src/Components/Form/Field/Combo/AttachmentDirectoriesComboEditor.js +22 -0
  6. package/src/Components/Form/Field/Combo/AttachmentsCombo.js +20 -0
  7. package/src/Components/Form/Field/Combo/AttachmentsComboEditor.js +22 -0
  8. package/src/Components/Form/Field/Tag/AttachmentDirectoriesTag.js +22 -0
  9. package/src/Components/Form/Field/Tag/AttachmentDirectoriesTagEditor.js +22 -0
  10. package/src/Components/Form/Field/Tag/AttachmentsTag.js +22 -0
  11. package/src/Components/Form/Field/Tag/AttachmentsTagEditor.js +22 -0
  12. package/src/Components/Form/Form.js +17 -17
  13. package/src/Components/Grid/AttachmentDirectoriesFilteredGrid.js +17 -0
  14. package/src/Components/Grid/AttachmentDirectoriesFilteredGridEditor.js +17 -0
  15. package/src/Components/Grid/AttachmentDirectoriesFilteredInlineGridEditor.js +17 -0
  16. package/src/Components/Grid/AttachmentDirectoriesFilteredSideGridEditor.js +17 -0
  17. package/src/Components/Grid/AttachmentDirectoriesGrid.js +20 -0
  18. package/src/Components/Grid/AttachmentDirectoriesGridEditor.js +27 -0
  19. package/src/Components/Grid/AttachmentDirectoriesInlineGridEditor.js +25 -0
  20. package/src/Components/Grid/AttachmentDirectoriesSideGridEditor.js +24 -0
  21. package/src/Components/Grid/AttachmentsFilteredGrid.js +17 -0
  22. package/src/Components/Grid/AttachmentsFilteredGridEditor.js +17 -0
  23. package/src/Components/Grid/AttachmentsFilteredInlineGridEditor.js +17 -0
  24. package/src/Components/Grid/AttachmentsFilteredSideGridEditor.js +17 -0
  25. package/src/Components/Grid/AttachmentsGrid.js +20 -0
  26. package/src/Components/Grid/AttachmentsGridEditor.js +27 -0
  27. package/src/Components/Grid/AttachmentsInlineGridEditor.js +25 -0
  28. package/src/Components/Grid/AttachmentsSideGridEditor.js +24 -0
  29. package/src/Components/Grid/Columns/AttachmentDirectoriesGridColumns.js +32 -0
  30. package/src/Components/Grid/Columns/AttachmentsGridColumns.js +133 -0
  31. package/src/Components/Grid/Grid.js +194 -21
  32. package/src/Components/Grid/GridHeaderRow.js +10 -17
  33. package/src/Components/Grid/GridRow.js +49 -22
  34. package/src/Components/Grid/RowHandle.js +8 -6
  35. package/src/Components/Hoc/withEditor.js +18 -1
  36. package/src/Components/Hoc/withModal.js +4 -0
  37. package/src/Components/Hoc/withPdfButtons.js +1 -1
  38. package/src/Components/Hoc/withSelection.js +26 -4
  39. package/src/Components/Layout/AsyncOperation.js +299 -195
  40. package/src/Components/Messages/GlobalModals.js +1 -2
  41. package/src/Components/Panel/Panel.js +14 -2
  42. package/src/Components/Panel/TabPanel.js +1 -1
  43. package/src/Components/Panel/TreePanel.js +1 -1
  44. package/src/Components/Report/Report.js +106 -17
  45. package/src/Components/Toolbar/PaginationToolbar.js +4 -3
  46. package/src/Components/Toolbar/Toolbar.js +6 -3
  47. package/src/Components/Tree/Tree.js +219 -148
  48. package/src/Components/Tree/TreeNode.js +20 -13
  49. package/src/Components/Window/AttachmentDirectoriesEditorWindow.js +34 -0
  50. package/src/Components/Window/AttachmentsEditorWindow.js +34 -0
  51. package/src/Components/index.js +92 -1
  52. package/src/Constants/Attachments.js +2 -0
  53. package/src/Constants/Dates.js +2 -2
  54. package/src/Constants/Progress.js +5 -1
  55. package/src/Models/Schemas/AttachmentDirectories.js +66 -0
  56. package/src/Models/Schemas/Attachments.js +88 -0
  57. package/src/Models/Slices/SystemSlice.js +220 -0
  58. package/src/PlatformImports/Web/Attachments.js +855 -161
  59. package/src/Styles/Global.css +7 -2
@@ -7,22 +7,44 @@ import {
7
7
  Text,
8
8
  VStack,
9
9
  } from '@project-components/Gluestack';
10
- import Button from '../../Components/Buttons/Button';
11
10
  import {
12
11
  CURRENT_MODE,
13
12
  UI_MODE_WEB,
14
13
  UI_MODE_NATIVE,
15
14
  } from '../../Constants/UiModes.js';
15
+ import {
16
+ HORIZONTAL,
17
+ } from '../../Constants/Directions.js';
18
+ import {
19
+ SELECTION_MODE_MULTI,
20
+ } from '../../Constants/Selection.js';
16
21
  import UiGlobals from '../../UiGlobals.js';
17
22
  import {
18
23
  FILE_MODE_IMAGE,
19
24
  FILE_MODE_FILE,
20
25
  } from '../../Constants/File.js';
26
+ import clsx from 'clsx';
27
+ import oneHatData from '@onehat/data';
28
+ import * as yup from 'yup'; // https://github.com/jquense/yup#string
21
29
  import { Avatar, Dropzone, FileMosaic, FileCard, FileInputButton, } from "@files-ui/react";
30
+ import TreePanel from '../../Components/Panel/TreePanel.js';
31
+ import AttachmentsGridEditor from '../../Components/Grid/AttachmentsGridEditor.js';
32
+ import Form from '../../Components/Form/Form.js';
33
+ import {
34
+ EDITOR_TYPE__PLAIN,
35
+ } from '../../Constants/Editor.js';
36
+ import {
37
+ ATTACHMENTS_VIEW_MODES__ICON,
38
+ ATTACHMENTS_VIEW_MODES__LIST,
39
+ } from '../../Constants/Attachments.js';
22
40
  import inArray from '../../Functions/inArray.js';
41
+ import { withDragSource } from '../../Components/Hoc/withDnd.js';
42
+ import Button from '../../Components/Buttons/Button';
23
43
  import IconButton from '../../Components/Buttons/IconButton.js';
24
44
  import Xmark from '../../Components/Icons/Xmark.js';
25
45
  import Eye from '../../Components/Icons/Eye.js';
46
+ import Images from '../../Components/Icons/Images.js';
47
+ import List from '../../Components/Icons/List.js';
26
48
  import ChevronLeft from '../../Components/Icons/ChevronLeft.js';
27
49
  import ChevronRight from '../../Components/Icons/ChevronRight.js';
28
50
  import withAlert from '../../Components/Hoc/withAlert.js';
@@ -32,6 +54,14 @@ import CenterBox from '../../Components/Layout/CenterBox.js';
32
54
  import downloadInBackground from '../../Functions/downloadInBackground.js';
33
55
  import downloadWithFetch from '../../Functions/downloadWithFetch.js';
34
56
  import useForceUpdate from '../../Hooks/useForceUpdate.js';
57
+ import getSaved from '../../Functions/getSaved.js';
58
+ import setSaved from '../../Functions/setSaved.js';
59
+ import Folder from '../../Components/Icons/Folder.js';
60
+ import Plus from '../../Components/Icons/Plus.js';
61
+ import Trash from '../../Components/Icons/Trash.js';
62
+ import Edit from '../../Components/Icons/Edit.js';
63
+ import Rotate from '../../Components/Icons/Rotate.js';
64
+ import delay from '../../Functions/delay.js';
35
65
  import _ from 'lodash';
36
66
 
37
67
  const
@@ -48,21 +78,110 @@ function FileCardCustom(props) {
48
78
  onSee,
49
79
  downloadUrl,
50
80
  uploadStatus,
81
+ // Drag props
82
+ isDragSource = false,
83
+ dragSourceType = 'Attachments',
84
+ dragSourceItem = {},
85
+ item, // The actual attachment entity
51
86
  } = props,
52
87
  isDownloading = uploadStatus && inArray(uploadStatus, ['preparing', 'uploading', 'success']),
53
88
  isPdf = mimetype === 'application/pdf';
54
89
 
55
- return <Pressable
56
- onPress={() => {
57
- downloadInBackground(downloadUrl);
90
+ let cardContent =
91
+ <Pressable
92
+ onPress={() => {
93
+ downloadInBackground(downloadUrl);
94
+ }}
95
+ className="Pressable px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
96
+ >
97
+ {isDownloading &&
98
+ <Spinner className="mr-2" />}
99
+ {onSee && isPdf &&
100
+ <IconButton
101
+ className="mr-1"
102
+ icon={Eye}
103
+ onPress={() => onSee(id)}
104
+ />}
105
+ <Text>{filename}</Text>
106
+ {onDelete &&
107
+ <IconButton
108
+ className="ml-1"
109
+ icon={Xmark}
110
+ onPress={() => onDelete(id)}
111
+ />}
112
+ </Pressable>;
113
+
114
+ // Wrap with drag source if needed
115
+ if (isDragSource) {
116
+ const DragSourceFileCard = withDragSource(({ children, ...dragProps }) => children);
117
+ return <DragSourceFileCard
118
+ isDragSource={isDragSource}
119
+ dragSourceType={dragSourceType}
120
+ dragSourceItem={dragSourceItem}
121
+ >
122
+ {cardContent}
123
+ </DragSourceFileCard>;
124
+ }
125
+
126
+ return cardContent;
127
+ }
128
+
129
+ function DraggableFileMosaic(props) {
130
+ const {
131
+ isDragSource = false,
132
+ dragSourceType = 'Attachments',
133
+ dragSourceItem = {},
134
+ onDragStart,
135
+ onDragEnd,
136
+ ...fileMosaicProps
137
+ } = props;
138
+
139
+ console.log('DraggableFileMosaic render:', { isDragSource, dragSourceType, hasItem: !!dragSourceItem.item });
140
+
141
+ // If not a drag source, just return the regular FileMosaic
142
+ if (!isDragSource) {
143
+ return <FileMosaic {...fileMosaicProps} />;
144
+ }
145
+
146
+ // Create a completely separate draggable container
147
+ const DragSourceContainer = withDragSource(({ dragSourceRef, ...dragProps }) => {
148
+ console.log('DragSourceContainer render with props:', dragProps);
149
+ return (
150
+ <div
151
+ ref={dragSourceRef}
152
+ style={{
153
+ display: 'inline-block',
154
+ cursor: 'grab'
58
155
  }}
59
- className="px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
60
156
  >
61
- {isDownloading && <Spinner className="mr-2" />}
62
- {onSee && isPdf && <IconButton mr={1} icon={Eye} onPress={() => onSee(null, id)} />}
63
- <Text>{filename}</Text>
64
- {onDelete && <IconButton ml={1} icon={Xmark} onPress={() => onDelete(id)} />}
65
- </Pressable>;
157
+ <FileMosaic
158
+ {...fileMosaicProps}
159
+ // Disable any built-in drag functionality of FileMosaic
160
+ draggable={false}
161
+ />
162
+ </div>
163
+ );
164
+ });
165
+
166
+ // Add drag handlers to the dragSourceItem
167
+ const enhancedDragSourceItem = {
168
+ ...dragSourceItem,
169
+ onDragStart: () => {
170
+ if (dragSourceItem.onDragStart) {
171
+ dragSourceItem.onDragStart();
172
+ }
173
+ if (onDragStart) {
174
+ onDragStart();
175
+ }
176
+ }
177
+ };
178
+
179
+ return <DragSourceContainer
180
+ isDragSource={true}
181
+ dragSourceType={dragSourceType}
182
+ dragSourceItem={enhancedDragSourceItem}
183
+ onDragEnd={onDragEnd}
184
+ />;
66
185
  }
67
186
 
68
187
 
@@ -80,6 +199,10 @@ function AttachmentsElement(props) {
80
199
  _dropZone = {},
81
200
  _fileMosaic = {},
82
201
  useFileMosaic = true,
202
+ usesDirectories = false,
203
+ isDirectoriesByModel = true, // if false, directories are by modelid
204
+ AttachmentDirectories,
205
+ initialViewMode = ATTACHMENTS_VIEW_MODES__ICON,
83
206
  accept, // 'image/*'
84
207
  maxFiles = null,
85
208
  disabled = false,
@@ -101,10 +224,11 @@ function AttachmentsElement(props) {
101
224
  selectorSelectedField = 'id',
102
225
 
103
226
  // withData
104
- Repository,
227
+ Repository: Attachments,
105
228
 
106
229
  // withAlert
107
230
  showModal,
231
+ hideModal,
108
232
  updateModalBody,
109
233
  alert,
110
234
  confirm,
@@ -114,10 +238,32 @@ function AttachmentsElement(props) {
114
238
  model = _.isArray(selectorSelected) && selectorSelected[0] ? selectorSelected[0].repository?.name : selectorSelected?.repository?.name,
115
239
  modelidCalc = _.isArray(selectorSelected) ? _.map(selectorSelected, (entity) => entity[selectorSelectedField]) : selectorSelected?.[selectorSelectedField],
116
240
  modelid = useRef(modelidCalc),
241
+ id = props.id || (model && modelid.current ? `attachments-${model}-${modelid.current}` : 'attachments'),
117
242
  forceUpdate = useForceUpdate(),
118
243
  [isReady, setIsReady] = useState(false),
119
244
  [isUploading, setIsUploading] = useState(false),
245
+ [isLoading, setIsLoading] = useState(false),
246
+ [isDirectoriesLoading, setIsDirectoriesLoading] = useState(false),
247
+ [viewMode, setViewModeRaw] = useState(initialViewMode),
248
+ setViewMode = (newViewMode) => {
249
+ setViewModeRaw(newViewMode);
250
+ if (id) {
251
+ setSaved(id + '-viewMode', newViewMode);
252
+ }
253
+ },
120
254
  [showAll, setShowAll] = useState(false),
255
+ [isDragging, setIsDragging] = useState(false),
256
+ treeSelectionRaw = useRef([]),
257
+ setTreeSelection = (selection) => {
258
+ treeSelectionRaw.current = selection;
259
+ forceUpdate();
260
+ },
261
+ getTreeSelection = () => {
262
+ return treeSelectionRaw.current;
263
+ },
264
+ treeSelection = getTreeSelection(),
265
+
266
+ // icon view only
121
267
  setFilesRaw = useRef([]),
122
268
  setFiles = (files) => {
123
269
  setFilesRaw.current = files;
@@ -127,7 +273,7 @@ function AttachmentsElement(props) {
127
273
  return setFilesRaw.current;
128
274
  },
129
275
  buildFiles = () => {
130
- const files = _.map(Repository.entities, (entity) => {
276
+ const files = _.map(Attachments.entities, (entity) => {
131
277
  return {
132
278
  id: entity.id, // string | number The identifier of the file
133
279
  // file: null, // File The file object obtained from client drop or selection
@@ -152,14 +298,66 @@ function AttachmentsElement(props) {
152
298
  clearFiles = () => {
153
299
  setFiles([]);
154
300
  },
301
+ onFileDelete = (id) => {
302
+ const
303
+ files = getFiles(),
304
+ file = _.find(files, { id });
305
+ if (confirmBeforeDelete) {
306
+ confirm('Are you sure you want to delete the file "' + file.name + '"?', () => doDelete(id));
307
+ } else {
308
+ doDelete(id);
309
+ }
310
+ },
155
311
  toggleShowAll = () => {
156
312
  setShowAll(!showAll);
157
313
  },
314
+ doDelete = (id) => {
315
+ const
316
+ files = getFiles(),
317
+ file = Attachments.getById(id);
318
+ if (file) {
319
+ // if the file exists in the repository, delete it there
320
+ Attachments.deleteById(id);
321
+ Attachments.save();
322
+
323
+ } else {
324
+ // simply remove it from the files array
325
+ const newFiles = [];
326
+ _.each(files, (file) => {
327
+ if (file.id !== id) {
328
+ newFiles.push(file);
329
+ }
330
+ });
331
+ setFiles(newFiles);
332
+ }
333
+ if (onDelete) {
334
+ onDelete(id);
335
+ }
336
+ },
337
+ onDownload = (id, url) => {
338
+ if (isPwa) {
339
+ // This doesn't work because iOS doesn't allow you to open another window within a PWA.
340
+ // downloadWithFetch(url);
341
+
342
+ alert('Files cannot be downloaded and viewed within an iOS PWA. Please use the Safari browser instead.');
343
+ } else {
344
+ downloadInBackground(url);
345
+ }
346
+ },
347
+
348
+ // dropzone
158
349
  onDropzoneChange = async (files) => {
159
350
  if (!files.length) {
160
351
  alert('No files accepted. Perhaps they were too large or the wrong file type?');
161
352
  return;
162
353
  }
354
+ if (usesDirectories) {
355
+ const treeSelection = getTreeSelection();
356
+ if (!treeSelection[0] || !treeSelection[0].id) {
357
+ alert('Please select a directory to upload the files to.');
358
+ return;
359
+ }
360
+ }
163
361
  setFiles(files);
164
362
  _.each(files, (file) => {
165
363
  file.extraUploadData = {
@@ -167,6 +365,9 @@ function AttachmentsElement(props) {
167
365
  modelid: modelid.current,
168
366
  ...extraUploadData,
169
367
  };
368
+ if (usesDirectories) {
369
+ file.extraUploadData.attachment_directory_id = treeSelection[0].id;
370
+ }
170
371
  });
171
372
  if (onAfterDropzoneChange) {
172
373
  const isChanged = await onAfterDropzoneChange(files);
@@ -200,89 +401,32 @@ function AttachmentsElement(props) {
200
401
  });
201
402
  if (!isError) {
202
403
  setIsUploading(false);
203
- Repository.reload();
404
+ Attachments.reload();
204
405
  if (onUpload) {
205
406
  onUpload(files);
206
407
  }
207
408
  }
208
409
  }
209
410
  },
210
- onFileDelete = (id) => {
211
- const
212
- files = getFiles(),
213
- file = _.find(files, { id });
214
- if (confirmBeforeDelete) {
215
- confirm('Are you sure you want to delete the file "' + file.name + '"?', () => doDelete(id));
216
- } else {
217
- doDelete(id);
218
- }
219
- },
220
- onDownload = (id, url) => {
221
- if (isPwa) {
222
- // This doesn't work because iOS doesn't allow you to open another window within a PWA.
223
- // downloadWithFetch(url);
224
-
225
- alert('Files cannot be downloaded and viewed within an iOS PWA. Please use the Safari browser instead.');
226
- } else {
227
- downloadInBackground(url);
228
- }
229
- },
230
- buildModalBody = (url, id) => {
231
- const files = getFiles();
232
- // This method was abstracted out so showModal/onPrev/onNext can all use it.
233
- // url comes from FileMosaic, which passes in imageUrl,
234
- // whereas FileCardCustom passes in id.
235
-
236
- function findFile(url, id) {
237
- if (id) {
238
- return _.find(files, { id });
239
- }
240
- return _.find(files, (file) => file.imageUrl === url);
241
- }
242
- function findPrevFile(url, id) {
243
- const
244
- currentFile = findFile(url, id),
245
- currentIx = _.findIndex(files, currentFile);
246
- if (currentIx > 0) {
247
- return files[currentIx - 1];
248
- }
249
- return null;
250
- }
251
- function findNextFile(url, id) {
252
- const
253
- currentFile = findFile(url, id),
254
- currentIx = _.findIndex(files, currentFile);
255
- if (currentIx < files.length - 1) {
256
- return files[currentIx + 1];
257
- }
258
- return null;
259
- }
260
411
 
412
+ // Lightbox
413
+ buildModalBody = (id) => {
261
414
  const
262
- prevFile = findPrevFile(url, id),
415
+ currentFile = Attachments.getById(id),
416
+ currentIx = Attachments.getIxById(id),
417
+ prevFile = Attachments.getByIx(currentIx - 1),
418
+ nextFile = Attachments.getByIx(currentIx + 1),
263
419
  isPrevDisabled = !prevFile,
264
- nextFile = findNextFile(url, id),
265
420
  isNextDisabled = !nextFile,
266
421
  onPrev = () => {
267
- const { imageUrl, id } = prevFile;
268
- updateModalBody(buildModalBody(imageUrl, id));
422
+ updateModalBody(buildModalBody(prevFile.id));
269
423
  },
270
424
  onNext = () => {
271
- const { imageUrl, id } = nextFile;
272
- updateModalBody(buildModalBody(imageUrl, id));
273
- };
274
-
275
- let isPdf = false,
276
- body = null;
277
-
278
- if (id) {
279
- const file = _.find(files, { id });
280
- url = file.imageUrl;
281
- isPdf = true;
282
- } else if (url?.match(/\.pdf$/)) {
283
- isPdf = true;
284
- }
285
-
425
+ updateModalBody(buildModalBody(nextFile.id));
426
+ },
427
+ url = currentFile.attachments__uri,
428
+ isPdf = currentFile.attachments__mimetype === 'application/pdf';
429
+ let body = null;
286
430
  if (isPdf) {
287
431
  body = <iframe
288
432
  src={url}
@@ -311,41 +455,168 @@ function AttachmentsElement(props) {
311
455
  />
312
456
  </HStack>;
313
457
  },
314
- onViewLightbox = (url, id) => {
315
- if (!url && !id) {
458
+ onViewLightbox = (id) => {
459
+ if (!id) {
316
460
  alert('Cannot view lightbox until image is uploaded.');
317
461
  return;
318
462
  }
319
463
  showModal({
320
464
  title: 'Lightbox',
321
- body: buildModalBody(url, id),
465
+ body: buildModalBody(id),
322
466
  canClose: true,
323
467
  includeCancel: true,
324
468
  w: 1920,
325
469
  h: 1080,
326
470
  });
327
471
  },
328
- doDelete = (id) => {
329
- const
330
- files = getFiles(),
331
- file = Repository.getById(id);
332
- if (file) {
333
- // if the file exists in the repository, delete it there
334
- Repository.deleteById(id);
335
- Repository.save();
336
472
 
337
- } else {
338
- // simply remove it from the files array
339
- const newFiles = [];
340
- _.each(files, (file) => {
341
- if (file.id !== id) {
342
- newFiles.push(file);
343
- }
344
- });
345
- setFiles(newFiles);
473
+ // AttachmentDirectories
474
+ onCreateDirectory = () => {
475
+ const treeSelection = getTreeSelection();
476
+ showModal({
477
+ title: 'New Directory',
478
+ w: 400,
479
+ h: 200,
480
+ canClose: true,
481
+ includeReset: false,
482
+ includeCancel: false,
483
+ body: <Form
484
+ editorType={EDITOR_TYPE__PLAIN}
485
+ items={[
486
+ {
487
+ type: 'Input',
488
+ name: 'directoryName',
489
+ placeholder: 'New Directory Name',
490
+ }
491
+ ]}
492
+ additionalFooterButtons={[
493
+ {
494
+ text: 'Cancel',
495
+ onPress: hideModal,
496
+ skipSubmit: true,
497
+ variant: 'outline',
498
+ }
499
+ ]}
500
+ validator={yup.object({
501
+ directoryName: yup.string().required(),
502
+ })}
503
+ onSave={async (values)=> {
504
+ const { directoryName } = values;
505
+ await AttachmentDirectories.add({
506
+ name: directoryName,
507
+ model: selectorSelected.repository.name,
508
+ modelid: selectorSelected[selectorSelectedField],
509
+ parentId: treeSelection?.[0]?.id || null,
510
+ });
511
+ hideModal();
512
+ }}
513
+ />,
514
+ });
515
+ },
516
+ onDeleteDirectory = async () => {
517
+
518
+ const
519
+ attachmentDirectory = getTreeSelection()[0],
520
+ isRoot = attachmentDirectory.isRoot;
521
+ if (isRoot) {
522
+ alert('Cannot delete the root directory.');
523
+ return;
346
524
  }
347
- if (onDelete) {
348
- onDelete(id);
525
+
526
+
527
+ // check if there are any attachments in this directory or its subdirectories
528
+ const
529
+ url = AttachmentDirectories.api.baseURL + 'AttachmentDirectories/hasAttachments',
530
+ data = {
531
+ attachment_directory_id: treeSelection[0].id,
532
+ },
533
+ result = await AttachmentDirectories._send('POST', url, data);
534
+
535
+ const {
536
+ root,
537
+ success,
538
+ total,
539
+ message
540
+ } = AttachmentDirectories._processServerResponse(result);
541
+
542
+ if (!success) {
543
+ alert(message);
544
+ return;
545
+ }
546
+
547
+ if (root.hasAttachments) {
548
+ alert('Cannot delete a directory that contains attachments somewhere down its hierarchy. Please move or delete the attachments first.');
549
+ return;
550
+ }
551
+
552
+
553
+ // transfer selection to the parent node
554
+ const
555
+ parentNode = attachmentDirectory.getParent(),
556
+ newSelection = [parentNode];
557
+ setTreeSelection(newSelection);
558
+ self.children.tree.setSelection(newSelection);
559
+
560
+
561
+ // now delete it
562
+ await attachmentDirectory.delete();
563
+ self.children.tree.buildAndSetTreeNodeData();
564
+
565
+ },
566
+ onRenameDirectory = () => {
567
+ const attachmentDirectory = getTreeSelection()[0];
568
+ showModal({
569
+ title: 'Rename Directory',
570
+ w: 400,
571
+ h: 200,
572
+ canClose: true,
573
+ includeReset: false,
574
+ includeCancel: false,
575
+ body: <Form
576
+ editorType={EDITOR_TYPE__PLAIN}
577
+ items={[
578
+ {
579
+ type: 'Input',
580
+ name: 'directoryName',
581
+ placeholder: 'New Directory Name',
582
+ }
583
+ ]}
584
+ additionalFooterButtons={[
585
+ {
586
+ text: 'Cancel',
587
+ onPress: hideModal,
588
+ skipSubmit: true,
589
+ variant: 'outline',
590
+ }
591
+ ]}
592
+ startingValues={{
593
+ directoryName: attachmentDirectory.attachment_directories__name,
594
+ }}
595
+ validator={yup.object({
596
+ directoryName: yup.string().required(),
597
+ })}
598
+ onSave={async (values)=> {
599
+ const {
600
+ directoryName,
601
+ } = values;
602
+ attachmentDirectory.attachment_directories__name = directoryName;
603
+ await delay(500);
604
+ await attachmentDirectory.save();
605
+ await delay(500);
606
+ self.children.tree.buildAndSetTreeNodeData();
607
+ hideModal();
608
+
609
+
610
+ }}
611
+ />,
612
+ });
613
+ },
614
+ onReloadDirectories = async () => {
615
+ await AttachmentDirectories.loadRootNodes(2);
616
+ const rootNodes = AttachmentDirectories.getRootNodes();
617
+ if (rootNodes) {
618
+ setTreeSelection(rootNodes);
619
+ self.children.tree.setSelection(rootNodes);
349
620
  }
350
621
  };
351
622
 
@@ -359,62 +630,125 @@ function AttachmentsElement(props) {
359
630
  return () => {};
360
631
  }
361
632
 
362
- (async () => {
633
+ const
634
+ setTrue = () => setIsLoading(true),
635
+ setFalse = () => setIsLoading(false),
636
+ setDirectoriesTrue = () => setIsDirectoriesLoading(true),
637
+ setDirectoriesFalse = () => setIsDirectoriesLoading(false);
363
638
 
364
- if (!_.isArray(modelid.current)) {
639
+ Attachments.on('beforeLoad', setTrue);
640
+ Attachments.on('load', setFalse);
641
+ Attachments.on('load', buildFiles);
642
+ if (usesDirectories) {
643
+ AttachmentDirectories.on('beforeLoad', setDirectoriesTrue);
644
+ AttachmentDirectories.on('loadRootNodes', setDirectoriesFalse);
645
+ }
365
646
 
366
- // Load Repository
367
- const filters = [
368
- {
369
- name: 'model',
370
- value: model,
371
- },
372
- {
373
- name: 'modelid',
374
- value: modelid.current,
647
+ (async () => {
648
+
649
+ if (modelid.current && !_.isArray(modelid.current)) {
650
+ const
651
+ currentConditions = Attachments.getParamConditions() || {},
652
+ newConditions = {
653
+ 'conditions[Attachments.model]': model,
654
+ 'conditions[Attachments.modelid]': modelid.current,
375
655
  },
376
- ];
656
+ currentPageSize = Attachments.pageSize,
657
+ newPageSize = showAll ? expandedMax : collapsedMax;
658
+
659
+ // figure out conditions
377
660
  if (accept) {
378
- let name,
661
+ let name = 'mimetype IN',
379
662
  mimetypes;
380
663
  if (_.isString(accept)) {
381
664
  if (accept.match(/,/)) {
382
- name = 'mimetype IN';
383
665
  mimetypes = accept.split(',');
384
666
  } else {
385
667
  name = 'mimetype LIKE';
386
668
  mimetypes = accept.replace('*', '%');
387
669
  }
388
670
  } else if (_.isArray(accept)) {
389
- name = 'mimetype IN';
390
671
  mimetypes = accept;
391
672
  }
392
- filters.push({
393
- name,
394
- value: mimetypes,
395
- });
673
+ newConditions['conditions[Attachments.' + name + ']'] = mimetypes;
674
+ }
675
+ if (usesDirectories) {
676
+ const treeSelection = getTreeSelection();
677
+ newConditions['conditions[Attachments.attachment_directory_id]'] = treeSelection[0]?.id || null;
678
+ }
679
+ let doReload = false;
680
+ if (!_.isEqual(currentConditions, newConditions)) {
681
+ Attachments.setParams(newConditions);
682
+ doReload = true;
683
+ }
684
+
685
+ // figure out pageSize
686
+ if (!_.isEqual(currentPageSize, newPageSize)) {
687
+ Attachments.setPageSize(newPageSize);
688
+ doReload = true;
689
+ }
690
+ if (doReload) {
691
+ await Attachments.load();
692
+ }
693
+ if (usesDirectories) {
694
+ const
695
+ wasAlreadyLoaded = AttachmentDirectories.areRootNodesLoaded,
696
+ currentConditions = AttachmentDirectories.getParamConditions() || {},
697
+ newConditions = {
698
+ 'conditions[AttachmentDirectories.model]': selectorSelected.repository.name,
699
+ 'conditions[AttachmentDirectories.modelid]': selectorSelected[selectorSelectedField],
700
+ };
701
+ let doReload = false;
702
+ if (!_.isEqual(currentConditions, newConditions)) {
703
+ AttachmentDirectories.setParams(newConditions);
704
+ doReload = true;
705
+ }
706
+ if (doReload) {
707
+ // setTreeSelection([]); // clear it; otherwise we get stale nodes after reloading AttachmentDirectories
708
+ await AttachmentDirectories.loadRootNodes(2);
709
+ if (wasAlreadyLoaded) {
710
+ const rootNodes = AttachmentDirectories.getRootNodes();
711
+ if (rootNodes) {
712
+ self.children.tree.setSelection(rootNodes);
713
+ }
714
+ }
715
+ }
396
716
  }
397
- Repository.filter(filters);
398
- Repository.setPageSize(showAll ? expandedMax : collapsedMax);
399
- await Repository.load();
400
717
 
401
718
  buildFiles();
402
719
  } else {
720
+ Attachments.clear();
721
+ if (usesDirectories) {
722
+ AttachmentDirectories.clear();
723
+ }
403
724
  clearFiles();
404
725
  }
405
726
 
406
727
 
728
+ // Load saved view mode preference before setting ready
729
+ if (id && !isReady) {
730
+ const savedViewMode = await getSaved(id + '-viewMode');
731
+ if (!_.isNil(savedViewMode)) {
732
+ setViewModeRaw(savedViewMode);
733
+ }
734
+ }
735
+
407
736
  if (!isReady) {
408
737
  setIsReady(true);
409
738
  }
410
739
 
411
740
  })();
412
741
 
413
- Repository.on('load', buildFiles);
414
742
  return () => {
415
- Repository.off('load', buildFiles);
743
+ Attachments.off('beforeLoad', setTrue);
744
+ Attachments.off('load', setFalse);
745
+ Attachments.off('load', buildFiles);
746
+ if (usesDirectories) {
747
+ AttachmentDirectories.off('beforeLoad', setDirectoriesTrue);
748
+ AttachmentDirectories.off('loadRootNodes', setDirectoriesFalse);
749
+ }
416
750
  };
417
- }, [model, modelid.current, showAll]);
751
+ }, [model, modelid.current, showAll, getTreeSelection()]);
418
752
 
419
753
  if (!isReady) {
420
754
  return null;
@@ -429,54 +763,109 @@ function AttachmentsElement(props) {
429
763
  if (canCrud) {
430
764
  _fileMosaic.onDelete = onFileDelete;
431
765
  }
432
- let className = `
433
- AttachmentsElement
434
- w-full
435
- h-full
436
- p-1
437
- rounded-[5px]
438
- `;
439
- if (props.className) {
440
- className += ' ' + props.className;
441
- }
442
766
  const files = getFiles();
443
- let content = <VStack className={className}>
444
- <HStack className="AttachmentsElement-HStack flex-wrap">
445
- {files.length === 0 && <Text className="text-grey-600 italic">No files</Text>}
767
+ let content = null;
768
+ // icon or list view
769
+ 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
784
+ 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',
794
+ ] : null,
795
+ )}
796
+ >
797
+ {files.length === 0 && <Text className="text-grey-600 italic">No files {usesDirectories ? 'in this directory' : ''}</Text>}
446
798
  {files.map((file) => {
447
- let seeProps = {};
799
+ let eyeProps = {};
448
800
  if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
449
- seeProps = {
450
- onSee: onViewLightbox,
801
+ eyeProps = {
802
+ onSee: () => {
803
+ onViewLightbox(file.id);
804
+ },
451
805
  };
452
806
  }
807
+
808
+ // Create drag source item for this file
809
+ const
810
+ fileEntity = Attachments.getById(file.id),
811
+ dragSourceItem = {
812
+ item: fileEntity, // Get the actual entity
813
+ sourceComponentRef: null, // Could be set to a ref if needed
814
+ getDragProxy: () => {
815
+ // Custom drag preview for file items
816
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
817
+ <Text className="font-semibold text-gray-800">{file.name}</Text>
818
+ <Text className="text-sm text-gray-600">File</Text>
819
+ </VStack>;
820
+ }
821
+ };
822
+
453
823
  return <Box
454
824
  key={file.id}
455
- className="mr-2"
825
+ className="BoxHERE mr-2"
456
826
  >
457
827
  {useFileMosaic &&
458
- <FileMosaic
828
+ <DraggableFileMosaic
459
829
  {...file}
460
830
  backgroundBlurImage={false}
461
831
  onDownload={onDownload}
462
832
  {..._fileMosaic}
463
- {...seeProps}
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
+ }}
464
843
  />}
465
844
  {!useFileMosaic &&
466
845
  <FileCardCustom
467
846
  {...file}
468
847
  backgroundBlurImage={false}
469
848
  {..._fileMosaic}
470
- {...seeProps}
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
+ }}
471
860
  />}
472
861
  </Box>;
473
862
  })}
474
863
  </HStack>
475
- {Repository.total <= collapsedMax ? null :
864
+ {Attachments.total <= collapsedMax ? null :
476
865
  <Button
477
866
  onPress={toggleShowAll}
478
867
  className="AttachmentsElement-toggleShowAll mt-2"
479
- text={'Show ' + (showAll ? ' Less' : ' All ' + Repository.total)}
868
+ text={'Show ' + (showAll ? ' Less' : ' All ' + Attachments.total)}
480
869
  _text={{
481
870
  className: `
482
871
  text-grey-600
@@ -488,48 +877,353 @@ function AttachmentsElement(props) {
488
877
  variant="outline"
489
878
  />}
490
879
  </VStack>;
880
+ } else if (viewMode === ATTACHMENTS_VIEW_MODES__LIST) {
881
+ content = <AttachmentsGridEditor
882
+ Repository={Attachments}
883
+ selectionMode={SELECTION_MODE_MULTI}
884
+ showSelectHandle={false}
885
+ disableAdd={true}
886
+ disableEdit={true}
887
+ disableView={true}
888
+ disableCopy={true}
889
+ disableDuplicate={true}
890
+ disableDelete={!canCrud}
891
+ className="flex-1 h-full" // Ensure it takes up full space
892
+ onDragStart={() => {
893
+ setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
894
+ }}
895
+ onDragEnd={() => {
896
+ setIsDragging(false);
897
+ }}
898
+ columnsConfig={[
899
+ {
900
+ id: 'view',
901
+ header: 'View',
902
+ w: 60,
903
+ isSortable: false,
904
+ isEditable: false,
905
+ isReorderable: false,
906
+ isResizable: false,
907
+ isHidable: false,
908
+ renderer: (item) => {
909
+ return <IconButton
910
+ className="w-[60px]"
911
+ icon={Eye}
912
+ _icon={{
913
+ size: 'xl',
914
+ }}
915
+ onPress={() => onViewLightbox(item.id)}
916
+ tooltip="View"
917
+ />;
918
+ },
919
+ },
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
+ },
941
+ {
942
+ "id": "attachments__filename",
943
+ "header": "Filename",
944
+ "fieldName": "attachments__filename",
945
+ "isSortable": true,
946
+ "isEditable": true,
947
+ "isReorderable": true,
948
+ "isResizable": true,
949
+ "w": 250
950
+ },
951
+ {
952
+ "id": "attachments__size_formatted",
953
+ "header": "Size Formatted",
954
+ "fieldName": "attachments__size_formatted",
955
+ "isSortable": false,
956
+ "isEditable": false,
957
+ "isReorderable": true,
958
+ "isResizable": true,
959
+ "w": 100
960
+ },
961
+ ]}
962
+ areRowsDragSource={canCrud}
963
+ rowDragSourceType="Attachments"
964
+ getCustomDragProxy={(item, selection) => {
965
+ let selectionCount = selection?.length || 1,
966
+ displayText = item.attachments__filename || 'Selected TreeNode';
967
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
968
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
969
+ {selectionCount > 1 &&
970
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
971
+ }
972
+ </VStack>;
973
+ }}
974
+
975
+ />;
976
+ }
977
+
978
+ // switches for icon/list view
979
+ content = <VStack
980
+ className={clsx(
981
+ 'w-full',
982
+ 'h-full',
983
+ )}
984
+ >
985
+ <HStack
986
+ className={clsx(
987
+ 'h-[30px]',
988
+ 'w-full',
989
+ 'gap-1',
990
+ 'p-1',
991
+ 'justify-start',
992
+ 'items-center',
993
+ 'bg-primary-500',
994
+ )}
995
+ >
996
+ <IconButton
997
+ onPress={() => setViewMode(ATTACHMENTS_VIEW_MODES__ICON)}
998
+ icon={Images}
999
+ className={clsx(
1000
+ viewMode === ATTACHMENTS_VIEW_MODES__ICON ? 'bg-gray-400' : null,
1001
+ 'w-[25px]',
1002
+ 'h-[25px]',
1003
+ 'px-[2px]',
1004
+ 'py-[2px]',
1005
+ )}
1006
+ tooltip="Icon View"
1007
+ />
1008
+ <IconButton
1009
+ onPress={() => setViewMode(ATTACHMENTS_VIEW_MODES__LIST)}
1010
+ icon={List}
1011
+ className={clsx(
1012
+ viewMode === ATTACHMENTS_VIEW_MODES__LIST ? 'bg-gray-400' : null,
1013
+ 'w-[25px]',
1014
+ 'h-[25px]',
1015
+ 'px-[2px]',
1016
+ 'py-[2px]',
1017
+ )}
1018
+ tooltip="List View"
1019
+ />
1020
+ </HStack>
1021
+
1022
+ {content}
1023
+
1024
+ </VStack>;
491
1025
 
1026
+ // Always wrap content in dropzone when canCrud is true, but conditionally disable functionality
492
1027
  if (canCrud) {
493
1028
  content = <Dropzone
494
1029
  value={files}
495
- onChange={onDropzoneChange}
496
- accept={accept}
497
- maxFiles={maxFiles}
1030
+ onChange={isDragging ? () => {} : onDropzoneChange} // Disable onChange when dragging
1031
+ accept={isDragging ? undefined : accept} // Remove accept types when dragging
1032
+ maxFiles={isDragging ? 0 : maxFiles} // Set to 0 when dragging to prevent drops
498
1033
  maxFileSize={styles.ATTACHMENTS_MAX_FILESIZE}
499
1034
  autoClean={true}
500
1035
  uploadConfig={{
501
- url: Repository.api.baseURL + Repository.name + '/uploadAttachment',
1036
+ url: Attachments.api.baseURL + Attachments.schema.name + '/uploadAttachment',
502
1037
  method: 'POST',
503
- headers: Repository.headers,
1038
+ headers: Attachments.headers,
504
1039
  autoUpload,
505
1040
  }}
506
1041
  headerConfig={{
1042
+ className: '!hidden',
507
1043
  deleteFiles: false,
508
1044
  }}
1045
+ className="attachments-dropzone flex-1 h-full" // Add flex classes to ensure full height
509
1046
  onUploadStart={onUploadStart}
510
1047
  onUploadFinish={onUploadFinish}
511
1048
  background={styles.ATTACHMENTS_BG}
512
1049
  color={styles.ATTACHMENTS_COLOR}
513
1050
  minHeight={150}
514
1051
  footer={false}
515
- clickable={clickable}
1052
+ clickable={viewMode === ATTACHMENTS_VIEW_MODES__ICON && !isDragging ? clickable : false} // Disable clickable when dragging
516
1053
  {..._dropZone}
517
1054
  >
518
1055
  {content}
519
1056
  </Dropzone>;
1057
+ }
1058
+
1059
+ // directories
1060
+ if (usesDirectories) {
1061
+ content = <HStack className="h-full w-full">
1062
+ <TreePanel
1063
+ _panel={{
1064
+ title: 'Directories',
1065
+ isScrollable: true,
1066
+ isCollapsible: false,
1067
+ isCollapsed: false,
1068
+ collapseDirection: HORIZONTAL,
1069
+ disableTitleChange: true,
1070
+ className: clsx(
1071
+ 'TreePanel-Panel',
1072
+ 'h-full',
1073
+ 'w-1/3',
1074
+ ),
1075
+ }}
1076
+ _tree={{
1077
+ reference: 'tree',
1078
+ parent: self,
1079
+ Repository: AttachmentDirectories,
1080
+ autoSelectRootNode: true,
1081
+ allowToggleSelection: false,
1082
+ allowDeselectAll: false,
1083
+ forceSelectionOnCollapse: true,
1084
+ showSelectHandle: canCrud,
1085
+ useFilters: false,
1086
+ showHeaderToolbar: false,
1087
+ canNodesMoveInternally: canCrud,
1088
+ hideReloadBtn: true,
1089
+ className: clsx(
1090
+ 'TreePanel-Tree',
1091
+ 'h-full',
1092
+ 'w-full',
1093
+ 'min-w-0', // override the Tree's min-w setting
1094
+ 'flex-none',
1095
+ isDirectoriesLoading ? [
1096
+ 'border-t-4',
1097
+ 'border-t-[#f00]',
1098
+ ] : null,
1099
+ ),
1100
+ areNodesDropTarget: canCrud,
1101
+ dropTargetAccept: 'Attachments',
1102
+ canNodeAcceptDrop: (targetNode, draggedItem) => {
1103
+ // disallow drop onto its parent
1104
+ if (draggedItem.item.attachments__attachment_directory_id === targetNode.id) {
1105
+ return false;
1106
+ }
1107
+ return true;
1108
+ },
1109
+ onNodeDrop: async (targetNode, droppedItem) => {
520
1110
 
1111
+ let selectedNodes = [];
1112
+ if (droppedItem.getSelection) {
1113
+ selectedNodes = droppedItem.getSelection();
1114
+ }
1115
+ if (_.isEmpty(selectedNodes)) {
1116
+ selectedNodes = [droppedItem.item];
1117
+ }
1118
+
1119
+ // set the attachment_directory_id of the draggedItems to the targetNode.id
1120
+ for (let i = 0; i < selectedNodes.length; i++) {
1121
+ const node = selectedNodes[i];
1122
+ node.attachments__attachment_directory_id = targetNode.id;
1123
+ await node.save();
1124
+ }
1125
+
1126
+ // refresh the repository from the dragged node
1127
+ await selectedNodes[0].repository.reload();
1128
+ },
1129
+ getCustomDragProxy: (item, selection) => {
1130
+ let selectionCount = selection?.length || 1,
1131
+ displayText = item.displayValue || 'Selected TreeNode';
1132
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
1133
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
1134
+ {selectionCount > 1 &&
1135
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
1136
+ }
1137
+ </VStack>;
1138
+ },
1139
+ getNodeIcon: (node) => {
1140
+ return Folder;
1141
+ },
1142
+ onChangeSelection: (selection) => {
1143
+ setTreeSelection(selection);
1144
+ },
1145
+ additionalToolbarButtons: canCrud ? [
1146
+ {
1147
+ key: 'Plus',
1148
+ text: 'New Directory',
1149
+ handler: onCreateDirectory,
1150
+ icon: Plus,
1151
+ isDisabled: !treeSelection.length, // disabled if no selection
1152
+ },
1153
+ {
1154
+ key: 'Edit',
1155
+ text: 'Rename Directory',
1156
+ handler: onRenameDirectory,
1157
+ icon: Edit,
1158
+ isDisabled: !treeSelection.length, // disabled if no selection
1159
+ },
1160
+ {
1161
+ key: 'Trash',
1162
+ text: 'Delete Directory',
1163
+ handler: onDeleteDirectory,
1164
+ icon: Trash,
1165
+ isDisabled: !treeSelection.length || !treeSelection[0].parentId, // disabled if selection is root or none
1166
+ },
1167
+ {
1168
+ key: 'Reload',
1169
+ text: 'Reload Directories',
1170
+ handler: onReloadDirectories,
1171
+ icon: Rotate,
1172
+ },
1173
+ ] : [],
1174
+ }}
1175
+ />
1176
+
1177
+ <Box className="w-2/3">
1178
+ {content}
1179
+ </Box>
1180
+
1181
+ </HStack>;
521
1182
  }
522
- return content;
1183
+
1184
+ let className = clsx(
1185
+ 'AttachmentsElement',
1186
+ 'testx',
1187
+ 'w-full',
1188
+ 'h-[400px]',
1189
+ 'border-2',
1190
+ 'rounded-[5px]',
1191
+ );
1192
+ if (props.className) {
1193
+ className += ' ' + props.className;
1194
+ }
1195
+ return <Box className={className}>{content}</Box>;
523
1196
  }
524
1197
 
525
1198
  function withAdditionalProps(WrappedComponent) {
526
1199
  return (props) => {
1200
+ const {
1201
+ usesDirectories = false,
1202
+ } = props,
1203
+ [isReady, setIsReady] = useState(false),
1204
+ [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!)
1205
+ [Attachments] = useState(() => oneHatData.getRepository('Attachments', true)); // same
1206
+
1207
+ useEffect(() => {
1208
+ (async () => {
1209
+ Attachments.setBaseParams(props.baseParams || {}); // have to add the baseParams here, because we're bypassing withData
1210
+ if (!isReady) {
1211
+ setIsReady(true);
1212
+ }
1213
+ })();
1214
+ }, []);
1215
+
1216
+ if (!isReady) {
1217
+ return null;
1218
+ }
1219
+
527
1220
  return <WrappedComponent
528
- model="Attachments"
529
- uniqueRepository={true}
1221
+ reference="attachments"
530
1222
  {...props}
1223
+ Repository={Attachments}
1224
+ AttachmentDirectories={AttachmentDirectories}
531
1225
  />;
532
1226
  };
533
1227
  }
534
1228
 
535
- export default withComponent(withAdditionalProps(withAlert(withData(AttachmentsElement))));
1229
+ export default withAdditionalProps(withComponent(withAlert(withData(AttachmentsElement))));