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