@performant-software/semantic-components 1.0.3 → 1.0.4-beta.1
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/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/main.css +25 -0
- package/package.json +2 -2
- package/src/components/FileUploadModal.js +294 -319
- package/src/components/FileUploadProgress.css +24 -0
- package/src/components/FileUploadProgress.js +75 -0
- package/src/components/FileUploadStatus.css +3 -0
- package/src/components/FileUploadStatus.js +65 -0
- package/src/i18n/en.json +2 -1
- package/types/components/FileUploadModal.js.flow +294 -319
- package/types/components/FileUploadProgress.js.flow +75 -0
- package/types/components/FileUploadStatus.js.flow +65 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ComponentType
|
|
8
|
+
} from 'react';
|
|
4
9
|
import {
|
|
5
10
|
Button,
|
|
6
11
|
Dimmer,
|
|
@@ -11,357 +16,290 @@ import {
|
|
|
11
16
|
Modal
|
|
12
17
|
} from 'semantic-ui-react';
|
|
13
18
|
import _ from 'underscore';
|
|
14
|
-
import i18n from '../i18n/i18n';
|
|
15
19
|
import FileUpload from './FileUpload';
|
|
20
|
+
import FileUploadStatus from './FileUploadStatus';
|
|
21
|
+
import FileUploadProgress from './FileUploadProgress';
|
|
22
|
+
import i18n from '../i18n/i18n';
|
|
16
23
|
import ModalContext from '../context/ModalContext';
|
|
17
|
-
import Toaster from './Toaster';
|
|
18
24
|
|
|
19
25
|
type Props = {
|
|
20
26
|
/**
|
|
21
|
-
*
|
|
27
|
+
* If <code>true</code>, the modal will close once the upload has completed.
|
|
22
28
|
*/
|
|
23
|
-
|
|
29
|
+
closeOnComplete?: boolean,
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
|
-
*
|
|
32
|
+
* Component to render within the modal.
|
|
27
33
|
*/
|
|
28
|
-
|
|
34
|
+
itemComponent: ComponentType<any>,
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
|
-
*
|
|
37
|
+
* Callback fired when a file is added.
|
|
32
38
|
*/
|
|
33
|
-
|
|
39
|
+
onAddFile: (file: File) => any,
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
|
-
* Callback fired when
|
|
42
|
+
* Callback fired when the close button is clicked.
|
|
37
43
|
*/
|
|
38
|
-
|
|
44
|
+
onClose: () => void,
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
|
-
* Callback fired when the
|
|
47
|
+
* Callback fired when the save button is clicked. See <code>strategy</code> prop.
|
|
42
48
|
*/
|
|
43
|
-
|
|
49
|
+
onSave: (items: Array<any>) => Promise<any>,
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
*
|
|
52
|
+
* An object with keys containing the names of properties that are required.
|
|
47
53
|
*/
|
|
48
|
-
|
|
54
|
+
required?: { [key: string]: string },
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
|
-
*
|
|
57
|
+
* If <code>true</code>, a full page loader will display while uploading is in progress.
|
|
52
58
|
*/
|
|
53
|
-
|
|
59
|
+
showPageLoader?: boolean,
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
|
-
*
|
|
62
|
+
* The upload strategy to use. If <code>batch</code>, we'll execute one <code>onSave</code> request with each item
|
|
63
|
+
* as an array in the body. If <code>single</code>, we'll execute an <code>onSave</code> request for each item.
|
|
57
64
|
*/
|
|
58
|
-
|
|
65
|
+
strategy?: string
|
|
59
66
|
};
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
saving: boolean
|
|
68
|
+
const Strategy = {
|
|
69
|
+
batch: 'batch',
|
|
70
|
+
single: 'single'
|
|
65
71
|
};
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const Status = {
|
|
74
|
+
pending: 'pending',
|
|
75
|
+
processing: 'processing',
|
|
76
|
+
complete: 'complete',
|
|
77
|
+
error: 'error'
|
|
78
|
+
};
|
|
68
79
|
|
|
69
80
|
/**
|
|
70
81
|
* The <code>FileUploadModal</code> is a convenience wrapper for the <code>FileUpload</code> component, allowing
|
|
71
82
|
* it to render in a modal.
|
|
72
83
|
*/
|
|
73
|
-
|
|
84
|
+
const FileUploadModal: ComponentType<any> = (props: Props) => {
|
|
85
|
+
const [items, setItems] = useState([]);
|
|
86
|
+
const [uploadCount, setUploadCount] = useState(0);
|
|
87
|
+
const [uploading, setUploading] = useState(false);
|
|
88
|
+
const [statuses, setStatuses] = useState({});
|
|
89
|
+
|
|
74
90
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* @param props
|
|
91
|
+
* Sets the <code>hasErrors</code> value to <code>true</code> if at least one item on the state contains errors.
|
|
78
92
|
*/
|
|
79
|
-
|
|
80
|
-
super(props);
|
|
81
|
-
|
|
82
|
-
this.state = this.getInitialState();
|
|
83
|
-
}
|
|
93
|
+
const hasErrors = useMemo(() => !!_.find(items, (item) => !_.isEmpty(item.errors)), [items]);
|
|
84
94
|
|
|
85
95
|
/**
|
|
86
|
-
*
|
|
96
|
+
* Calls the <code>onAddFile</code> prop for each item in the passed collection of files and adds them
|
|
97
|
+
* to the items on the state.
|
|
87
98
|
*
|
|
88
|
-
* @
|
|
99
|
+
* @type {function(*): void}
|
|
89
100
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
101
|
+
const onAddFiles = useCallback((files) => (
|
|
102
|
+
setItems((prevItems) => [
|
|
103
|
+
...prevItems,
|
|
104
|
+
..._.map(files, props.onAddFile)
|
|
105
|
+
])
|
|
106
|
+
), []);
|
|
97
107
|
|
|
98
108
|
/**
|
|
99
|
-
*
|
|
109
|
+
* Updates the passed association for the passed item.
|
|
100
110
|
*
|
|
101
|
-
* @
|
|
111
|
+
* @type {function(*, string, string, *): void}
|
|
102
112
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const onAssociationInputChange = useCallback((item: any, idAttribute: string, attribute: string, value: any) => (
|
|
114
|
+
setItems((prevItems) => _.map(prevItems, (i) => (i !== item ? i : {
|
|
115
|
+
...i,
|
|
116
|
+
[idAttribute]: value.id,
|
|
117
|
+
[attribute]: value,
|
|
118
|
+
errors: _.without(item.errors, idAttribute)
|
|
119
|
+
})))
|
|
120
|
+
), []);
|
|
106
121
|
|
|
107
122
|
/**
|
|
108
|
-
*
|
|
109
|
-
* This component calls the onAddFiles prop to transform the items stored in the state.
|
|
123
|
+
* Calls the <code>onSave</code> prop with the current list of items.
|
|
110
124
|
*
|
|
111
|
-
* @
|
|
125
|
+
* @type {function(): *}
|
|
112
126
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
items
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
]
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
127
|
+
const onBatchUpload = useCallback(() => (
|
|
128
|
+
props
|
|
129
|
+
.onSave(items)
|
|
130
|
+
.then(() => setUploadCount(1))
|
|
131
|
+
), [items]);
|
|
121
132
|
|
|
122
133
|
/**
|
|
123
|
-
*
|
|
134
|
+
* Sets the uploading state <code>false</code> and calls the <code>onClose</code> prop if necessary.
|
|
124
135
|
*
|
|
125
|
-
* @
|
|
126
|
-
* @param idAttribute
|
|
127
|
-
* @param attribute
|
|
128
|
-
* @param value
|
|
136
|
+
* @type {(function(): void)|*}
|
|
129
137
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
items: _.map(state.items, (i) => (i !== item ? i : ({
|
|
133
|
-
...i,
|
|
134
|
-
[idAttribute]: value.id,
|
|
135
|
-
[attribute]: value,
|
|
136
|
-
errors: _.without(item.errors, idAttribute)
|
|
137
|
-
})))
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
138
|
+
const onComplete = useCallback(() => {
|
|
139
|
+
setUploading(false);
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
*/
|
|
144
|
-
onClose() {
|
|
145
|
-
if (this.props.onClose) {
|
|
146
|
-
this.props.onClose();
|
|
147
|
-
} else {
|
|
148
|
-
this.setState(this.getInitialState());
|
|
141
|
+
if (props.closeOnComplete) {
|
|
142
|
+
props.onClose();
|
|
149
143
|
}
|
|
150
|
-
}
|
|
144
|
+
}, [props.closeOnComplete, props.onClose]);
|
|
151
145
|
|
|
152
146
|
/**
|
|
153
147
|
* Deletes the passed item from the state.
|
|
154
148
|
*
|
|
155
|
-
* @
|
|
149
|
+
* @type {function(*): void}
|
|
156
150
|
*/
|
|
157
|
-
onDelete(item
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
151
|
+
const onDelete = useCallback((item) => (
|
|
152
|
+
setItems((prevItems) => _.filter(prevItems, (i) => i !== item))
|
|
153
|
+
), []);
|
|
162
154
|
|
|
163
155
|
/**
|
|
164
|
-
*
|
|
156
|
+
* Sets the status for the item at the passed index.
|
|
157
|
+
*
|
|
158
|
+
* @type {function(*, *): void}
|
|
165
159
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}));
|
|
170
|
-
}
|
|
160
|
+
const setStatus = useCallback((index, status) => (
|
|
161
|
+
setStatuses((prevStatuses) => ({ ...prevStatuses, [index]: status }))
|
|
162
|
+
));
|
|
171
163
|
|
|
172
164
|
/**
|
|
173
|
-
*
|
|
165
|
+
* Iterates of the list of items and sequentially calls the <code>onSave</code> prop for each.
|
|
166
|
+
*
|
|
167
|
+
* @type {function(): Promise<unknown>}
|
|
174
168
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
const onSingleUpload = useCallback(async () => {
|
|
170
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
171
|
+
const item = items[i];
|
|
172
|
+
|
|
173
|
+
// Update the status for the item
|
|
174
|
+
setStatus(i, Status.processing);
|
|
175
|
+
|
|
176
|
+
let error = false;
|
|
177
|
+
|
|
178
|
+
// Do the upload
|
|
179
|
+
try {
|
|
180
|
+
// eslint-disable-next-line no-await-in-loop
|
|
181
|
+
await props.onSave(item);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
error = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update the status for the item
|
|
187
|
+
if (error) {
|
|
188
|
+
setStatus(i, Status.error);
|
|
189
|
+
} else {
|
|
190
|
+
setStatus(i, Status.complete);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Update the upload count
|
|
194
|
+
setUploadCount((prevCount) => prevCount + 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return Promise.resolve();
|
|
198
|
+
}, [items, props.onSave]);
|
|
180
199
|
|
|
181
200
|
/**
|
|
182
201
|
* Updates the text value for the passed item.
|
|
183
202
|
*
|
|
184
|
-
* @
|
|
185
|
-
* @param attribute
|
|
186
|
-
* @param e
|
|
187
|
-
* @param value
|
|
203
|
+
* @type {function(*, string, Event, {value: *}): void}
|
|
188
204
|
*/
|
|
189
|
-
onTextInputChange(item: any, attribute: string, e: Event, { value }: { value: any })
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}));
|
|
197
|
-
}
|
|
205
|
+
const onTextInputChange = useCallback((item: any, attribute: string, e: Event, { value }: { value: any }) => (
|
|
206
|
+
setItems((prevItems) => _.map(prevItems, (i) => (i !== item ? i : {
|
|
207
|
+
...i,
|
|
208
|
+
[attribute]: value,
|
|
209
|
+
errors: _.without(item.errors, attribute)
|
|
210
|
+
})))
|
|
211
|
+
), []);
|
|
198
212
|
|
|
199
213
|
/**
|
|
200
|
-
* Updates the passed item with the passed
|
|
214
|
+
* Updates the passed item with the passed object of attributes.
|
|
201
215
|
*
|
|
202
|
-
* @
|
|
203
|
-
* @param props
|
|
216
|
+
* @type {function(*, *): void}
|
|
204
217
|
*/
|
|
205
|
-
onUpdate(item
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
...i,
|
|
209
|
-
...props,
|
|
210
|
-
errors: _.without(item.errors, _.keys(props))
|
|
211
|
-
})))
|
|
212
|
-
}));
|
|
213
|
-
}
|
|
218
|
+
const onUpdate = useCallback((item, attributes) => (
|
|
219
|
+
setItems((prevItems) => _.map(prevItems, (i) => (i !== item ? i : { ...i, ...attributes })))
|
|
220
|
+
), []);
|
|
214
221
|
|
|
215
222
|
/**
|
|
216
|
-
*
|
|
223
|
+
* Validates the passed item.
|
|
217
224
|
*
|
|
218
|
-
* @
|
|
225
|
+
* @type {function(*): *&{errors: []}}
|
|
219
226
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<>
|
|
223
|
-
{ this.props.includeButton && (
|
|
224
|
-
<Button
|
|
225
|
-
content={this.props.button}
|
|
226
|
-
icon='cloud upload'
|
|
227
|
-
onClick={() => this.setState({ modal: true })}
|
|
228
|
-
primary
|
|
229
|
-
/>
|
|
230
|
-
)}
|
|
231
|
-
{ this.state.modal && (
|
|
232
|
-
<ModalContext.Consumer>
|
|
233
|
-
{ (mountNode) => (
|
|
234
|
-
<Modal
|
|
235
|
-
centered={false}
|
|
236
|
-
className='file-upload-modal'
|
|
237
|
-
mountNode={mountNode}
|
|
238
|
-
open
|
|
239
|
-
>
|
|
240
|
-
<Dimmer
|
|
241
|
-
active={this.state.saving}
|
|
242
|
-
inverted
|
|
243
|
-
>
|
|
244
|
-
<Loader
|
|
245
|
-
content={i18n.t('FileUploadModal.loader')}
|
|
246
|
-
/>
|
|
247
|
-
</Dimmer>
|
|
248
|
-
{ this.renderErrors() }
|
|
249
|
-
<Modal.Header
|
|
250
|
-
content={this.props.title || i18n.t('FileUploadModal.title')}
|
|
251
|
-
/>
|
|
252
|
-
<Modal.Content>
|
|
253
|
-
<FileUpload
|
|
254
|
-
onFilesAdded={this.onAddFiles.bind(this)}
|
|
255
|
-
/>
|
|
256
|
-
{ this.renderItems() }
|
|
257
|
-
</Modal.Content>
|
|
258
|
-
<Modal.Actions>
|
|
259
|
-
<Button
|
|
260
|
-
content={i18n.t('Common.buttons.save')}
|
|
261
|
-
disabled={!(this.state.items && this.state.items.length)}
|
|
262
|
-
primary
|
|
263
|
-
onClick={this.onSave.bind(this)}
|
|
264
|
-
/>
|
|
265
|
-
<Button
|
|
266
|
-
basic
|
|
267
|
-
content={i18n.t('Common.buttons.cancel')}
|
|
268
|
-
onClick={this.onClose.bind(this)}
|
|
269
|
-
/>
|
|
270
|
-
</Modal.Actions>
|
|
271
|
-
</Modal>
|
|
272
|
-
)}
|
|
273
|
-
</ModalContext.Consumer>
|
|
274
|
-
)}
|
|
275
|
-
</>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
227
|
+
const validateItem = useCallback((item) => {
|
|
228
|
+
const errors = [];
|
|
278
229
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
* @returns {null|*}
|
|
283
|
-
*/
|
|
284
|
-
renderErrors() {
|
|
285
|
-
if (!this.hasErrors()) {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
230
|
+
_.each(_.keys(props.required), (key) => {
|
|
231
|
+
const value = item[key];
|
|
232
|
+
let invalid;
|
|
288
233
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
234
|
+
if (_.isNumber(value)) {
|
|
235
|
+
invalid = _.isEmpty(value.toString());
|
|
236
|
+
} else {
|
|
237
|
+
invalid = _.isEmpty(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (invalid) {
|
|
241
|
+
errors.push(key);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...item,
|
|
247
|
+
errors
|
|
248
|
+
};
|
|
249
|
+
}, [props.required]);
|
|
304
250
|
|
|
305
251
|
/**
|
|
306
|
-
*
|
|
252
|
+
* Validates the items on the state.
|
|
307
253
|
*
|
|
308
|
-
* @
|
|
309
|
-
*
|
|
310
|
-
* @returns {*}
|
|
254
|
+
* @type {function(): Promise<void>}
|
|
311
255
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
256
|
+
const onValidate = useCallback(() => {
|
|
257
|
+
let error = false;
|
|
314
258
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
326
|
-
}
|
|
259
|
+
setItems((prevItems) => _.map(prevItems, (item) => {
|
|
260
|
+
const valid = validateItem(item);
|
|
261
|
+
|
|
262
|
+
if (!_.isEmpty(item.errors)) {
|
|
263
|
+
error = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return valid;
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
return error ? Promise.reject() : Promise.resolve();
|
|
270
|
+
}, [validateItem]);
|
|
327
271
|
|
|
328
272
|
/**
|
|
329
|
-
*
|
|
273
|
+
* Updates the passed item with the passed props.
|
|
330
274
|
*
|
|
331
|
-
* @
|
|
275
|
+
* @type {(function(): void)|*}
|
|
332
276
|
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</Item.Group>
|
|
347
|
-
);
|
|
348
|
-
}
|
|
277
|
+
const onUpload = useCallback(() => {
|
|
278
|
+
// Set the uploading indicator
|
|
279
|
+
setUploading(true);
|
|
280
|
+
|
|
281
|
+
// Upload the files
|
|
282
|
+
onValidate()
|
|
283
|
+
.then(() => (
|
|
284
|
+
props.strategy === Strategy.batch
|
|
285
|
+
? onBatchUpload()
|
|
286
|
+
: onSingleUpload()
|
|
287
|
+
))
|
|
288
|
+
.finally(onComplete);
|
|
289
|
+
}, [onBatchUpload, onComplete, onSingleUpload, onValidate, props.strategy]);
|
|
349
290
|
|
|
350
291
|
/**
|
|
351
|
-
* Renders the
|
|
352
|
-
*
|
|
353
|
-
* @param item
|
|
354
|
-
* @param index
|
|
292
|
+
* Renders the error message for the passed item.
|
|
355
293
|
*
|
|
356
|
-
* @
|
|
294
|
+
* @type {(function(*, *): (null|*))|*}
|
|
357
295
|
*/
|
|
358
|
-
renderMessageItem(item
|
|
296
|
+
const renderMessageItem = useCallback((item, index) => {
|
|
359
297
|
if (_.isEmpty(item.errors)) {
|
|
360
298
|
return null;
|
|
361
299
|
}
|
|
362
300
|
|
|
363
|
-
const filename = !_.isEmpty(item.name) ? item.name :
|
|
364
|
-
const fields = _.map(item.errors, (e) =>
|
|
301
|
+
const filename = !_.isEmpty(item.name) ? item.name : index;
|
|
302
|
+
const fields = _.map(item.errors, (e) => props.required[e]).join(', ');
|
|
365
303
|
|
|
366
304
|
return (
|
|
367
305
|
<Message.Item
|
|
@@ -369,78 +307,115 @@ class FileUploadModal extends Component<Props, State> {
|
|
|
369
307
|
key={index}
|
|
370
308
|
/>
|
|
371
309
|
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Saves the uploaded items.
|
|
376
|
-
*/
|
|
377
|
-
save() {
|
|
378
|
-
if (this.hasErrors()) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this.setState({ saving: true }, () => {
|
|
383
|
-
this.props
|
|
384
|
-
.onSave(this.state.items)
|
|
385
|
-
.then(this.onClose.bind(this));
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Validates the list of items.
|
|
391
|
-
*/
|
|
392
|
-
validate() {
|
|
393
|
-
this.setState((state) => {
|
|
394
|
-
const items = _.map(state.items, this.validateItem.bind(this));
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
items,
|
|
398
|
-
saving: !_.find(items, (item) => !_.isEmpty(item.errors))
|
|
399
|
-
};
|
|
400
|
-
}, this.save.bind(this));
|
|
401
|
-
}
|
|
310
|
+
}, []);
|
|
402
311
|
|
|
403
312
|
/**
|
|
404
|
-
*
|
|
313
|
+
* Memoization and case correction for the <code>itemComponent</code> prop.
|
|
405
314
|
*
|
|
406
|
-
* @
|
|
407
|
-
*
|
|
408
|
-
* @returns {{errors: []}}
|
|
315
|
+
* @type {React$AbstractComponent<*, *>}
|
|
409
316
|
*/
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
FileUploadModal.
|
|
433
|
-
|
|
317
|
+
const UploadItem = useMemo(() => props.itemComponent, [props.itemComponent]);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<ModalContext.Consumer>
|
|
321
|
+
{ (mountNode) => (
|
|
322
|
+
<Modal
|
|
323
|
+
centered={false}
|
|
324
|
+
className='serial-upload-modal'
|
|
325
|
+
mountNode={mountNode}
|
|
326
|
+
open
|
|
327
|
+
>
|
|
328
|
+
{ props.showPageLoader && (
|
|
329
|
+
<Dimmer
|
|
330
|
+
active={uploading}
|
|
331
|
+
inverted
|
|
332
|
+
>
|
|
333
|
+
<Loader
|
|
334
|
+
content={i18n.t('FileUploadModal.loader')}
|
|
335
|
+
/>
|
|
336
|
+
</Dimmer>
|
|
337
|
+
)}
|
|
338
|
+
<Modal.Header>
|
|
339
|
+
{ props.strategy === Strategy.batch && i18n.t('FileUploadModal.title') }
|
|
340
|
+
{ props.strategy === Strategy.single && (
|
|
341
|
+
<FileUploadProgress
|
|
342
|
+
completed={uploadCount}
|
|
343
|
+
total={items.length}
|
|
344
|
+
uploading={uploading}
|
|
345
|
+
/>
|
|
346
|
+
)}
|
|
347
|
+
</Modal.Header>
|
|
348
|
+
<Modal.Content
|
|
349
|
+
scrolling
|
|
350
|
+
>
|
|
351
|
+
{ hasErrors && (
|
|
352
|
+
<Message
|
|
353
|
+
error
|
|
354
|
+
>
|
|
355
|
+
<Message.Header
|
|
356
|
+
content={i18n.t('FileUploadModal.errors.header')}
|
|
357
|
+
/>
|
|
358
|
+
<Message.List>
|
|
359
|
+
{ _.map(items, renderMessageItem) }
|
|
360
|
+
</Message.List>
|
|
361
|
+
</Message>
|
|
362
|
+
)}
|
|
363
|
+
<FileUpload
|
|
364
|
+
onFilesAdded={onAddFiles}
|
|
365
|
+
/>
|
|
366
|
+
<Item.Group
|
|
367
|
+
as={Form}
|
|
368
|
+
divided
|
|
369
|
+
noValidate
|
|
370
|
+
relaxed='very'
|
|
371
|
+
>
|
|
372
|
+
{ _.map(items, (item, index) => (
|
|
373
|
+
<UploadItem
|
|
374
|
+
isError={(key) => _.contains(item.errors, key)}
|
|
375
|
+
isRequired={(key) => !!(props.required && props.required[key])}
|
|
376
|
+
item={item}
|
|
377
|
+
key={index}
|
|
378
|
+
onAssociationInputChange={onAssociationInputChange.bind(this, item)}
|
|
379
|
+
onDelete={onDelete.bind(this, item)}
|
|
380
|
+
onTextInputChange={onTextInputChange.bind(this, item)}
|
|
381
|
+
onUpdate={onUpdate.bind(this, item)}
|
|
382
|
+
>
|
|
383
|
+
{ props.strategy === Strategy.single && (
|
|
384
|
+
<FileUploadStatus
|
|
385
|
+
status={statuses[index]}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
</UploadItem>
|
|
389
|
+
))}
|
|
390
|
+
</Item.Group>
|
|
391
|
+
</Modal.Content>
|
|
392
|
+
<Modal.Actions>
|
|
393
|
+
<Button
|
|
394
|
+
content={i18n.t('Common.buttons.upload')}
|
|
395
|
+
disabled={uploading || uploadCount > 0 || _.isEmpty(items)}
|
|
396
|
+
icon='cloud upload'
|
|
397
|
+
loading={uploading && !props.showPageLoader}
|
|
398
|
+
onClick={onUpload}
|
|
399
|
+
primary
|
|
400
|
+
/>
|
|
401
|
+
<Button
|
|
402
|
+
content={uploadCount > 0
|
|
403
|
+
? i18n.t('Common.buttons.close')
|
|
404
|
+
: i18n.t('Common.buttons.cancel')}
|
|
405
|
+
disabled={uploading}
|
|
406
|
+
onClick={props.onClose}
|
|
407
|
+
/>
|
|
408
|
+
</Modal.Actions>
|
|
409
|
+
</Modal>
|
|
410
|
+
)}
|
|
411
|
+
</ModalContext.Consumer>
|
|
412
|
+
);
|
|
434
413
|
};
|
|
435
414
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
onAssociationInputChange: (idKey: string, valueKey: string, item: any) => void,
|
|
441
|
-
onDelete: (item: any) => void,
|
|
442
|
-
onTextInputChange: (item: any, value: any) => void,
|
|
443
|
-
onUpdate: (item: any, props: any) => void
|
|
415
|
+
FileUploadModal.defaultProps = {
|
|
416
|
+
closeOnComplete: true,
|
|
417
|
+
strategy: Strategy.batch,
|
|
418
|
+
showPageLoader: true
|
|
444
419
|
};
|
|
445
420
|
|
|
446
421
|
export default FileUploadModal;
|