@onehat/ui 0.4.77 → 0.4.78

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 (58) 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 +193 -20
  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 +1 -1
  36. package/src/Components/Hoc/withPdfButtons.js +1 -1
  37. package/src/Components/Hoc/withSelection.js +26 -4
  38. package/src/Components/Layout/AsyncOperation.js +299 -195
  39. package/src/Components/Messages/GlobalModals.js +1 -2
  40. package/src/Components/Panel/Panel.js +14 -2
  41. package/src/Components/Panel/TabPanel.js +1 -1
  42. package/src/Components/Panel/TreePanel.js +1 -1
  43. package/src/Components/Report/Report.js +106 -17
  44. package/src/Components/Toolbar/PaginationToolbar.js +4 -3
  45. package/src/Components/Toolbar/Toolbar.js +6 -3
  46. package/src/Components/Tree/Tree.js +218 -147
  47. package/src/Components/Tree/TreeNode.js +20 -13
  48. package/src/Components/Window/AttachmentDirectoriesEditorWindow.js +34 -0
  49. package/src/Components/Window/AttachmentsEditorWindow.js +34 -0
  50. package/src/Components/index.js +92 -1
  51. package/src/Constants/Attachments.js +2 -0
  52. package/src/Constants/Dates.js +2 -2
  53. package/src/Constants/Progress.js +5 -1
  54. package/src/Models/Schemas/AttachmentDirectories.js +66 -0
  55. package/src/Models/Schemas/Attachments.js +88 -0
  56. package/src/Models/Slices/SystemSlice.js +220 -0
  57. package/src/PlatformImports/Web/Attachments.js +783 -145
  58. 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,13 @@ 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 Minus from '../../Components/Icons/Minus.js';
62
+ import Edit from '../../Components/Icons/Edit.js';
63
+ import delay from '../../Functions/delay.js';
35
64
  import _ from 'lodash';
36
65
 
37
66
  const
@@ -48,21 +77,98 @@ function FileCardCustom(props) {
48
77
  onSee,
49
78
  downloadUrl,
50
79
  uploadStatus,
80
+ // Drag props
81
+ isDragSource = false,
82
+ dragSourceType = 'Attachments',
83
+ dragSourceItem = {},
84
+ item, // The actual attachment entity
51
85
  } = props,
52
86
  isDownloading = uploadStatus && inArray(uploadStatus, ['preparing', 'uploading', 'success']),
53
87
  isPdf = mimetype === 'application/pdf';
54
88
 
55
- return <Pressable
89
+ let cardContent = <Pressable
56
90
  onPress={() => {
57
91
  downloadInBackground(downloadUrl);
58
92
  }}
59
- className="px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
93
+ className="Pressable px-3 py-1 items-center flex-row rounded-[5px] border border-primary.700"
60
94
  >
61
95
  {isDownloading && <Spinner className="mr-2" />}
62
- {onSee && isPdf && <IconButton mr={1} icon={Eye} onPress={() => onSee(null, id)} />}
96
+ {onSee && isPdf && <IconButton className="mr-1" icon={Eye} onPress={() => onSee(id)} />}
63
97
  <Text>{filename}</Text>
64
- {onDelete && <IconButton ml={1} icon={Xmark} onPress={() => onDelete(id)} />}
98
+ {onDelete && <IconButton className="ml-1" icon={Xmark} onPress={() => onDelete(id)} />}
65
99
  </Pressable>;
