@performant-software/semantic-components 0.5.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.
Files changed (218) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/build/index.js +2 -0
  4. package/build/index.js.map +1 -0
  5. package/build/main.css +786 -0
  6. package/index.js +1 -0
  7. package/package.json +37 -0
  8. package/src/components/AccordionDataList.css +8 -0
  9. package/src/components/AccordionDataList.js +224 -0
  10. package/src/components/AccordionList.css +27 -0
  11. package/src/components/AccordionList.js +596 -0
  12. package/src/components/AccordionSelector.css +3 -0
  13. package/src/components/AccordionSelector.js +359 -0
  14. package/src/components/ArrowButtons.css +4 -0
  15. package/src/components/ArrowButtons.js +38 -0
  16. package/src/components/AssociatedDropdown.css +44 -0
  17. package/src/components/AssociatedDropdown.js +338 -0
  18. package/src/components/BooleanIcon.css +0 -0
  19. package/src/components/BooleanIcon.js +33 -0
  20. package/src/components/CancelButton.css +0 -0
  21. package/src/components/CancelButton.js +25 -0
  22. package/src/components/ColorButton.css +4 -0
  23. package/src/components/ColorButton.js +34 -0
  24. package/src/components/ColorPickerModal.css +3 -0
  25. package/src/components/ColorPickerModal.js +77 -0
  26. package/src/components/ColumnResize.css +9 -0
  27. package/src/components/ColumnResize.js +20 -0
  28. package/src/components/DataList.css +0 -0
  29. package/src/components/DataList.js +531 -0
  30. package/src/components/DataTable.css +43 -0
  31. package/src/components/DataTable.js +596 -0
  32. package/src/components/DataTableColumnSelector.css +10 -0
  33. package/src/components/DataTableColumnSelector.js +146 -0
  34. package/src/components/DateInput.css +6 -0
  35. package/src/components/DateInput.js +58 -0
  36. package/src/components/DatePicker.css +72 -0
  37. package/src/components/DatePicker.js +81 -0
  38. package/src/components/DescriptorField.css +0 -0
  39. package/src/components/DescriptorField.js +42 -0
  40. package/src/components/DownloadButton.css +0 -0
  41. package/src/components/DownloadButton.js +23 -0
  42. package/src/components/Draggable.css +0 -0
  43. package/src/components/Draggable.js +94 -0
  44. package/src/components/DropdownButton.css +3 -0
  45. package/src/components/DropdownButton.js +65 -0
  46. package/src/components/DropdownMenu.css +0 -0
  47. package/src/components/DropdownMenu.js +68 -0
  48. package/src/components/EditModal.css +8 -0
  49. package/src/components/EditModal.js +99 -0
  50. package/src/components/EditPage.css +7 -0
  51. package/src/components/EditPage.js +249 -0
  52. package/src/components/EmbeddedList.css +7 -0
  53. package/src/components/EmbeddedList.js +278 -0
  54. package/src/components/FileInputButton.css +0 -0
  55. package/src/components/FileInputButton.js +54 -0
  56. package/src/components/FileUpload.css +31 -0
  57. package/src/components/FileUpload.js +188 -0
  58. package/src/components/FileUploadModal.css +0 -0
  59. package/src/components/FileUploadModal.js +408 -0
  60. package/src/components/FuzzyDate.css +8 -0
  61. package/src/components/FuzzyDate.js +575 -0
  62. package/src/components/GoogleMap.css +0 -0
  63. package/src/components/GoogleMap.js +105 -0
  64. package/src/components/GooglePlacesSearch.css +0 -0
  65. package/src/components/GooglePlacesSearch.js +43 -0
  66. package/src/components/HorizontalCards.css +50 -0
  67. package/src/components/HorizontalCards.js +226 -0
  68. package/src/components/ItemCollection.css +3 -0
  69. package/src/components/ItemCollection.js +159 -0
  70. package/src/components/ItemList.css +0 -0
  71. package/src/components/ItemList.js +126 -0
  72. package/src/components/Items.css +19 -0
  73. package/src/components/Items.js +365 -0
  74. package/src/components/ItemsToggle.css +0 -0
  75. package/src/components/ItemsToggle.js +168 -0
  76. package/src/components/KeyboardField.css +4 -0
  77. package/src/components/KeyboardField.js +147 -0
  78. package/src/components/LazyDocument.css +21 -0
  79. package/src/components/LazyDocument.js +113 -0
  80. package/src/components/LazyImage.css +21 -0
  81. package/src/components/LazyImage.js +119 -0
  82. package/src/components/LazyVideo.css +21 -0
  83. package/src/components/LazyVideo.js +131 -0
  84. package/src/components/LinkButton.css +8 -0
  85. package/src/components/LinkButton.js +23 -0
  86. package/src/components/LinkLabel.css +8 -0
  87. package/src/components/LinkLabel.js +29 -0
  88. package/src/components/List.css +8 -0
  89. package/src/components/List.js +761 -0
  90. package/src/components/ListFilters.css +0 -0
  91. package/src/components/ListFilters.js +408 -0
  92. package/src/components/ListLoader.css +8 -0
  93. package/src/components/ListLoader.js +32 -0
  94. package/src/components/ListTable.css +3 -0
  95. package/src/components/ListTable.js +86 -0
  96. package/src/components/LoginModal.css +7 -0
  97. package/src/components/LoginModal.js +102 -0
  98. package/src/components/MasonryGrid.css +48 -0
  99. package/src/components/MasonryGrid.js +202 -0
  100. package/src/components/MediaGallery.css +37 -0
  101. package/src/components/MediaGallery.js +148 -0
  102. package/src/components/MediaGrid.css +72 -0
  103. package/src/components/MediaGrid.js +74 -0
  104. package/src/components/MediaList.css +3 -0
  105. package/src/components/MediaList.js +98 -0
  106. package/src/components/ModalDropdown.css +11 -0
  107. package/src/components/ModalDropdown.js +84 -0
  108. package/src/components/NestedAccordion.css +41 -0
  109. package/src/components/NestedAccordion.js +276 -0
  110. package/src/components/PhotoViewer.css +3 -0
  111. package/src/components/PhotoViewer.js +36 -0
  112. package/src/components/PlayButton.css +3 -0
  113. package/src/components/PlayButton.js +37 -0
  114. package/src/components/RemoteDropdown.css +13 -0
  115. package/src/components/RemoteDropdown.js +368 -0
  116. package/src/components/SaveButton.css +0 -0
  117. package/src/components/SaveButton.js +31 -0
  118. package/src/components/Section.css +0 -0
  119. package/src/components/Section.js +41 -0
  120. package/src/components/Selectize.css +11 -0
  121. package/src/components/Selectize.js +297 -0
  122. package/src/components/SelectizeHeader.css +3 -0
  123. package/src/components/SelectizeHeader.js +40 -0
  124. package/src/components/TabbedModal.css +14 -0
  125. package/src/components/TabbedModal.js +165 -0
  126. package/src/components/TabsMenu.css +0 -0
  127. package/src/components/TabsMenu.js +35 -0
  128. package/src/components/TagsList.css +0 -0
  129. package/src/components/TagsList.js +43 -0
  130. package/src/components/Thumbnail.css +0 -0
  131. package/src/components/Thumbnail.js +47 -0
  132. package/src/components/Toaster.css +9 -0
  133. package/src/components/Toaster.js +73 -0
  134. package/src/components/VideoFrameSelector.css +3 -0
  135. package/src/components/VideoFrameSelector.js +148 -0
  136. package/src/components/VideoPlayer.css +3 -0
  137. package/src/components/VideoPlayer.js +55 -0
  138. package/src/components/VideoPlayerButton.css +17 -0
  139. package/src/components/VideoPlayerButton.js +17 -0
  140. package/src/components/ViewXML.css +0 -0
  141. package/src/components/ViewXML.js +72 -0
  142. package/src/i18n/en.json +204 -0
  143. package/src/i18n/i18n.js +24 -0
  144. package/src/index.js +76 -0
  145. package/types/components/AccordionDataList.js.flow +224 -0
  146. package/types/components/AccordionList.js.flow +596 -0
  147. package/types/components/AccordionSelector.js.flow +359 -0
  148. package/types/components/ArrowButtons.js.flow +38 -0
  149. package/types/components/AssociatedDropdown.js.flow +338 -0
  150. package/types/components/BooleanIcon.js.flow +33 -0
  151. package/types/components/CancelButton.js.flow +25 -0
  152. package/types/components/ColorButton.js.flow +34 -0
  153. package/types/components/ColorPickerModal.js.flow +77 -0
  154. package/types/components/ColumnResize.js.flow +20 -0
  155. package/types/components/DataList.js.flow +531 -0
  156. package/types/components/DataTable.js.flow +596 -0
  157. package/types/components/DataTableColumnSelector.js.flow +146 -0
  158. package/types/components/DataView.js.flow +125 -0
  159. package/types/components/DateInput.js.flow +58 -0
  160. package/types/components/DatePicker.js.flow +81 -0
  161. package/types/components/DescriptorField.js.flow +42 -0
  162. package/types/components/DownloadButton.js.flow +23 -0
  163. package/types/components/Draggable.js.flow +94 -0
  164. package/types/components/DropdownButton.js.flow +65 -0
  165. package/types/components/DropdownMenu.js.flow +68 -0
  166. package/types/components/EditModal.js.flow +99 -0
  167. package/types/components/EditPage.js.flow +249 -0
  168. package/types/components/EmbeddedList.js.flow +278 -0
  169. package/types/components/FileInputButton.js.flow +54 -0
  170. package/types/components/FileUpload.js.flow +188 -0
  171. package/types/components/FileUploadModal.js.flow +408 -0
  172. package/types/components/FuzzyDate.js.flow +575 -0
  173. package/types/components/GoogleMap.js.flow +105 -0
  174. package/types/components/GooglePlacesSearch.js.flow +43 -0
  175. package/types/components/HorizontalCards.js.flow +226 -0
  176. package/types/components/ItemCollection.js.flow +159 -0
  177. package/types/components/ItemList.js.flow +126 -0
  178. package/types/components/Items.js.flow +365 -0
  179. package/types/components/ItemsToggle.js.flow +168 -0
  180. package/types/components/KeyboardField.js.flow +147 -0
  181. package/types/components/LazyDocument.js.flow +113 -0
  182. package/types/components/LazyImage.js.flow +119 -0
  183. package/types/components/LazyVideo.js.flow +131 -0
  184. package/types/components/LinkButton.js.flow +23 -0
  185. package/types/components/LinkLabel.js.flow +29 -0
  186. package/types/components/List.js.flow +761 -0
  187. package/types/components/ListFilters.js.flow +408 -0
  188. package/types/components/ListLoader.js.flow +32 -0
  189. package/types/components/ListTable.js.flow +86 -0
  190. package/types/components/LoginModal.js.flow +102 -0
  191. package/types/components/MasonryGrid.js.flow +202 -0
  192. package/types/components/MediaGallery.js.flow +148 -0
  193. package/types/components/MediaGrid.js.flow +74 -0
  194. package/types/components/MediaList.js.flow +98 -0
  195. package/types/components/MenuBar.js.flow +77 -0
  196. package/types/components/MenuSidebar.js.flow +72 -0
  197. package/types/components/ModalDropdown.js.flow +84 -0
  198. package/types/components/NestedAccordion.js.flow +276 -0
  199. package/types/components/PhotoViewer.js.flow +36 -0
  200. package/types/components/PlayButton.js.flow +37 -0
  201. package/types/components/RemoteDropdown.js.flow +368 -0
  202. package/types/components/SaveButton.js.flow +31 -0
  203. package/types/components/Section.js.flow +41 -0
  204. package/types/components/Selectize.js.flow +297 -0
  205. package/types/components/SelectizeHeader.js.flow +40 -0
  206. package/types/components/TabbedModal.js.flow +165 -0
  207. package/types/components/TabsMenu.js.flow +35 -0
  208. package/types/components/TagsList.js.flow +43 -0
  209. package/types/components/Thumbnail.js.flow +47 -0
  210. package/types/components/Toaster.js.flow +73 -0
  211. package/types/components/VideoFrameSelector.js.flow +148 -0
  212. package/types/components/VideoPlayer.js.flow +55 -0
  213. package/types/components/VideoPlayerButton.js.flow +17 -0
  214. package/types/components/ViewXML.js.flow +72 -0
  215. package/types/hooks/Imageable.js.flow +54 -0
  216. package/types/i18n/i18n.js.flow +24 -0
  217. package/types/index.js.flow +78 -0
  218. package/webpack.config.js +3 -0
