@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.
- package/package.json +1 -1
- package/src/Components/Editor/AttachmentDirectoriesEditor.js +51 -0
- package/src/Components/Editor/AttachmentsEditor.js +81 -0
- package/src/Components/Form/Field/Combo/AttachmentDirectoriesCombo.js +20 -0
- package/src/Components/Form/Field/Combo/AttachmentDirectoriesComboEditor.js +22 -0
- package/src/Components/Form/Field/Combo/AttachmentsCombo.js +20 -0
- package/src/Components/Form/Field/Combo/AttachmentsComboEditor.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentDirectoriesTag.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentDirectoriesTagEditor.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentsTag.js +22 -0
- package/src/Components/Form/Field/Tag/AttachmentsTagEditor.js +22 -0
- package/src/Components/Form/Form.js +17 -17
- package/src/Components/Grid/AttachmentDirectoriesFilteredGrid.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredInlineGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesFilteredSideGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentDirectoriesGrid.js +20 -0
- package/src/Components/Grid/AttachmentDirectoriesGridEditor.js +27 -0
- package/src/Components/Grid/AttachmentDirectoriesInlineGridEditor.js +25 -0
- package/src/Components/Grid/AttachmentDirectoriesSideGridEditor.js +24 -0
- package/src/Components/Grid/AttachmentsFilteredGrid.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredInlineGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsFilteredSideGridEditor.js +17 -0
- package/src/Components/Grid/AttachmentsGrid.js +20 -0
- package/src/Components/Grid/AttachmentsGridEditor.js +27 -0
- package/src/Components/Grid/AttachmentsInlineGridEditor.js +25 -0
- package/src/Components/Grid/AttachmentsSideGridEditor.js +24 -0
- package/src/Components/Grid/Columns/AttachmentDirectoriesGridColumns.js +32 -0
- package/src/Components/Grid/Columns/AttachmentsGridColumns.js +133 -0
- package/src/Components/Grid/Grid.js +194 -21
- package/src/Components/Grid/GridHeaderRow.js +10 -17
- package/src/Components/Grid/GridRow.js +49 -22
- package/src/Components/Grid/RowHandle.js +8 -6
- package/src/Components/Hoc/withEditor.js +18 -1
- package/src/Components/Hoc/withModal.js +4 -0
- package/src/Components/Hoc/withPdfButtons.js +1 -1
- package/src/Components/Hoc/withSelection.js +26 -4
- package/src/Components/Layout/AsyncOperation.js +299 -195
- package/src/Components/Messages/GlobalModals.js +1 -2
- package/src/Components/Panel/Panel.js +14 -2
- package/src/Components/Panel/TabPanel.js +1 -1
- package/src/Components/Panel/TreePanel.js +1 -1
- package/src/Components/Report/Report.js +106 -17
- package/src/Components/Toolbar/PaginationToolbar.js +4 -3
- package/src/Components/Toolbar/Toolbar.js +6 -3
- package/src/Components/Tree/Tree.js +219 -148
- package/src/Components/Tree/TreeNode.js +20 -13
- package/src/Components/Window/AttachmentDirectoriesEditorWindow.js +34 -0
- package/src/Components/Window/AttachmentsEditorWindow.js +34 -0
- package/src/Components/index.js +92 -1
- package/src/Constants/Attachments.js +2 -0
- package/src/Constants/Dates.js +2 -2
- package/src/Constants/Progress.js +5 -1
- package/src/Models/Schemas/AttachmentDirectories.js +66 -0
- package/src/Models/Schemas/Attachments.js +88 -0
- package/src/Models/Slices/SystemSlice.js +220 -0
- package/src/PlatformImports/Web/Attachments.js +855 -161
- 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
updateModalBody(buildModalBody(imageUrl, id));
|
|
422
|
+
updateModalBody(buildModalBody(prevFile.id));
|
|
269
423
|
},
|
|
270
424
|
onNext = () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let
|
|
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 = (
|
|
315
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
633
|
+
const
|
|
634
|
+
setTrue = () => setIsLoading(true),
|
|
635
|
+
setFalse = () => setIsLoading(false),
|
|
636
|
+
setDirectoriesTrue = () => setIsDirectoriesLoading(true),
|
|
637
|
+
setDirectoriesFalse = () => setIsDirectoriesLoading(false);
|
|
363
638
|
|
|
364
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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 =
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
799
|
+
let eyeProps = {};
|
|
448
800
|
if (file.type && (file.type.match(/^image\//) || file.type === 'application/pdf')) {
|
|
449
|
-
|
|
450
|
-
onSee:
|
|
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
|
-
<
|
|
828
|
+
<DraggableFileMosaic
|
|
459
829
|
{...file}
|
|
460
830
|
backgroundBlurImage={false}
|
|
461
831
|
onDownload={onDownload}
|
|
462
832
|
{..._fileMosaic}
|
|
463
|
-
{...
|
|
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
|
-
{...
|
|
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
|
-
{
|
|
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 ' +
|
|
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:
|
|
1036
|
+
url: Attachments.api.baseURL + Attachments.schema.name + '/uploadAttachment',
|
|
502
1037
|
method: 'POST',
|
|
503
|
-
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
|
-
|
|
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
|
-
|
|
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(
|
|
1229
|
+
export default withAdditionalProps(withComponent(withAlert(withData(AttachmentsElement))));
|