100
+
101
+ // Wrap with drag source if needed
102
+ if (isDragSource) {
103
+ const DragSourceFileCard = withDragSource(({ children, ...dragProps }) => children);
104
+ return <DragSourceFileCard
105
+ isDragSource={isDragSource}
106
+ dragSourceType={dragSourceType}
107
+ dragSourceItem={dragSourceItem}
108
+ >
109
+ {cardContent}
110
+ </DragSourceFileCard>;
111
+ }
112
+
113
+ return cardContent;
114
+ }
115
+
116
+ function DraggableFileMosaic(props) {
117
+ const {
118
+ isDragSource = false,
119
+ dragSourceType = 'Attachments',
120
+ dragSourceItem = {},
121
+ onDragStart,
122
+ onDragEnd,
123
+ ...fileMosaicProps
124
+ } = props;
125
+
126
+ console.log('DraggableFileMosaic render:', { isDragSource, dragSourceType, hasItem: !!dragSourceItem.item });
127
+
128
+ // If not a drag source, just return the regular FileMosaic
129
+ if (!isDragSource) {
130
+ return <FileMosaic {...fileMosaicProps} />;
131
+ }
132
+
133
+ // Create a completely separate draggable container
134
+ const DragSourceContainer = withDragSource(({ dragSourceRef, ...dragProps }) => {
135
+ console.log('DragSourceContainer render with props:', dragProps);
136
+ return (
137
+ <div
138
+ ref={dragSourceRef}
139
+ style={{
140
+ display: 'inline-block',
141
+ cursor: 'grab'
142
+ }}
143
+ >
144
+ <FileMosaic
145
+ {...fileMosaicProps}
146
+ // Disable any built-in drag functionality of FileMosaic
147
+ draggable={false}
148
+ />
149
+ </div>
150
+ );
151
+ });
152
+
153
+ // Add drag handlers to the dragSourceItem
154
+ const enhancedDragSourceItem = {
155
+ ...dragSourceItem,
156
+ onDragStart: () => {
157
+ if (dragSourceItem.onDragStart) {
158
+ dragSourceItem.onDragStart();
159
+ }
160
+ if (onDragStart) {
161
+ onDragStart();
162
+ }
163
+ }
164
+ };
165
+
166
+ return <DragSourceContainer
167
+ isDragSource={true}
168
+ dragSourceType={dragSourceType}
169
+ dragSourceItem={enhancedDragSourceItem}
170
+ onDragEnd={onDragEnd}
171
+ />;
66
172
  }
67
173
 
68
174
 
@@ -80,6 +186,10 @@ function AttachmentsElement(props) {
80
186
  _dropZone = {},
81
187
  _fileMosaic = {},
82
188
  useFileMosaic = true,
189
+ usesDirectories = false,
190
+ isDirectoriesByModel = true, // if false, directories are by modelid
191
+ AttachmentDirectories,
192
+ initialViewMode = ATTACHMENTS_VIEW_MODES__ICON,
83
193
  accept, // 'image/*'
84
194
  maxFiles = null,
85
195
  disabled = false,
@@ -105,6 +215,7 @@ function AttachmentsElement(props) {
105
215
 
106
216
  // withAlert
107
217
  showModal,
218
+ hideModal,
108
219
  updateModalBody,
109
220
  alert,
110
221
  confirm,
@@ -114,10 +225,32 @@ function AttachmentsElement(props) {
114
225
  model = _.isArray(selectorSelected) && selectorSelected[0] ? selectorSelected[0].repository?.name : selectorSelected?.repository?.name,
115
226
  modelidCalc = _.isArray(selectorSelected) ? _.map(selectorSelected, (entity) => entity[selectorSelectedField]) : selectorSelected?.[selectorSelectedField],
116
227
  modelid = useRef(modelidCalc),
228
+ id = props.id || (model && modelid.current ? `attachments-${model}-${modelid.current}` : 'attachments'),
117
229
  forceUpdate = useForceUpdate(),
118
230
  [isReady, setIsReady] = useState(false),
119
231
  [isUploading, setIsUploading] = useState(false),
232
+ [isLoading, setIsLoading] = useState(false),
233
+ [isDirectoriesLoading, setIsDirectoriesLoading] = useState(false),
234
+ [viewMode, setViewModeRaw] = useState(initialViewMode),
235
+ setViewMode = (newViewMode) => {
236
+ setViewModeRaw(newViewMode);
237
+ if (id) {
238
+ setSaved(id + '-viewMode', newViewMode);
239
+ }
240
+ },
120
241
  [showAll, setShowAll] = useState(false),
242
+ [isDragging, setIsDragging] = useState(false),
243
+ treeSelectionRaw = useRef([]),
244
+ setTreeSelection = (selection) => {
245
+ treeSelectionRaw.current = selection;
246
+ forceUpdate();
247
+ },
248
+ getTreeSelection = () => {
249
+ return treeSelectionRaw.current;
250
+ },
251
+ treeSelection = getTreeSelection(),
252
+
253
+ // icon view only
121
254
  setFilesRaw = useRef([]),
122
255
  setFiles = (files) => {
123
256
  setFilesRaw.current = files;
@@ -152,14 +285,66 @@ function AttachmentsElement(props) {
152
285
  clearFiles = () => {
153
286
  setFiles([]);
154
287
  },
288
+ onFileDelete = (id) => {
289
+ const
290
+ files = getFiles(),
291
+ file = _.find(files, { id });
292
+ if (confirmBeforeDelete) {
293
+ confirm('Are you sure you want to delete the file "' + file.name + '"?', () => doDelete(id));
294
+ } else {
295
+ doDelete(id);
296
+ }
297
+ },
155
298
  toggleShowAll = () => {
156
299
  setShowAll(!showAll);
157
300
  },
301
+ doDelete = (id) => {
302
+ const
303
+ files = getFiles(),
304
+ file = Repository.getById(id);
305
+ if (file) {
306
+ // if the file exists in the repository, delete it there
307
+ Repository.deleteById(id);
308
+ Repository.save();
309
+
310
+ } else {
311
+ // simply remove it from the files array
312
+ const newFiles = [];
313
+ _.each(files, (file) => {
314
+ if (file.id !== id) {
315
+ newFiles.push(file);
316
+ }
317
+ });
318
+ setFiles(newFiles);
319
+ }
320
+ if (onDelete) {
321
+ onDelete(id);
322
+ }
323
+ },
324
+ onDownload = (id, url) => {
325
+ if (isPwa) {
326
+ // This doesn't work because iOS doesn't allow you to open another window within a PWA.
327
+ // downloadWithFetch(url);
328
+
329
+ alert('Files cannot be downloaded and viewed within an iOS PWA. Please use the Safari browser instead.');
330
+ } else {
331
+ downloadInBackground(url);
332
+ }
333
+ },
334
+
335
+ // dropzone
158
336
  onDropzoneChange = async (files) => {
159
337
  if (!files.length) {
160
338
  alert('No files accepted. Perhaps they were too large or the wrong file type?');
161
339
  return;
162
340
  }
341
+ if (usesDirectories) {
342
+ const treeSelection = getTreeSelection();
343
+ if (!treeSelection[0] || !treeSelection[0].id) {
344
+ alert('Please select a directory to upload the files to.');
345
+ return;
346
+ }
347
+ }
163
348
  setFiles(files);
164
349
  _.each(files, (file) => {
165
350
  file.extraUploadData = {
@@ -167,6 +352,9 @@ function AttachmentsElement(props) {
167
352
  modelid: modelid.current,
168
353
  ...extraUploadData,
169
354
  };
355
+ if (usesDirectories) {
356
+ file.extraUploadData.attachment_directory_id = treeSelection[0].id;
357
+ }
170
358
  });
171
359
  if (onAfterDropzoneChange) {
172
360
  const isChanged = await onAfterDropzoneChange(files);
@@ -207,82 +395,79 @@ function AttachmentsElement(props) {
207
395
  }
208
396
  }
209
397
  },
210
- onFileDelete = (id) => {
398
+
399
+ // 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) => {
211
408
  const
212
409
  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);
410
+ currentFile = findFile(id),
411
+ currentIx = _.findIndex(files, currentFile);
412
+ if (currentIx > 0) {
413
+ return files[currentIx - 1];
218
414
  }
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);
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];
228
424
  }
425
+ return null;
229
426
  },
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];
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;
248
452
  }
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];
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';
257
469
  }
