@onehat/ui 0.3.181 → 0.3.186
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 +3 -2
- package/src/Components/Form/Field/Combo/Combo.js +1 -0
- package/src/Components/Grid/Grid.js +2 -1
- package/src/Components/Hoc/Secondary/withSecondaryEditor.js +9 -1
- package/src/Components/Hoc/withEditor.js +9 -1
- package/src/Components/Hoc/withFilters.js +4 -0
- package/src/Components/Hoc/withPresetButtons.js +52 -14
- package/src/Components/Panel/Panel.js +2 -1
- package/src/Components/Window/UploadsDownloadsWindow.js +189 -0
- package/src/Functions/downloadWithFetch.js +6 -3
- package/src/PlatformImports/Web/File.js +93 -213
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onehat/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.186",
|
|
4
4
|
"description": "Base UI for OneHat apps",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"react-draggable": "^4.4.5",
|
|
50
50
|
"react-native": "*",
|
|
51
51
|
"react-native-draggable": "^3.3.0",
|
|
52
|
-
"react-native-svg": "*"
|
|
52
|
+
"react-native-svg": "*",
|
|
53
|
+
"use-file-picker": "^2.1.1"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@babel/core": "^7.22.1",
|
|
@@ -686,6 +686,7 @@ export function ComboComponent(props) {
|
|
|
686
686
|
h={UiGlobals.mode === UI_MODE_WEB ? styles.FORM_COMBO_MENU_HEIGHT + 'px' : null}
|
|
687
687
|
newEntityDisplayValue={newEntityDisplayValue}
|
|
688
688
|
disablePresetButtons={!isEditor}
|
|
689
|
+
alternateRowBackgrounds={false}
|
|
689
690
|
onChangeSelection={(selection) => {
|
|
690
691
|
|
|
691
692
|
if (Repository && selection[0]?.isPhantom) {
|
|
@@ -121,6 +121,7 @@ function GridComponent(props) {
|
|
|
121
121
|
flex,
|
|
122
122
|
bg = '#fff',
|
|
123
123
|
verifyCanEdit,
|
|
124
|
+
alternateRowBackgrounds = true,
|
|
124
125
|
alternatingInterval = 2,
|
|
125
126
|
|
|
126
127
|
// withComponent
|
|
@@ -410,7 +411,7 @@ function GridComponent(props) {
|
|
|
410
411
|
}
|
|
411
412
|
} else if (showHovers && isHovered) {
|
|
412
413
|
mixWith = styles.GRID_ROW_HOVER_BG;
|
|
413
|
-
} else if (index % alternatingInterval === 0) { // i.e. every second line, or every third line
|
|
414
|
+
} else if (alternateRowBackgrounds && index % alternatingInterval === 0) { // i.e. every second line, or every third line
|
|
414
415
|
mixWith = styles.GRID_ROW_ALTERNATE_BG;
|
|
415
416
|
}
|
|
416
417
|
if (mixWith) {
|
|
@@ -138,7 +138,15 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
|
|
|
138
138
|
} else {
|
|
139
139
|
// Set repository to sort by id DESC and switch to page 1, so this new entity is guaranteed to show up on the current page, even after saving
|
|
140
140
|
const currentSorter = SecondaryRepository.sorters[0];
|
|
141
|
-
if (currentSorter.name
|
|
141
|
+
if (currentSorter.name.match(/__sort_order$/)) { // when it's using a sort column, keep using it
|
|
142
|
+
if (currentSorter.direction !== 'DESC') {
|
|
143
|
+
SecondaryRepository.pauseEvents();
|
|
144
|
+
SecondaryRepository.sort(currentSorter.name, 'DESC');
|
|
145
|
+
SecondaryRepository.setPage(1);
|
|
146
|
+
SecondaryRepository.resumeEvents();
|
|
147
|
+
await SecondaryRepository.reload();
|
|
148
|
+
}
|
|
149
|
+
} else if (currentSorter.name !== SecondaryRepository.schema.model.idProperty || currentSorter.direction !== 'DESC') {
|
|
142
150
|
SecondaryRepository.pauseEvents();
|
|
143
151
|
SecondaryRepository.sort(SecondaryRepository.schema.model.idProperty, 'DESC');
|
|
144
152
|
SecondaryRepository.setPage(1);
|
|
@@ -135,7 +135,15 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
135
135
|
} else {
|
|
136
136
|
// Set repository to sort by id DESC and switch to page 1, so this new entity is guaranteed to show up on the current page, even after saving
|
|
137
137
|
const currentSorter = Repository.sorters[0];
|
|
138
|
-
if (currentSorter.name
|
|
138
|
+
if (currentSorter.name.match(/__sort_order$/)) { // when it's using a sort column, keep using it
|
|
139
|
+
if (currentSorter.direction !== 'DESC') {
|
|
140
|
+
Repository.pauseEvents();
|
|
141
|
+
Repository.sort(currentSorter.name, 'DESC');
|
|
142
|
+
Repository.setPage(1);
|
|
143
|
+
Repository.resumeEvents();
|
|
144
|
+
await Repository.reload();
|
|
145
|
+
}
|
|
146
|
+
} else if (currentSorter.name !== Repository.schema.model.idProperty || currentSorter.direction !== 'DESC') {
|
|
139
147
|
Repository.pauseEvents();
|
|
140
148
|
Repository.sort(Repository.schema.model.idProperty, 'DESC');
|
|
141
149
|
Repository.setPage(1);
|
|
@@ -297,6 +297,10 @@ export default function withFilters(WrappedComponent) {
|
|
|
297
297
|
elementProps.autoSubmit = true;
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
if (!Element) {
|
|
301
|
+
debugger;
|
|
302
|
+
return; // to protect against errors
|
|
303
|
+
}
|
|
300
304
|
if (field === 'q') {
|
|
301
305
|
elementProps.flex = 1;
|
|
302
306
|
elementProps.minWidth = 100;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
} from 'native-base';
|
|
2
5
|
import Clipboard from '../Icons/Clipboard.js';
|
|
3
6
|
import Duplicate from '../Icons/Duplicate.js';
|
|
4
7
|
import Edit from '../Icons/Edit.js';
|
|
@@ -6,7 +9,9 @@ import Eye from '../Icons/Eye.js';
|
|
|
6
9
|
import Trash from '../Icons/Trash.js';
|
|
7
10
|
import Plus from '../Icons/Plus.js';
|
|
8
11
|
import Print from '../Icons/Print.js';
|
|
12
|
+
import UploadDownload from '../Icons/UploadDownload.js';
|
|
9
13
|
import inArray from '../../Functions/inArray.js';
|
|
14
|
+
import UploadsDownloadsWindow from '../Window/UploadsDownloadsWindow.js';
|
|
10
15
|
import _ from 'lodash';
|
|
11
16
|
|
|
12
17
|
// Note: A 'present button' will create both a context menu item
|
|
@@ -20,6 +25,7 @@ const presetButtons = [
|
|
|
20
25
|
'copy',
|
|
21
26
|
'duplicate',
|
|
22
27
|
// 'print',
|
|
28
|
+
'uploadDownload',
|
|
23
29
|
];
|
|
24
30
|
|
|
25
31
|
export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
@@ -34,6 +40,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
34
40
|
// extract and pass
|
|
35
41
|
contextMenuItems = [],
|
|
36
42
|
additionalToolbarButtons = [],
|
|
43
|
+
useUploadDownload = false,
|
|
37
44
|
onChangeColumnsConfig,
|
|
38
45
|
verifyCanEdit,
|
|
39
46
|
verifyCanDelete,
|
|
@@ -60,6 +67,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
60
67
|
// withComponent
|
|
61
68
|
self,
|
|
62
69
|
|
|
70
|
+
// withData
|
|
71
|
+
Repository,
|
|
72
|
+
|
|
63
73
|
// withEditor
|
|
64
74
|
userCanEdit = true,
|
|
65
75
|
userCanView = true,
|
|
@@ -77,6 +87,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
77
87
|
selectorSelected,
|
|
78
88
|
} = props,
|
|
79
89
|
[isReady, setIsReady] = useState(false),
|
|
90
|
+
[isModalShown, setIsModalShown] = useState(false),
|
|
80
91
|
[localContextMenuItems, setLocalContextMenuItems] = useState([]),
|
|
81
92
|
[localAdditionalToolbarButtons, setLocalAdditionalToolbarButtons] = useState([]),
|
|
82
93
|
[localColumnsConfig, setLocalColumnsConfig] = useState([]),
|
|
@@ -124,6 +135,11 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
124
135
|
isDisabled = true;
|
|
125
136
|
}
|
|
126
137
|
break;
|
|
138
|
+
case 'uploadDownload':
|
|
139
|
+
if (!useUploadDownload) {
|
|
140
|
+
isDisabled = true;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
127
143
|
default:
|
|
128
144
|
}
|
|
129
145
|
return isDisabled;
|
|
@@ -224,6 +240,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
224
240
|
// handler = onPrint;
|
|
225
241
|
// icon = <Print />;
|
|
226
242
|
// break;
|
|
243
|
+
case 'uploadDownload':
|
|
244
|
+
key = 'uploadDownloadBtn';
|
|
245
|
+
text = 'Upload/Download';
|
|
246
|
+
handler = onUploadDownload;
|
|
247
|
+
icon = <UploadDownload />;
|
|
248
|
+
break;
|
|
227
249
|
default:
|
|
228
250
|
}
|
|
229
251
|
return {
|
|
@@ -282,7 +304,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
282
304
|
if (showInfo) {
|
|
283
305
|
showInfo('Copied to clipboard!');
|
|
284
306
|
}
|
|
285
|
-
}
|
|
307
|
+
},
|
|
308
|
+
onUploadDownload = () => setIsModalShown(true),
|
|
309
|
+
onModalClose = () => setIsModalShown(false);
|
|
286
310
|
// onPrint = () => {
|
|
287
311
|
// debugger;
|
|
288
312
|
// };
|
|
@@ -298,18 +322,32 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
298
322
|
return null;
|
|
299
323
|
}
|
|
300
324
|
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
325
|
+
return <>
|
|
326
|
+
<WrappedComponent
|
|
327
|
+
{...propsToPass}
|
|
328
|
+
disablePresetButtons={false}
|
|
329
|
+
contextMenuItems={[
|
|
330
|
+
...contextMenuItems,
|
|
331
|
+
...localContextMenuItems,
|
|
332
|
+
]}
|
|
333
|
+
additionalToolbarButtons={[
|
|
334
|
+
...additionalToolbarButtons,
|
|
335
|
+
...localAdditionalToolbarButtons,
|
|
336
|
+
]}
|
|
337
|
+
onChangeColumnsConfig={onChangeColumnsConfigDecorator}
|
|
338
|
+
/>
|
|
339
|
+
{isModalShown &&
|
|
340
|
+
<Modal
|
|
341
|
+
isOpen={true}
|
|
342
|
+
onClose={onModalClose}
|
|
343
|
+
>
|
|
344
|
+
<UploadsDownloadsWindow
|
|
345
|
+
reference="uploadsDownloads"
|
|
346
|
+
onClose={onModalClose}
|
|
347
|
+
Repository={Repository}
|
|
348
|
+
columnsConfig={props.columnsConfig}
|
|
349
|
+
/>
|
|
350
|
+
</Modal>}
|
|
351
|
+
</>;
|
|
314
352
|
};
|
|
315
353
|
}
|
|
@@ -13,6 +13,7 @@ import Inflector from 'inflector-js';
|
|
|
13
13
|
import Header from './Header.js';
|
|
14
14
|
import Mask from './Mask.js';
|
|
15
15
|
import withCollapsible from '../Hoc/withCollapsible.js';
|
|
16
|
+
import withComponent from '../Hoc/withComponent.js';
|
|
16
17
|
import emptyFn from '../../Functions/emptyFn.js';
|
|
17
18
|
import UiGlobals from '../../UiGlobals.js';
|
|
18
19
|
import _ from 'lodash';
|
|
@@ -169,4 +170,4 @@ function Panel(props) {
|
|
|
169
170
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
export default withCollapsible(Panel);
|
|
173
|
+
export default withComponent(withCollapsible(Panel));
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COPYRIGHT NOTICE
|
|
3
|
+
* This file is categorized as "Custom Source Code"
|
|
4
|
+
* and is subject to the terms and conditions defined in the
|
|
5
|
+
* "LICENSE.txt" file, which is part of this source code package.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Icon,
|
|
10
|
+
} from 'native-base';
|
|
11
|
+
import Excel from '../Icons/Excel';
|
|
12
|
+
import Panel from '../Panel/Panel.js';
|
|
13
|
+
import Form from '../Form/Form.js';
|
|
14
|
+
import useAdjustedWindowSize from '../../Hooks/useAdjustedWindowSize.js';
|
|
15
|
+
import downloadWithFetch from '../../Functions/downloadWithFetch.js';
|
|
16
|
+
import withAlert from '../Hoc/withAlert.js';
|
|
17
|
+
import withComponent from '../Hoc/withComponent.js';
|
|
18
|
+
import Cookies from 'js-cookie';
|
|
19
|
+
import _ from 'lodash';
|
|
20
|
+
|
|
21
|
+
function UploadsDownloadsWindow(props) {
|
|
22
|
+
const
|
|
23
|
+
{
|
|
24
|
+
Repository,
|
|
25
|
+
columnsConfig = [],
|
|
26
|
+
|
|
27
|
+
// withComponent
|
|
28
|
+
self,
|
|
29
|
+
|
|
30
|
+
// withAlert
|
|
31
|
+
alert,
|
|
32
|
+
showInfo,
|
|
33
|
+
} = props,
|
|
34
|
+
[importFile, setImportFile] = useState(null),
|
|
35
|
+
[width, height] = useAdjustedWindowSize(400, 400),
|
|
36
|
+
onDownload = (isTemplate = false) => {
|
|
37
|
+
const
|
|
38
|
+
baseURL = Repository.api.baseURL,
|
|
39
|
+
filters = Repository.filters.reduce((result, current) => {
|
|
40
|
+
result[current.name] = current.value;
|
|
41
|
+
return result;
|
|
42
|
+
}, {}),
|
|
43
|
+
columns = columnsConfig.map((column) => {
|
|
44
|
+
return column.fieldName;
|
|
45
|
+
}),
|
|
46
|
+
order = Repository.getSortField() + ' ' + Repository.getSortDirection(),
|
|
47
|
+
model = Repository.name,
|
|
48
|
+
url = baseURL + 'Reports/getReport',
|
|
49
|
+
download_token = 'dl' + (new Date()).getTime(),
|
|
50
|
+
options = {
|
|
51
|
+
// method: 'GET',
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
download_token,
|
|
55
|
+
report_id: 1,
|
|
56
|
+
filters,
|
|
57
|
+
columns,
|
|
58
|
+
order,
|
|
59
|
+
model,
|
|
60
|
+
isTemplate,
|
|
61
|
+
}),
|
|
62
|
+
headers: _.merge({ 'Content-Type': 'application/json' }, Repository.headers),
|
|
63
|
+
},
|
|
64
|
+
fetchWindow = downloadWithFetch(url, options),
|
|
65
|
+
interval = setInterval(function() {
|
|
66
|
+
const cookie = Cookies.get(download_token);
|
|
67
|
+
if (fetchWindow.window && cookie) {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
Cookies.remove(download_token);
|
|
70
|
+
fetchWindow.window.close();
|
|
71
|
+
}
|
|
72
|
+
}, 1000);
|
|
73
|
+
},
|
|
74
|
+
onDownloadTemplate = () => {
|
|
75
|
+
onDownload(true);
|
|
76
|
+
},
|
|
77
|
+
onUpload = async () => {
|
|
78
|
+
const
|
|
79
|
+
url = Repository.api.baseURL + Repository.name + '/uploadBatch',
|
|
80
|
+
result = await Repository._send('POST', url, { importFile })
|
|
81
|
+
.catch(error => {
|
|
82
|
+
if (Repository.debugMode) {
|
|
83
|
+
console.log(url + ' error', error);
|
|
84
|
+
console.log('response:', error.response);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (Repository.debugMode) {
|
|
88
|
+
console.log('Result ' + url, result);
|
|
89
|
+
}
|
|
90
|
+
const
|
|
91
|
+
parsed = JSON.parse(result.data),
|
|
92
|
+
{
|
|
93
|
+
data,
|
|
94
|
+
success,
|
|
95
|
+
message,
|
|
96
|
+
} = parsed;
|
|
97
|
+
if (!success) {
|
|
98
|
+
const msgElements = ['Could not upload.'];
|
|
99
|
+
if (message === 'Errors') {
|
|
100
|
+
// assemble the errors from the upload
|
|
101
|
+
_.each(data, (obj) => {
|
|
102
|
+
// {
|
|
103
|
+
// "2": "ID does not exist."
|
|
104
|
+
// }
|
|
105
|
+
const line = Object.entries(obj)
|
|
106
|
+
.map(([key, value]) => `Line ${key}: ${value}`)
|
|
107
|
+
.join("\n");
|
|
108
|
+
msgElements.push(line);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
alert(msgElements.join("\n"));
|
|
112
|
+
} else {
|
|
113
|
+
setImportFile(null);
|
|
114
|
+
self.formSetValue('file', null);
|
|
115
|
+
showInfo("Upload successful.\n");
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return <Panel
|
|
120
|
+
{...props}
|
|
121
|
+
parent={self}
|
|
122
|
+
reference="UploadsDownloadsWindow"
|
|
123
|
+
isCollapsible={false}
|
|
124
|
+
title="Uploads & Downloads"
|
|
125
|
+
bg="#fff"
|
|
126
|
+
w={width}
|
|
127
|
+
h={height}
|
|
128
|
+
flex={null}
|
|
129
|
+
>
|
|
130
|
+
<Form
|
|
131
|
+
{...props}
|
|
132
|
+
parent={self}
|
|
133
|
+
reference="form"
|
|
134
|
+
items={[
|
|
135
|
+
{
|
|
136
|
+
"type": "Column",
|
|
137
|
+
"flex": 1,
|
|
138
|
+
"defaults": {},
|
|
139
|
+
"items": [
|
|
140
|
+
{
|
|
141
|
+
type: 'DisplayField',
|
|
142
|
+
text: 'Download an Excel file of the current grid contents.',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'Button',
|
|
146
|
+
text: 'Download',
|
|
147
|
+
isEditable: false,
|
|
148
|
+
leftIcon: <Icon as={Excel} />,
|
|
149
|
+
onPress: () => onDownload(),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'DisplayField',
|
|
153
|
+
text: 'Upload an Excel file to the current grid.',
|
|
154
|
+
mt: 10,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'File',
|
|
158
|
+
name: 'file',
|
|
159
|
+
onChangeValue: setImportFile,
|
|
160
|
+
accept: '.xlsx',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'Button',
|
|
164
|
+
text: 'Upload',
|
|
165
|
+
isEditable: false,
|
|
166
|
+
leftIcon: <Icon as={Excel} />,
|
|
167
|
+
isDisabled: !importFile,
|
|
168
|
+
onPress: onUpload,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'Button',
|
|
172
|
+
text: 'Get Template',
|
|
173
|
+
isEditable: false,
|
|
174
|
+
onPress: onDownloadTemplate,
|
|
175
|
+
variant: 'ghost',
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
]}
|
|
180
|
+
// record={selection}
|
|
181
|
+
// onCancel={onCancel}
|
|
182
|
+
// onSave={onSave}
|
|
183
|
+
// onClose={onClose}
|
|
184
|
+
// onDelete={onDelete}
|
|
185
|
+
/>
|
|
186
|
+
</Panel>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default withComponent(withAlert(UploadsDownloadsWindow));
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
const downloadWithFetch = (url, options = {}) => {
|
|
2
|
+
let obj = {};
|
|
2
3
|
fetch(url, options)
|
|
3
4
|
.then( res => res.blob() )
|
|
4
5
|
.then( blob => {
|
|
5
6
|
const
|
|
6
7
|
winName = 'ReportWindow',
|
|
7
8
|
opts = 'resizable=yes,height=600,width=800,location=0,menubar=0,scrollbars=1',
|
|
8
|
-
|
|
9
|
-
file =
|
|
10
|
-
|
|
9
|
+
win = window.open('', winName, opts),
|
|
10
|
+
file = win.URL.createObjectURL(blob);
|
|
11
|
+
obj.window = win;
|
|
12
|
+
win.location.assign(file);
|
|
11
13
|
});
|
|
14
|
+
return obj;
|
|
12
15
|
};
|
|
13
16
|
export default downloadWithFetch;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Box,
|
|
4
3
|
Button,
|
|
5
4
|
Column,
|
|
6
|
-
Pressable,
|
|
7
5
|
Row,
|
|
8
|
-
Spinner,
|
|
9
6
|
Text,
|
|
10
7
|
} from 'native-base';
|
|
11
8
|
import {
|
|
@@ -14,52 +11,22 @@ import {
|
|
|
14
11
|
UI_MODE_REACT_NATIVE,
|
|
15
12
|
} from '../../Constants/UiModes.js';
|
|
16
13
|
import UiGlobals from '../../UiGlobals.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// import {
|
|
15
|
+
// FileAmountLimitValidator,
|
|
16
|
+
// FileTypeValidator,
|
|
17
|
+
// FileSizeValidator,
|
|
18
|
+
// ImageDimensionsValidator,
|
|
19
|
+
// } from 'use-file-picker/validators';
|
|
20
|
+
import { useFilePicker, useImperativeFilePicker } from 'use-file-picker'; // https://www.npmjs.com/package/use-file-picker
|
|
23
21
|
import IconButton from '../../Components/Buttons/IconButton.js';
|
|
24
22
|
import Xmark from '../../Components/Icons/Xmark.js'
|
|
25
23
|
import withAlert from '../../Components/Hoc/withAlert.js';
|
|
26
24
|
import withValue from '../../Components/Hoc/withValue.js';
|
|
27
|
-
import
|
|
25
|
+
import Loading from '../../Components/Messages/Loading.js';
|
|
28
26
|
import _ from 'lodash';
|
|
29
27
|
|
|
30
|
-
const
|
|
31
|
-
EXPANDED_MAX = 100,
|
|
32
|
-
COLLAPSED_MAX = 2;
|
|
33
|
-
|
|
34
|
-
function FileCardCustom(props) {
|
|
35
|
-
const
|
|
36
|
-
{
|
|
37
|
-
id,
|
|
38
|
-
name: filename,
|
|
39
|
-
type: mimetype,
|
|
40
|
-
onDelete,
|
|
41
|
-
downloadUrl,
|
|
42
|
-
uploadStatus,
|
|
43
|
-
} = props,
|
|
44
|
-
isDownloading = uploadStatus && inArray(uploadStatus, ['preparing', 'uploading', 'success']);
|
|
45
|
-
return <Pressable
|
|
46
|
-
px={3}
|
|
47
|
-
py={1}
|
|
48
|
-
alignItems="center"
|
|
49
|
-
flexDirection="row"
|
|
50
|
-
borderRadius={5}
|
|
51
|
-
borderWidth={1}
|
|
52
|
-
borderColor="primary.700"
|
|
53
|
-
onPress={() => {
|
|
54
|
-
downloadWithFetch(downloadUrl);
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{isDownloading && <Spinner mr={2} />}
|
|
58
|
-
<Text>{filename}</Text>
|
|
59
|
-
{onDelete && <IconButton ml={1} icon={Xmark} onPress={() => onDelete(id)} />}
|
|
60
|
-
</Pressable>;
|
|
61
|
-
}
|
|
62
28
|
|
|
29
|
+
// This component is used to present a single file upload button
|
|
63
30
|
|
|
64
31
|
function FileComponent(props) {
|
|
65
32
|
|
|
@@ -68,186 +35,99 @@ function FileComponent(props) {
|
|
|
68
35
|
}
|
|
69
36
|
|
|
70
37
|
const {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
selectorSelected,
|
|
38
|
+
encodeAsBase64 = true,
|
|
39
|
+
readAs = 'BinaryString', // 'DataURL', 'Text', 'BinaryString', 'ArrayBuffer'
|
|
40
|
+
accept, // ['.png', '.txt'], 'image/*', '.txt'
|
|
41
|
+
multiple = false,
|
|
42
|
+
readFilesContent = true, // Ignores files content and omits reading process if set to false
|
|
43
|
+
validators, // [ new FileAmountLimitValidator({ max: 1 }), new FileTypeValidator(['jpg', 'png']), new FileSizeValidator({ maxFileSize: 50 * 1024 * 1024 /* 50 MB */ }), new ImageDimensionsValidator({maxHeight: 900,maxWidth: 1600,minHeight: 600,minWidth: 768,}),]
|
|
44
|
+
onFilesSelected, // always called, even if there are errors
|
|
45
|
+
onFilesRejected, // called when there were validation errors
|
|
46
|
+
onFilesSuccessfullySelected, // called when there were no validation errors
|
|
47
|
+
onFileRemoved, // called when a file is removed from the list of selected files
|
|
48
|
+
onClear, // called when the selection is cleared
|
|
83
49
|
|
|
84
50
|
// withValue
|
|
85
51
|
value,
|
|
86
52
|
setValue,
|
|
87
|
-
|
|
88
|
-
// withAlert
|
|
89
|
-
alert,
|
|
90
|
-
confirm,
|
|
91
|
-
|
|
92
53
|
} = props,
|
|
93
54
|
styles = UiGlobals.styles,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// imageUrl: entity.attachments__uri, // string A string representation or web url of the image that will be set to the "src" prop of an <img/> tag. If given, the component will use this image source instead of reading the image file.
|
|
114
|
-
// downloadUrl: entity.attachments__uri, // string The url to be used to perform a GET request in order to download the file. If defined, the download icon will be shown.
|
|
115
|
-
// // progress: null, // number The current percentage of upload progress. This value will have a higher priority over the upload progress value calculated inside the component.
|
|
116
|
-
// // extraUploadData: null, // Record<string, any> The additional data that will be sent to the server when files are uploaded individually
|
|
117
|
-
// // extraData: null, // Object Any kind of extra data that could be needed.
|
|
118
|
-
// // serverResponse: null, // ServerResponse The upload response from server.
|
|
119
|
-
// // xhr: null, // XMLHttpRequest A reference to the XHR object that allows the upload, progress and abort events.
|
|
120
|
-
// };
|
|
121
|
-
// });
|
|
122
|
-
// setFiles(files);
|
|
123
|
-
// },
|
|
124
|
-
clearFiles = () => {
|
|
125
|
-
setFiles([]);
|
|
126
|
-
},
|
|
127
|
-
toggleShowAll = () => {
|
|
128
|
-
setShowAll(!showAll);
|
|
129
|
-
},
|
|
130
|
-
onDropzoneChange = (files) => {
|
|
131
|
-
if (!files.length) {
|
|
132
|
-
alert('No files accepted. Perhaps they were too large or the wrong file type?');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
setFiles(files);
|
|
136
|
-
},
|
|
137
|
-
onUploadStart = (files) => {
|
|
138
|
-
setIsUploading(true);
|
|
139
|
-
},
|
|
140
|
-
onUploadFinish = (files) => {
|
|
141
|
-
let isDoneUploading = true,
|
|
142
|
-
isError = false;
|
|
143
|
-
|
|
144
|
-
_.each(files, (file) => {
|
|
145
|
-
if (!file.xhr || file.xhr.status !== 200) {
|
|
146
|
-
isDoneUploading = false;
|
|
147
|
-
return false; // break
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (isDoneUploading) {
|
|
152
|
-
_.each(files, (file) => {
|
|
153
|
-
if (file.uploadStatus === 'error') {
|
|
154
|
-
isError = true;
|
|
155
|
-
const msg = file.serverResponse?.payload || 'An error occurred';
|
|
156
|
-
alert(msg);
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
if (!isError) {
|
|
161
|
-
setIsUploading(false);
|
|
55
|
+
{
|
|
56
|
+
openFilePicker,
|
|
57
|
+
filesContent,
|
|
58
|
+
loading,
|
|
59
|
+
errors,
|
|
60
|
+
plainFiles,
|
|
61
|
+
clear,
|
|
62
|
+
} = useFilePicker({
|
|
63
|
+
readAs,
|
|
64
|
+
accept,
|
|
65
|
+
multiple,
|
|
66
|
+
readFilesContent,
|
|
67
|
+
validators,
|
|
68
|
+
onFilesSelected,
|
|
69
|
+
onFilesRejected,
|
|
70
|
+
onFilesSuccessfullySelected: ({ filesContent, plainFiles }) => {
|
|
71
|
+
let value = filesContent[0].content;
|
|
72
|
+
if (readAs === 'BinaryString' && encodeAsBase64) {
|
|
73
|
+
value = btoa(value); // convert to base64 encoded string
|
|
162
74
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
75
|
+
setValue(value);
|
|
76
|
+
},
|
|
77
|
+
onFileRemoved,
|
|
78
|
+
onClear: () => setValue(null),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (errors.length) {
|
|
83
|
+
const
|
|
84
|
+
errorStack = errors.map(err => err.name + ': ' + err.reason),
|
|
85
|
+
msg = errorStack.join("\n");
|
|
86
|
+
alert(msg);
|
|
87
|
+
}
|
|
88
|
+
}, [errors.length]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!value && filesContent.length) {
|
|
92
|
+
clear();
|
|
93
|
+
}
|
|
94
|
+
}, [value, filesContent.length]);
|
|
95
|
+
|
|
96
|
+
if (loading) {
|
|
97
|
+
return <Loading />;
|
|
179
98
|
}
|
|
180
|
-
let content = <Column
|
|
181
|
-
w="100%"
|
|
182
|
-
p={2}
|
|
183
|
-
background={styles.ATTACHMENTS_BG}
|
|
184
|
-
>
|
|
185
|
-
<Row flexWrap="wrap">
|
|
186
|
-
{files.map((file) => {
|
|
187
|
-
return <Box
|
|
188
|
-
key={file.id}
|
|
189
|
-
marginRight={4}
|
|
190
|
-
>
|
|
191
|
-
{useFileMosaic &&
|
|
192
|
-
<FileMosaic
|
|
193
|
-
{...file}
|
|
194
|
-
backgroundBlurImage={false}
|
|
195
|
-
{..._fileMosaic}
|
|
196
|
-
/>}
|
|
197
|
-
{!useFileMosaic &&
|
|
198
|
-
<FileCardCustom
|
|
199
|
-
{...file}
|
|
200
|
-
backgroundBlurImage={false}
|
|
201
|
-
{..._fileMosaic}
|
|
202
|
-
/>}
|
|
203
|
-
</Box>;
|
|
204
|
-
})}
|
|
205
|
-
</Row>
|
|
206
|
-
{Repository.total <= COLLAPSED_MAX ? null :
|
|
207
|
-
<Button
|
|
208
|
-
onPress={toggleShowAll}
|
|
209
|
-
mt={4}
|
|
210
|
-
_text={{
|
|
211
|
-
color: 'trueGray.600',
|
|
212
|
-
fontStyle: 'italic',
|
|
213
|
-
textAlign: 'left',
|
|
214
|
-
width: '100%',
|
|
215
|
-
}}
|
|
216
|
-
variant="ghost"
|
|
217
|
-
>{'Show ' + (showAll ? ' Less' : ' All ' + Repository.total)}</Button>}
|
|
218
|
-
</Column>;
|
|
219
|
-
|
|
220
|
-
if (canCrud) {
|
|
221
|
-
content = <Dropzone
|
|
222
|
-
value={files}
|
|
223
|
-
onChange={onDropzoneChange}
|
|
224
|
-
accept={accept}
|
|
225
|
-
maxFiles={maxFiles}
|
|
226
|
-
maxFileSize={styles.ATTACHMENTS_MAX_FILESIZE}
|
|
227
|
-
autoClean={true}
|
|
228
|
-
uploadConfig={{
|
|
229
|
-
url: Repository.api.baseURL + Repository.name + '/uploadAttachment',
|
|
230
|
-
method: 'POST',
|
|
231
|
-
headers: Repository.headers,
|
|
232
|
-
autoUpload: true,
|
|
233
|
-
}}
|
|
234
|
-
headerConfig={{
|
|
235
|
-
deleteFiles: false,
|
|
236
|
-
}}
|
|
237
|
-
onUploadStart={onUploadStart}
|
|
238
|
-
onUploadFinish={onUploadFinish}
|
|
239
|
-
background={styles.ATTACHMENTS_BG}
|
|
240
|
-
color={styles.ATTACHMENTS_COLOR}
|
|
241
|
-
minHeight={150}
|
|
242
|
-
footer={false}
|
|
243
|
-
clickable={clickable}
|
|
244
|
-
{..._dropZone}
|
|
245
|
-
>
|
|
246
|
-
{content}
|
|
247
|
-
</Dropzone>;
|
|
248
99
|
|
|
100
|
+
let assembledComponents = null;
|
|
101
|
+
if (_.isEmpty(filesContent)) {
|
|
102
|
+
assembledComponents = <Button onPress={() => openFilePicker()}>Select File</Button>;
|
|
103
|
+
} else {
|
|
104
|
+
assembledComponents = <Row
|
|
105
|
+
px={3}
|
|
106
|
+
py={1}
|
|
107
|
+
alignItems="center"
|
|
108
|
+
borderRadius={5}
|
|
109
|
+
borderWidth={1}
|
|
110
|
+
borderColor="primary.700"
|
|
111
|
+
>
|
|
112
|
+
<IconButton
|
|
113
|
+
_icon={{
|
|
114
|
+
as: Xmark,
|
|
115
|
+
color: 'trueGray.600',
|
|
116
|
+
size: 'sm',
|
|
117
|
+
}}
|
|
118
|
+
onPress={() => clear()}
|
|
119
|
+
h="100%"
|
|
120
|
+
bg={styles.FORM_COMBO_TRIGGER_BG}
|
|
121
|
+
_hover={{
|
|
122
|
+
bg: styles.FORM_COMBO_TRIGGER_HOVER_BG,
|
|
123
|
+
}}
|
|
124
|
+
mr={1}
|
|
125
|
+
/>
|
|
126
|
+
<Text>{plainFiles[0].name}</Text>
|
|
127
|
+
</Row>;
|
|
249
128
|
}
|
|
250
|
-
|
|
129
|
+
|
|
130
|
+
return assembledComponents;
|
|
251
131
|
}
|
|
252
132
|
|
|
253
133
|
export default withAlert(withValue(FileComponent));
|