@@ -0,0 +1,188 @@
1
+ // @flow
2
+
3
+ import React, { Component, createRef } from 'react';
4
+ import { Icon, Message } from 'semantic-ui-react';
5
+ import { Trans } from 'react-i18next';
6
+ import _ from 'underscore';
7
+ import i18n from '../i18n/i18n';
8
+ import './FileUpload.css';
9
+
10
+ type Props = {
11
+ fileTypes?: Array<string>,
12
+ maxSize?: number,
13
+ onFilesAdded: (files: Array<File>) => void,
14
+ };
15
+
16
+ type State = {
17
+ errors: ?Array<string>
18
+ };
19
+
20
+ type FileEvent = {
21
+ dataTransfer: HTMLInputElement,
22
+ preventDefault: () => void,
23
+ target: HTMLInputElement
24
+ };
25
+
26
+ class FileUpload extends Component<Props, State> {
27
+ fileInput: any;
28
+ filePattern: any;
29
+
30
+ /**
31
+ * Constructs a new FileUpload component.
32
+ *
33
+ * @param props
34
+ */
35
+ constructor(props: Props) {
36
+ super(props);
37
+
38
+ this.state = {
39
+ errors: null
40
+ };
41
+
42
+ this.fileInput = createRef();
43
+ this.filePattern = this.props.fileTypes && new RegExp(`(.*?)\\.${this.props.fileTypes.join('|')}$`);
44
+ }
45
+
46
+ /**
47
+ * Triggers the file input click.
48
+ */
49
+ onClickDropzone() {
50
+ this.fileInput.current.click();
51
+ }
52
+
53
+ /**
54
+ * Adds the selected files.
55
+ *
56
+ * @param e
57
+ */
58
+ onDropFiles(e: FileEvent) {
59
+ e.preventDefault();
60
+ const files = this.toArray(e.dataTransfer.files);
61
+ const validFiles = this.validate(files);
62
+ if (validFiles.length === files.length) {
63
+ this.props.onFilesAdded(files);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Adds the selected files.
69
+ *
70
+ * @param e
71
+ */
72
+ onFilesAdded(e: FileEvent) {
73
+ e.preventDefault();
74
+ const files = this.toArray(e.target.files);
75
+ const validFiles = this.validate(files);
76
+ if (validFiles.length === files.length) {
77
+ this.props.onFilesAdded(files);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Renders the FileUpload component.
83
+ */
84
+ render() {
85
+ return (
86
+ <div
87
+ className='file-upload'
88
+ >
89
+ <Message
90
+ error
91
+ header={i18n.t('Common.errors.title')}
92
+ hidden={!(this.state.errors && this.state.errors.length)}
93
+ visible={this.state.errors && this.state.errors.length}
94
+ onDismiss={() => this.setState({ errors: null })}
95
+ list={this.state.errors}
96
+ />
97
+ <div
98
+ className='file-dropzone'
99
+ onClick={this.onClickDropzone.bind(this)}
100
+ onDragLeave={(e) => { e.preventDefault(); }}
101
+ onDragOver={(e) => { e.preventDefault(); }}
102
+ onDrop={this.onDropFiles.bind(this)}
103
+ onKeyDown={() => {}}
104
+ role='button'
105
+ tabIndex={-1}
106
+ >
107
+ <Icon
108
+ color='blue'
109
+ name='cloud upload'
110
+ size='large'
111
+ />
112
+ <div className='file-dropzone-text'>
113
+ <Trans i18nKey='FileUpload.add'>
114
+ <span className='link-text'>Add files</span>
115
+ &nbsp;or drop files here
116
+ </Trans>
117
+ </div>
118
+ <input
119
+ ref={this.fileInput}
120
+ className='file-input'
121
+ type='file'
122
+ multiple
123
+ onChange={this.onFilesAdded.bind(this)}
124
+ />
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Converts the passed file list to an array.
132
+ *
133
+ * @param fileList
134
+ *
135
+ * @returns {[]}
136
+ */
137
+ toArray(fileList: any) {
138
+ const array = [];
139
+
140
+ for (let i = 0; i < fileList.length; i += 1) {
141
+ array.push(fileList.item(i));
142
+ }
143
+
144
+ return array;
145
+ }
146
+
147
+ /**
148
+ * Validates the passed list of files.
149
+ *
150
+ * @param files
151
+ *
152
+ * @returns {[]|*}
153
+ */
154
+ validate(files: Array<File>) {
155
+ if (!this.props.maxSize && !this.props.fileTypes) {
156
+ return files;
157
+ }
158
+
159
+ const validFiles = [];
160
+ const errors = [];
161
+
162
+ _.each(files, (file) => {
163
+ let valid = true;
164
+
165
+ // Files are invalid if they exceed the maximum size
166
+ if (this.props.maxSize && file.size > this.props.maxSize) {
167
+ valid = false;
168
+ errors.push(i18n.t('FileUpload.errors.maxSize', { name: file.name }));
169
+ }
170
+
171
+ // Files are invalid if they do not match the acceptable types
172
+ if (this.filePattern && !file.name.match(this.filePattern)) {
173
+ valid = false;
174
+ errors.push(i18n.t('FileUpload.errors.fileType', { name: file.name, type: /[^.]+$/.exec(file.name) }));
175
+ }
176
+
177
+ if (valid) {
178
+ validFiles.push(file);
179
+ }
180
+ });
181
+
182
+ this.setState({ errors });
183
+
184
+ return validFiles;
185
+ }
186
+ }
187
+
188
+ export default FileUpload;
@@ -0,0 +1,408 @@
1
+ // @flow
2
+
3
+ import React, { Component, type ComponentType } from 'react';
4
+ import {
5
+ Button,
6
+ Dimmer,
7
+ Form,
8
+ Item,
9
+ Loader,
10
+ Message,
11
+ Modal
12
+ } from 'semantic-ui-react';
13
+ import _ from 'underscore';
14
+ import i18n from '../i18n/i18n';
15
+ import FileUpload from './FileUpload';
16
+ import Toaster from './Toaster';
17
+
18
+ type Props = {
19
+ button: string,
20
+ includeButton?: boolean,
21
+ itemComponent: ComponentType<any>,
22
+ onAddFile: (file: File) => any,
23
+ onClose?: () => void,
24
+ onSave: (items: Array<any>) => Promise<any>,
25
+ required: { [string]: string },
26
+ title?: string
27
+ };
28
+
29
+ type State = {
30
+ items: Array<any>,
31
+ modal: boolean,
32
+ saving: boolean
33
+ };
34
+
35
+ const LIST_DELIMITER = ', ';
36
+
37
+ class FileUploadModal extends Component<Props, State> {
38
+ static defaultProps: any;
39
+
40
+ /**
41
+ * Constructs a new FileUploadModal component.
42
+ *
43
+ * @param props
44
+ */
45
+ constructor(props: Props) {
46
+ super(props);
47
+
48
+ this.state = this.getInitialState();
49
+ }
50
+
51
+ /**
52
+ * Returns the initial component state.
53
+ *
54
+ * @returns {{saving: boolean, items: [], modal: boolean}}
55
+ */
56
+ getInitialState() {
57
+ return {
58
+ items: [],
59
+ modal: !this.props.includeButton,
60
+ saving: false
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Returns true if any of the items contain errors.
66
+ *
67
+ * @returns {boolean}
68
+ */
69
+ hasErrors() {
70
+ return !!_.find(this.state.items, (item) => !_.isEmpty(item.errors));
71
+ }
72
+
73
+ /**
74
+ * Adds the passed collection of files to the state. Typically files will be added as a property of another model.
75
+ * This component calls the onAddFiles prop to transform the items stored in the state.
76
+ *
77
+ * @param files
78
+ */
79
+ onAddFiles(files: Array<File>) {
80
+ this.setState((state) => ({
81
+ items: [
82
+ ...state.items,
83
+ ..._.map(files, this.props.onAddFile.bind(this))
84
+ ]
85
+ }));
86
+ }
87
+
88
+ /**
89
+ * Updates the passed association for the passed item.
90
+ *
91
+ * @param item
92
+ * @param idAttribute
93
+ * @param attribute
94
+ * @param value
95
+ */
96
+ onAssociationInputChange(item: any, idAttribute: string, attribute: string, value: any) {
97
+ this.setState((state) => ({
98
+ items: _.map(state.items, (i) => (i !== item ? i : ({
99
+ ...i,
100
+ [idAttribute]: value.id,
101
+ [attribute]: value,
102
+ errors: _.without(item.errors, idAttribute)
103
+ })))
104
+ }));
105
+ }
106
+
107
+ /**
108
+ * Resets the state or calls the onClose prop.
109
+ */
110
+ onClose() {
111
+ if (this.props.onClose) {
112
+ this.props.onClose();
113
+ } else {
114
+ this.setState(this.getInitialState());
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Deletes the passed item from the state.
120
+ *
121
+ * @param item
122
+ */
123
+ onDelete(item: any) {
124
+ this.setState((state) => ({
125
+ items: _.filter(state.items, (i) => i !== item)
126
+ }));
127
+ }
128
+
129
+ /**
130
+ * Clears the errors for all items in the state.
131
+ */
132
+ onDismissErrors() {
133
+ this.setState((state) => ({
134
+ items: _.map(state.items, (item) => _.omit(item, 'errors'))
135
+ }));
136
+ }
137
+
138
+ /**
139
+ * Validates the items and saves.
140
+ */
141
+ onSave() {
142
+ this.setState((state) => ({
143
+ items: _.map(state.items, this.validateItem.bind(this))
144
+ }), this.save.bind(this));
145
+ }
146
+
147
+ /**
148
+ * Updates the text value for the passed item.
149
+ *
150
+ * @param item
151
+ * @param attribute
152
+ * @param e
153
+ * @param value
154
+ */
155
+ onTextInputChange(item: any, attribute: string, e: Event, { value }: { value: any }) {
156
+ this.setState((state) => ({
157
+ items: _.map(state.items, (i) => (i !== item ? i : ({
158
+ ...i,
159
+ [attribute]: value,
160
+ errors: _.without(item.errors, attribute)
161
+ })))
162
+ }));
163
+ }
164
+
165
+ /**
166
+ * Updates the passed item with the passed props.
167
+ *
168
+ * @param item
169
+ * @param props
170
+ */
171
+ onUpdate(item: any, props: any) {
172
+ this.setState((state) => ({
173
+ items: _.map(state.items, (i) => (i !== item ? i : ({
174
+ ...i,
175
+ ...props,
176
+ errors: _.without(item.errors, _.keys(props))
177
+ })))
178
+ }));
179
+ }
180
+
181
+ /**
182
+ * Renders the FileUploadModal component.
183
+ *
184
+ * @returns {*}
185
+ */
186
+ render() {
187
+ return (
188
+ <>
189
+ { this.props.includeButton && (
190
+ <Button
191
+ content={this.props.button}
192
+ icon='cloud upload'
193
+ onClick={() => this.setState({ modal: true })}
194
+ primary
195
+ />
196
+ )}
197
+ { this.state.modal && (
198
+ <Modal
199
+ centered={false}
200
+ className='file-upload-modal'
201
+ open
202
+ >
203
+ <Dimmer
204
+ active={this.state.saving}
205
+ inverted
206
+ >
207
+ <Loader
208
+ content={i18n.t('FileUploadModal.loader')}
209
+ />
210
+ </Dimmer>
211
+ { this.renderErrors() }
212
+ <Modal.Header
213
+ content={this.props.title || i18n.t('FileUploadModal.title')}
214
+ />
215
+ <Modal.Content>
216
+ <FileUpload
217
+ onFilesAdded={this.onAddFiles.bind(this)}
218
+ />
219
+ { this.renderItems() }
220
+ </Modal.Content>
221
+ <Modal.Actions>
222
+ <Button
223
+ content={i18n.t('Common.buttons.save')}
224
+ disabled={!(this.state.items && this.state.items.length)}
225
+ primary
226
+ onClick={this.onSave.bind(this)}
227
+ />
228
+ <Button
229
+ content={i18n.t('Common.buttons.cancel')}
230
+ inverted
231
+ primary
232
+ onClick={this.onClose.bind(this)}
233
+ />
234
+ </Modal.Actions>
235
+ </Modal>
236
+ )}
237
+ </>
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Renders the error modal.
243
+ *
244
+ * @returns {null|*}
245
+ */
246
+ renderErrors() {
247
+ if (!this.hasErrors()) {
248
+ return null;
249
+ }
250
+
251
+ return (
252
+ <Toaster
253
+ onDismiss={this.onDismissErrors.bind(this)}
254
+ timeout={0}
255
+ type={Toaster.MessageTypes.negative}
256
+ >
257
+ <Message.Header
258
+ content={i18n.t('Common.messages.error.header')}
259
+ />
260
+ <Message.List>
261
+ { _.map(this.state.items, this.renderMessageItem.bind(this)) }
262
+ </Message.List>
263
+ </Toaster>
264
+ );
265
+ }
266
+
267
+ /**
268
+ * Renders the passed item.
269
+ *
270
+ * @param item
271
+ *
272
+ * @returns {*}
273
+ */
274
+ renderItem(item: any) {
275
+ const FileItem = this.props.itemComponent;
276
+
277
+ return (
278
+ <FileItem
279
+ isError={(key) => _.contains(item.errors, key)}
280
+ isRequired={(key) => !!this.props.required[key]}
281
+ item={item}
282
+ onAssociationInputChange={this.onAssociationInputChange.bind(this, item)}
283
+ onDelete={this.onDelete.bind(this, item)}
284
+ onTextInputChange={this.onTextInputChange.bind(this, item)}
285
+ onUpdate={this.onUpdate.bind(this, item)}
286
+ />
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Renders the list of items.
292
+ *
293
+ * @returns {null|*}
294
+ */
295
+ renderItems() {
296
+ if (!(this.state.items && this.state.items.length)) {
297
+ return null;
298
+ }
299
+
300
+ return (
301
+ <Item.Group
302
+ as={Form}
303
+ divided
304
+ noValidate
305
+ relaxed='very'
306
+ >
307
+ { _.map(this.state.items, this.renderItem.bind(this)) }
308
+ </Item.Group>
309
+ );
310
+ }
311
+
312
+ /**
313
+ * Renders the errors message for the passed item.
314
+ *
315
+ * @param item
316
+ * @param index
317
+ *
318
+ * @returns {null|*}
319
+ */
320
+ renderMessageItem(item: any, index: number) {
321
+ if (_.isEmpty(item.errors)) {
322
+ return null;
323
+ }
324
+
325
+ const filename = !_.isEmpty(item.name) ? item.name : `File ${index}`;
326
+ const fields = _.map(item.errors, (e) => this.props.required[e]).join(LIST_DELIMITER);
327
+
328
+ return (
329
+ <Message.Item
330
+ content={i18n.t('FileUploadModal.errors.required', { filename, fields })}
331
+ key={index}
332
+ />
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Saves the uploaded items.
338
+ */
339
+ save() {
340
+ if (this.hasErrors()) {
341
+ return;
342
+ }
343
+
344
+ this.setState({ saving: true }, () => {
345
+ this.props
346
+ .onSave(this.state.items)
347
+ .then(this.onClose.bind(this));
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Validates the list of items.
353
+ */
354
+ validate() {
355
+ this.setState((state) => {
356
+ const items = _.map(state.items, this.validateItem.bind(this));
357
+
358
+ return {
359
+ items,
360
+ saving: !_.find(items, (item) => !_.isEmpty(item.errors))
361
+ };
362
+ }, this.save.bind(this));
363
+ }
364
+
365
+ /**
366
+ * Validates the passed item.
367
+ *
368
+ * @param item
369
+ *
370
+ * @returns {{errors: []}}
371
+ */
372
+ validateItem(item: any) {
373
+ const errors = [];
374
+
375
+ _.each(_.keys(this.props.required), (key) => {
376
+ const value = item[key];
377
+ let invalid;
378
+
379
+ if (_.isNumber(value)) {
380
+ invalid = _.isEmpty(value.toString());
381
+ } else {
382
+ invalid = _.isEmpty(value);
383
+ }
384
+
385
+ if (invalid) {
386
+ errors.push(key);
387
+ }
388
+ });
389
+
390
+ return { ...item, errors };
391
+ }
392
+ }
393
+
394
+ FileUploadModal.defaultProps = {
395
+ includeButton: true
396
+ };
397
+
398
+ export type FileUploadProps = {
399
+ isError: (key: string) => boolean,
400
+ isRequired: (key: string) => boolean,
401
+ item: any,
402
+ onAssociationInputChange: (idKey: string, valueKey: string, item: any) => void,
403
+ onDelete: (item: any) => void,
404
+ onTextInputChange: (item: any, value: any) => void,
405
+ onUpdate: (item: any, props: any) => void
406
+ };
407
+
408
+ export default FileUploadModal;