258
- return null;
259
- }
260
-
261
- const
262
- prevFile = findPrevFile(url, id),
263
- isPrevDisabled = !prevFile,
264
- nextFile = findNextFile(url, id),
265
- isNextDisabled = !nextFile,
266
- onPrev = () => {
267
- const { imageUrl, id } = prevFile;
268
- updateModalBody(buildModalBody(imageUrl, id));
269
- },
270
- 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
470
  }
285
-
286
471
  if (isPdf) {
287
472
  body = <iframe
288
473
  src={url}
@@ -311,42 +496,116 @@ function AttachmentsElement(props) {
311
496
  />
312
497
  </HStack>;
313
498
  },
314
- onViewLightbox = (url, id) => {
315
- if (!url && !id) {
499
+ onViewLightbox = (id) => {
500
+ if (!id) {
316
501
  alert('Cannot view lightbox until image is uploaded.');
317
502
  return;
318
503
  }
319
504
  showModal({
320
505
  title: 'Lightbox',
321
- body: buildModalBody(url, id),
506
+ body: buildModalBody(id),
322
507
  canClose: true,
323
508
  includeCancel: true,
324
509
  w: 1920,
325
510
  h: 1080,
326
511
  });
327
512
  },
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
513
 
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);
346
- }
347
- if (onDelete) {
348
- onDelete(id);
349
- }
514
+ // AttachmentDirectories
515
+ onCreateDirectory = () => {
516
+ const treeSelection = getTreeSelection();
517
+ showModal({
518
+ title: 'New Directory',
519
+ w: 400,
520
+ h: 200,
521
+ canClose: true,
522
+ includeReset: false,
523
+ includeCancel: false,
524
+ body: <Form
525
+ editorType={EDITOR_TYPE__PLAIN}
526
+ items={[
527
+ {
528
+ type: 'Input',
529
+ name: 'directoryName',
530
+ placeholder: 'New Directory Name',
531
+ }
532
+ ]}
533
+ additionalFooterButtons={[
534
+ {
535
+ text: 'Cancel',
536
+ onPress: hideModal,
537
+ skipSubmit: true,
538
+ variant: 'outline',
539
+ }
540
+ ]}
541
+ validator={yup.object({
542
+ directoryName: yup.string().required(),
543
+ })}
544
+ onSave={async (values)=> {
545
+ const { directoryName } = values;
546
+ await AttachmentDirectories.add({
547
+ name: directoryName,
548
+ model: selectorSelected.repository.name,
549
+ modelid: selectorSelected[selectorSelectedField],
550
+ parentId: treeSelection?.[0]?.id || null,
551
+ });
552
+ hideModal();
553
+ }}
554
+ />,
555
+ });
556
+ },
557
+ onDeleteDirectory = async () => {
558
+ const attachmentDirectory = getTreeSelection()[0];
559
+ await attachmentDirectory.delete();
560
+ self.children.tree.buildAndSetTreeNodeData();
561
+ },
562
+ onRenameDirectory = () => {
563
+ const attachmentDirectory = getTreeSelection()[0];
564
+ showModal({
565
+ title: 'Rename Directory',
566
+ w: 400,
567
+ h: 200,
568
+ canClose: true,
569
+ includeReset: false,
570
+ includeCancel: false,
571
+ body: <Form
572
+ editorType={EDITOR_TYPE__PLAIN}
573
+ items={[
574
+ {
575
+ type: 'Input',
576
+ name: 'directoryName',
577
+ placeholder: 'New Directory Name',
578
+ }
579
+ ]}
580
+ additionalFooterButtons={[
581
+ {
582
+ text: 'Cancel',
583
+ onPress: hideModal,
584
+ skipSubmit: true,
585
+ variant: 'outline',
586
+ }
587
+ ]}
588
+ startingValues={{
589
+ directoryName: attachmentDirectory.attachment_directories__name,
590
+ }}
591
+ validator={yup.object({
592
+ directoryName: yup.string().required(),
593
+ })}
594
+ onSave={async (values)=> {
595
+ const {
596
+ directoryName,
597
+ } = values;
598
+ attachmentDirectory.attachment_directories__name = directoryName;
599
+ await delay(500);
600
+ await attachmentDirectory.save();
601
+ await delay(500);
602
+ self.children.tree.buildAndSetTreeNodeData();
603
+ hideModal();
604
+
605
+
606
+ }}
607
+ />,
608
+ });
350
609
  };
351
610
 
352
611
  if (!_.isEqual(modelidCalc, modelid.current)) {
@@ -359,62 +618,125 @@ function AttachmentsElement(props) {
359
618
  return () => {};
360
619
  }
361
620
 
362
- (async () => {
621
+ const
622
+ setTrue = () => setIsLoading(true),
623
+ setFalse = () => setIsLoading(false),
624
+ setDirectoriesTrue = () => setIsDirectoriesLoading(true),
625
+ setDirectoriesFalse = () => setIsDirectoriesLoading(false);
363
626
 
364
- if (!_.isArray(modelid.current)) {
627
+ Repository.on('beforeLoad', setTrue);
628
+ Repository.on('load', setFalse);
629
+ Repository.on('load', buildFiles);
630
+ if (usesDirectories) {
631
+ AttachmentDirectories.on('beforeLoad', setDirectoriesTrue);
632
+ AttachmentDirectories.on('loadRootNodes', setDirectoriesFalse);
633
+ }
365
634
 
366
- // Load Repository
367
- const filters = [
368
- {
369
- name: 'model',
370
- value: model,
371
- },
372
- {
373
- name: 'modelid',
374
- value: modelid.current,
635
+ (async () => {
636
+
637
+ if (modelid.current && !_.isArray(modelid.current)) {
638
+ const
639
+ currentConditions = Repository.getBaseParamConditions() || {},
640
+ newConditions = {
641
+ 'conditions[Attachments.model]': model,
642
+ 'conditions[Attachments.modelid]': modelid.current,
375
643
  },
376
- ];
644
+ currentPageSize = Repository.pageSize,
645
+ newPageSize = showAll ? expandedMax : collapsedMax;
646
+
647
+ // figure out conditions
377
648
  if (accept) {
378
- let name,
649
+ let name = 'mimetype IN',
379
650
  mimetypes;
380
651
  if (_.isString(accept)) {
381
652
  if (accept.match(/,/)) {
382
- name = 'mimetype IN';
383
653
  mimetypes = accept.split(',');
384
654
  } else {
385
655
  name = 'mimetype LIKE';
386
656
  mimetypes = accept.replace('*', '%');
387
657
  }
388
658
  } else if (_.isArray(accept)) {
389
- name = 'mimetype IN';
390
659
  mimetypes = accept;
391
660
  }
392
- filters.push({
393
- name,
394
- value: mimetypes,
395
- });
661
+ newConditions['conditions[Attachments.' + name + ']'] = mimetypes;
662
+ }
663
+ if (usesDirectories) {
664
+ const treeSelection = getTreeSelection();
665
+ newConditions['conditions[Attachments.attachment_directory_id]'] = treeSelection[0]?.id || null;
666
+ }
667
+ let doReload = false;
668
+ if (!_.isEqual(currentConditions, newConditions)) {
669
+ Repository.setBaseParams(newConditions);
670
+ doReload = true;
671
+ }
672
+
673
+ // figure out pageSize
674
+ if (!_.isEqual(currentPageSize, newPageSize)) {
675
+ Repository.setPageSize(newPageSize);
676
+ doReload = true;
677
+ }
678
+ if (doReload) {
679
+ await Repository.load();
680
+ }
681
+ if (usesDirectories) {
682
+ const
683
+ wasAlreadyLoaded = AttachmentDirectories.areRootNodesLoaded,
684
+ currentConditions = AttachmentDirectories.getBaseParamConditions() || {},
685
+ newConditions = {
686
+ 'conditions[AttachmentDirectories.model]': selectorSelected.repository.name,
687
+ 'conditions[AttachmentDirectories.modelid]': selectorSelected[selectorSelectedField],
688
+ };
689
+ let doReload = false;
690
+ if (!_.isEqual(currentConditions, newConditions)) {
691
+ AttachmentDirectories.setBaseParams(newConditions);
692
+ doReload = true;
693
+ }
694
+ if (doReload) {
695
+ // setTreeSelection([]); // clear it; otherwise we get stale nodes after reloading AttachmentDirectories
696
+ await AttachmentDirectories.reload();
697
+ if (wasAlreadyLoaded) {
698
+ const rootNodes = AttachmentDirectories.getRootNodes();
699
+ if (rootNodes) {
700
+ self.children.tree.setSelection(rootNodes);
701
+ }
702
+ }
703
+ }
396
704
  }
397
- Repository.filter(filters);
398
- Repository.setPageSize(showAll ? expandedMax : collapsedMax);
399
- await Repository.load();
400
705
 
401
706
  buildFiles();
402
707
  } else {
708
+ Repository.clear();
709
+ if (usesDirectories) {
710
+ AttachmentDirectories.clear();
711
+ }
403
712
  clearFiles();
404
713
  }
405
714
 
406
715
 
716
+ // Load saved view mode preference before setting ready
717
+ if (id && !isReady) {
718
+ const savedViewMode = await getSaved(id + '-viewMode');
719
+ if (!_.isNil(savedViewMode)) {
720
+ setViewModeRaw(savedViewMode);
721
+ }
722
+ }
723
+
407
724
  if (!isReady) {
408
725
  setIsReady(true);
409
726
  }
410
727
 
411
728
  })();
412
729
 
413
- Repository.on('load', buildFiles);
414
730
  return () => {
731
+ Repository.off('beforeLoad', setTrue);
732
+ Repository.off('load', setFalse);
415
733
  Repository.off('load', buildFiles);
734
+ if (usesDirectories) {
735
+ AttachmentDirectories.off('beforeLoad', setDirectoriesTrue);
736
+ AttachmentDirectories.off('loadRootNodes', setDirectoriesFalse);
737
+ }
416
738
  };
417
- }, [model, modelid.current, showAll]);
739
+ }, [model, modelid.current, showAll, getTreeSelection()]);
418
740
 
419
741
  if (!isReady) {
420
742
  return null;
@@ -429,45 +751,97 @@ function AttachmentsElement(props) {
429
751
  if (canCrud) {
430
752
  _fileMosaic.onDelete = onFileDelete;
431
753
  }
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
754
  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>}
755
+ let content = null;
756
+ // 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
772
+ className={clsx(
773
+ 'AttachmentsElement-HStack',
774
+ 'h-full',
775
+ 'flex-1',
776
+ 'flex-wrap',
777
+ files.length === 0 ? [
778
+ // So the 'No files' text is centered
779
+ 'justify-center',
780
+ 'items-center',
781
+ ] : null,
782
+ )}
783
+ >
784
+ {files.length === 0 && <Text className="text-grey-600 italic">No files {usesDirectories ? 'in this directory' : ''}</Text>}
446
785
  {files.map((file) => {
447
- let seeProps = {};
786
+ let eyeProps = {};
448
787
  if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
449
- seeProps = {
788
+ eyeProps = {
450
789
  onSee: onViewLightbox,
451
790
  };
452
791
  }
792
+
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
+
453
808
  return <Box
454
809
  key={file.id}
455
810
  className="mr-2"
456
811
  >
457
812
  {useFileMosaic &&
458
- <FileMosaic
813
+ <DraggableFileMosaic
459
814
  {...file}
460
815
  backgroundBlurImage={false}
461
816
  onDownload={onDownload}
462
817
  {..._fileMosaic}
463
- {...seeProps}
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
+ }}
464
828
  />}
465
829
  {!useFileMosaic &&
466
830
  <FileCardCustom
467
831
  {...file}
468
832
  backgroundBlurImage={false}
469
833
  {..._fileMosaic}
470
- {...seeProps}
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
+ }}
471
845
  />}
472
846
  </Box>;
473
847
  })}
@@ -488,13 +862,138 @@ function AttachmentsElement(props) {
488
862
  variant="outline"
489
863
  />}
490
864
  </VStack>;
865
+ } else if (viewMode === ATTACHMENTS_VIEW_MODES__LIST) {
866
+ content = <AttachmentsGridEditor
867
+ Repository={Repository}
868
+ selectionMode={SELECTION_MODE_MULTI}
869
+ showSelectHandle={false}
870
+ disableAdd={true}
871
+ disableEdit={true}
872
+ disableView={true}
873
+ disableCopy={true}
874
+ disableDuplicate={true}
875
+ disableDelete={!canCrud}
876
+ className="flex-1 h-full" // Ensure it takes up full space
877
+ onDragStart={() => {
878
+ setTimeout(() => setIsDragging(true), 50); // Delay to avoid interfering with drag initialization
879
+ }}
880
+ onDragEnd={() => {
881
+ setIsDragging(false);
882
+ }}
883
+ columnsConfig={[
884
+ {
885
+ id: 'view',
886
+ header: 'View',
887
+ w: 70,
888
+ isSortable: false,
889
+ isEditable: false,
890
+ isReorderable: false,
891
+ isResizable: false,
892
+ isHidable: false,
893
+ renderer: (item) => {
894
+ return <IconButton
895
+ className="w-[70px]"
896
+ icon={Eye}
897
+ _icon={{
898
+ size: 'xl',
899
+ }}
900
+ onPress={() => onViewLightbox(item.id)}
901
+ tooltip="View"
902
+ />;
903
+ },
904
+ },
905
+ {
906
+ "id": "attachments__filename",
907
+ "header": "Filename",
908
+ "fieldName": "attachments__filename",
909
+ "isSortable": true,
910
+ "isEditable": true,
911
+ "isReorderable": true,
912
+ "isResizable": true,
913
+ "w": 150
914
+ },
915
+ {
916
+ "id": "attachments__size_formatted",
917
+ "header": "Size Formatted",
918
+ "fieldName": "attachments__size_formatted",
919
+ "isSortable": false,
920
+ "isEditable": false,
921
+ "isReorderable": true,
922
+ "isResizable": true,
923
+ "w": 200
924
+ },
925
+ ]}
926
+ areRowsDragSource={canCrud}
927
+ rowDragSourceType="Attachments"
928
+ getCustomDragProxy={(item, selection) => {
929
+ let selectionCount = selection?.length || 1,
930
+ displayText = item.attachments__filename || 'Selected TreeNode';
931
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
932
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
933
+ {selectionCount > 1 &&
934
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
935
+ }
936
+ </VStack>;
937
+ }}
938
+
939
+ />;
940
+ }
941
+
942
+ // switches for icon/list view
943
+ content = <VStack
944
+ className={clsx(
945
+ 'w-full',
946
+ 'h-full',
947
+ )}
948
+ >
949
+ <HStack
950
+ className={clsx(
951
+ 'h-[30px]',
952
+ 'w-full',
953
+ 'gap-1',
954
+ 'p-1',
955
+ 'justify-start',
956
+ 'items-center',
957
+ 'bg-primary-500',
958
+ )}
959
+ >
960
+ <IconButton
961
+ onPress={() => setViewMode(ATTACHMENTS_VIEW_MODES__ICON)}
962
+ icon={Images}
963
+ className={clsx(
964
+ viewMode === ATTACHMENTS_VIEW_MODES__ICON ? 'bg-gray-400' : null,
965
+ 'w-[25px]',
966
+ 'h-[25px]',
967
+ 'px-[2px]',
968
+ 'py-[2px]',
969
+ )}
970
+ tooltip="Icon View"
971
+ />
972
+ <IconButton
973
+ onPress={() => setViewMode(ATTACHMENTS_VIEW_MODES__LIST)}
974
+ icon={List}
975
+ className={clsx(
976
+ viewMode === ATTACHMENTS_VIEW_MODES__LIST ? 'bg-gray-400' : null,
977
+ 'w-[25px]',
978
+ 'h-[25px]',
979
+ 'px-[2px]',
980
+ 'py-[2px]',
981
+ )}
982
+ tooltip="List View"
983
+ />
984
+ </HStack>
985
+
986
+ {content}
987
+
988
+ </VStack>;
491
989
 
990
+ // Always wrap content in dropzone when canCrud is true, but conditionally disable functionality
492
991
  if (canCrud) {
493
992
  content = <Dropzone
494
993
  value={files}
495
- onChange={onDropzoneChange}
496
- accept={accept}
497
- maxFiles={maxFiles}
994
+ onChange={isDragging ? () => {} : onDropzoneChange} // Disable onChange when dragging
995
+ accept={isDragging ? undefined : accept} // Remove accept types when dragging
996
+ maxFiles={isDragging ? 0 : maxFiles} // Set to 0 when dragging to prevent drops
498
997
  maxFileSize={styles.ATTACHMENTS_MAX_FILESIZE}
499
998
  autoClean={true}
500
999
  uploadConfig={{
@@ -504,32 +1003,171 @@ function AttachmentsElement(props) {
504
1003
  autoUpload,
505
1004
  }}
506
1005
  headerConfig={{
1006
+ className: '!hidden',
507
1007
  deleteFiles: false,
508
1008
  }}
1009
+ className="attachments-dropzone flex-1 h-full" // Add flex classes to ensure full height
509
1010
  onUploadStart={onUploadStart}
510
1011
  onUploadFinish={onUploadFinish}
511
1012
  background={styles.ATTACHMENTS_BG}
512
1013
  color={styles.ATTACHMENTS_COLOR}
513
1014
  minHeight={150}
514
1015
  footer={false}
515
- clickable={clickable}
1016
+ clickable={viewMode === ATTACHMENTS_VIEW_MODES__ICON && !isDragging ? clickable : false} // Disable clickable when dragging
516
1017
  {..._dropZone}
517
1018
  >
518
1019
  {content}
519
1020
  </Dropzone>;
1021
+ }
1022
+
1023
+ // directories
1024
+ if (usesDirectories) {
1025
+ content = <HStack className="h-full w-full">
1026
+ <TreePanel
1027
+ _panel={{
1028
+ title: 'Directories',
1029
+ isScrollable: true,
1030
+ isCollapsible: false,
1031
+ isCollapsed: false,
1032
+ collapseDirection: HORIZONTAL,
1033
+ disableTitleChange: true,
1034
+ className: clsx(
1035
+ 'TreePanel-Panel',
1036
+ 'h-full',
1037
+ 'w-1/3',
1038
+ ),
1039
+ }}
1040
+ _tree={{
1041
+ reference: 'tree',
1042
+ parent: self,
1043
+ Repository: AttachmentDirectories,
1044
+ autoSelectRootNode: true,
1045
+ allowToggleSelection: false,
1046
+ allowDeselectAll: false,
1047
+ forceSelectionOnCollapse: true,
1048
+ showSelectHandle: canCrud,
1049
+ useFilters: false,
1050
+ showHeaderToolbar: false,
1051
+ canNodesMoveInternally: canCrud,
1052
+ hideReloadBtn: true,
1053
+ className: clsx(
1054
+ 'TreePanel-Tree',
1055
+ 'h-full',
1056
+ 'w-full',
1057
+ 'min-w-0', // override the Tree's min-w setting
1058
+ 'flex-none',
1059
+ isDirectoriesLoading ? [
1060
+ 'border-t-4',
1061
+ 'border-t-[#f00]',
1062
+ ] : null,
1063
+ ),
1064
+ areNodesDropTarget: canCrud,
1065
+ dropTargetAccept: 'Attachments',
1066
+ canNodeAcceptDrop: (targetNode, draggedItem) => {
1067
+ // disallow drop onto its parent
1068
+ if (draggedItem.item.attachments__attachment_directory_id === targetNode.id) {
1069
+ return false;
1070
+ }
1071
+ return true;
1072
+ },
1073
+ onNodeDrop: async (targetNode, droppedItem) => {
1074
+
1075
+ let selectedNodes = [];
1076
+ if (droppedItem.getSelection) {
1077
+ selectedNodes = droppedItem.getSelection();
1078
+ }
1079
+ if (_.isEmpty(selectedNodes)) {
1080
+ selectedNodes = [droppedItem.item];
1081
+ }
1082
+
1083
+ // set the attachment_directory_id of the draggedItems to the targetNode.id
1084
+ for (let i = 0; i < selectedNodes.length; i++) {
1085
+ const node = selectedNodes[i];
1086
+ node.attachments__attachment_directory_id = targetNode.id;
1087
+ await node.save();
1088
+ }
1089
+
1090
+ // refresh the repository from the dragged node
1091
+ await selectedNodes[0].repository.reload();
1092
+ },
1093
+ getCustomDragProxy: (item, selection) => {
1094
+ let selectionCount = selection?.length || 1,
1095
+ displayText = item.displayValue || 'Selected TreeNode';
1096
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
1097
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
1098
+ {selectionCount > 1 &&
1099
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
1100
+ }
1101
+ </VStack>;
1102
+ },
1103
+ getNodeIcon: (node) => {
1104
+ return Folder;
1105
+ },
1106
+ onChangeSelection: (selection) => {
1107
+ setTreeSelection(selection);
1108
+ },
1109
+ additionalToolbarButtons: canCrud ? [
1110
+ {
1111
+ key: 'Plus',
1112
+ text: 'New Directory',
1113
+ handler: onCreateDirectory,
1114
+ icon: Plus,
1115
+ isDisabled: !treeSelection.length, // disabled if no selection
1116
+ },
1117
+ {
1118
+ key: 'Edit',
1119
+ text: 'Rename Directory',
1120
+ handler: onRenameDirectory,
1121
+ icon: Edit,
1122
+ isDisabled: !treeSelection.length, // disabled if no selection
1123
+ },
1124
+ {
1125
+ key: 'Minus',
1126
+ text: 'Delete Directory',
1127
+ handler: onDeleteDirectory,
1128
+ icon: Minus,
1129
+ isDisabled: !treeSelection.length || !treeSelection[0].parentId, // disabled if selection is root or none
1130
+ },
1131
+ ] : [],
1132
+ }}
1133
+ />
520
1134
 
1135
+ <Box className="w-2/3">
1136
+ {content}
1137
+ </Box>
1138
+
1139
+ </HStack>;
1140
+ }
1141
+
1142
+ let className = clsx(
1143
+ 'AttachmentsElement',
1144
+ 'testx',
1145
+ 'w-full',
1146
+ 'h-[400px]',
1147
+ 'border-2',
1148
+ 'rounded-[5px]',
1149
+ );
1150
+ if (props.className) {
1151
+ className += ' ' + props.className;
521
1152
  }
522
- return content;
1153
+ return <Box className={className}>{content}</Box>;
523
1154
  }
524
1155
 
525
1156
  function withAdditionalProps(WrappedComponent) {
526
1157
  return (props) => {
1158
+ const {
1159
+ usesDirectories = false,
1160
+ } = props,
1161
+ AttachmentDirectories = usesDirectories ? oneHatData.getRepository('AttachmentDirectories', true) : null; // put this here; otherwise a new unique repository will be created on every render!
1162
+
527
1163
  return <WrappedComponent
528
1164
  model="Attachments"
529
1165
  uniqueRepository={true}
1166
+ reference="attachments"
530
1167
  {...props}
1168
+ AttachmentDirectories={AttachmentDirectories}
531
1169
  />;
532
1170
  };
533
1171
  }
534
1172
 
535
- export default withComponent(withAdditionalProps(withAlert(withData(AttachmentsElement))));
1173
+ export default withAdditionalProps(withComponent(withAlert(withData(AttachmentsElement))));