@performant-software/semantic-components 0.5.1 → 0.5.4
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 +5 -0
- package/package.json +5 -5
- package/src/components/DataList.js +10 -2
- package/src/components/DataTableColumnSelector.js +37 -33
- package/src/components/DataView.css +4 -0
- package/src/components/DataView.js +407 -0
- package/src/components/MenuBar.css +0 -0
- package/src/components/MenuBar.js +82 -0
- package/src/components/MenuSidebar.css +0 -0
- package/src/components/MenuSidebar.js +84 -0
- package/src/components/ReferenceCodeDropdown.css +0 -0
- package/src/components/ReferenceCodeDropdown.js +118 -0
- package/src/components/ReferenceCodeModal.css +0 -0
- package/src/components/ReferenceCodeModal.js +32 -0
- package/src/components/ReferenceTableModal.css +0 -0
- package/src/components/ReferenceTableModal.js +67 -0
- package/src/components/ReferenceTablesList.css +0 -0
- package/src/components/ReferenceTablesList.js +44 -0
- package/src/i18n/en.json +32 -0
- package/src/index.js +7 -0
- package/types/components/DataList.js.flow +10 -2
- package/types/components/DataTableColumnSelector.js.flow +37 -33
- package/types/components/DataView.js.flow +302 -20
- package/types/components/MenuBar.js.flow +9 -4
- package/types/components/MenuSidebar.js.flow +18 -6
- package/types/components/ReferenceCodeDropdown.js.flow +118 -0
- package/types/components/ReferenceCodeModal.js.flow +32 -0
- package/types/components/ReferenceTableModal.js.flow +67 -0
- package/types/components/ReferenceTablesList.js.flow +44 -0
- package/types/index.js.flow +5 -0
|
@@ -1,42 +1,263 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
|
|
3
3
|
import axios from 'axios';
|
|
4
|
-
import React, {
|
|
5
|
-
|
|
4
|
+
import React, {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Form, Grid, Modal } from 'semantic-ui-react';
|
|
6
12
|
import _ from 'underscore';
|
|
7
|
-
import
|
|
13
|
+
import DataTable from './DataTable';
|
|
14
|
+
import DropdownButton from './DropdownButton';
|
|
15
|
+
import i18n from '../i18n/i18n';
|
|
8
16
|
import MenuBar from './MenuBar';
|
|
9
|
-
import
|
|
17
|
+
import MenuSidebar from './MenuSidebar';
|
|
18
|
+
import useDataList, { SORT_ASCENDING, SORT_DESCENDING } from './DataList';
|
|
19
|
+
import './DataView.css';
|
|
20
|
+
|
|
21
|
+
import type { Column } from './DataTable';
|
|
22
|
+
|
|
23
|
+
type Sort = {
|
|
24
|
+
label: string,
|
|
25
|
+
value: string
|
|
26
|
+
};
|
|
10
27
|
|
|
11
28
|
type Item = {
|
|
12
29
|
id: string,
|
|
13
30
|
label: string,
|
|
14
31
|
group?: string,
|
|
15
|
-
|
|
32
|
+
columns?: Array<Column>,
|
|
33
|
+
sorts?: Array<Sort>
|
|
34
|
+
};
|
|
16
35
|
|
|
17
36
|
type Props = {
|
|
37
|
+
columnCount?: number,
|
|
18
38
|
items: Array<Item>,
|
|
19
39
|
layout: 'top' | 'left',
|
|
20
40
|
title: string
|
|
21
41
|
};
|
|
22
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Sets up the List component used by the DataView component. This component is responsible for rendering the DataTable
|
|
45
|
+
* and handling sorting.
|
|
46
|
+
*/
|
|
47
|
+
const List = useDataList((props) => {
|
|
48
|
+
const [activeSort, setActiveSort] = useState();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sets the active sort object on the state.
|
|
52
|
+
*
|
|
53
|
+
* @type {(function(*): void)|*}
|
|
54
|
+
*/
|
|
55
|
+
const onSortChange = useCallback((sort) => {
|
|
56
|
+
const newSort = { ...sort };
|
|
57
|
+
|
|
58
|
+
if (activeSort && activeSort.value === sort.value) {
|
|
59
|
+
newSort.direction = activeSort.direction === SORT_DESCENDING ? SORT_ASCENDING : SORT_DESCENDING;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setActiveSort(newSort);
|
|
63
|
+
}, [props.sorts, activeSort]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calls the onSort prop when the active sort is changed.
|
|
67
|
+
*/
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (activeSort) {
|
|
70
|
+
props.onSort(activeSort.value, activeSort.direction, props.page);
|
|
71
|
+
}
|
|
72
|
+
}, [activeSort]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Calls the onSort prop when the component is loaded to initialize the data set.
|
|
76
|
+
*/
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const { page } = props;
|
|
79
|
+
|
|
80
|
+
let { sortColumn = '', sortDirection = SORT_ASCENDING } = props;
|
|
81
|
+
|
|
82
|
+
if (!sortColumn) {
|
|
83
|
+
const defaultSort = _.first(props.sort);
|
|
84
|
+
|
|
85
|
+
if (defaultSort) {
|
|
86
|
+
sortColumn = defaultSort.value;
|
|
87
|
+
|
|
88
|
+
if (defaultSort.direction) {
|
|
89
|
+
sortDirection = defaultSort.direction;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
props.onSort(sortColumn, sortDirection, page);
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<DataTable
|
|
99
|
+
{...props}
|
|
100
|
+
renderListHeader={() => props.sorts && (
|
|
101
|
+
<DropdownButton
|
|
102
|
+
basic
|
|
103
|
+
icon={activeSort && activeSort.direction === SORT_DESCENDING
|
|
104
|
+
? 'sort alphabet down'
|
|
105
|
+
: 'sort alphabet up'}
|
|
106
|
+
onChange={() => {}}
|
|
107
|
+
options={_.map(props.sorts, (sort) => ({
|
|
108
|
+
key: sort.value,
|
|
109
|
+
value: sort.value,
|
|
110
|
+
text: sort.label,
|
|
111
|
+
onClick: () => onSortChange(sort)
|
|
112
|
+
}))}
|
|
113
|
+
text={activeSort
|
|
114
|
+
? i18n.t('DataView.labels.sortBy', { column: activeSort.label })
|
|
115
|
+
: i18n.t('DataView.labels.noSort')}
|
|
116
|
+
value={activeSort && activeSort.value}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
tableProps={{
|
|
120
|
+
...props.tableProps || {},
|
|
121
|
+
celled: true
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
23
127
|
const DataView = (props: Props) => {
|
|
24
128
|
const [activeItem, setActiveItem] = useState();
|
|
129
|
+
const [selectedRecord, setSelectedRecord] = useState();
|
|
130
|
+
|
|
25
131
|
const [columns, setColumns] = useState([]);
|
|
26
132
|
const [menu, setMenu] = useState([]);
|
|
27
133
|
const [paddingLeft, setPaddingLeft] = useState();
|
|
134
|
+
const [key, setKey] = useState();
|
|
28
135
|
|
|
29
136
|
const menuRef = useRef();
|
|
30
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Sets the collection name based on the URL of the active item.
|
|
140
|
+
*
|
|
141
|
+
* @type {string}
|
|
142
|
+
*/
|
|
143
|
+
const collectionName = useMemo(() => {
|
|
144
|
+
let name = '';
|
|
145
|
+
|
|
146
|
+
if (activeItem) {
|
|
147
|
+
name = activeItem.url.substr(activeItem.url.lastIndexOf('/') + 1, activeItem.url.length);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return name;
|
|
151
|
+
}, [activeItem]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Merges the calculated columns with the columns passed with the active item. The idea is to allow customization
|
|
155
|
+
* of the columns by the implementing component, but not require it.
|
|
156
|
+
*
|
|
157
|
+
* @type {(function(*): ([]|*))|*}
|
|
158
|
+
*/
|
|
159
|
+
const mergeColumns = useCallback((items) => {
|
|
160
|
+
if (!activeItem) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Build the list of columns based on the unique set of properties
|
|
165
|
+
const keys = _.chain(items)
|
|
166
|
+
.map((item) => _.keys(item))
|
|
167
|
+
.flatten()
|
|
168
|
+
.uniq()
|
|
169
|
+
.value();
|
|
170
|
+
|
|
171
|
+
// Merge any columns provided by the implementing component by name
|
|
172
|
+
let cols = _.map(keys, (col, index) => {
|
|
173
|
+
const columnValue = _.findWhere(activeItem.columns, { name: col }) || {};
|
|
174
|
+
|
|
175
|
+
const defaultValue = {
|
|
176
|
+
name: col,
|
|
177
|
+
label: col.trim().replace(/^\w/, (c) => c.toUpperCase()).replaceAll('_', ' '),
|
|
178
|
+
sortable: false,
|
|
179
|
+
resolve: (item) => resolveValue(item, col),
|
|
180
|
+
hidden: index > props.columnCount,
|
|
181
|
+
index
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return _.extend(defaultValue, columnValue);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Include any columns provided by the implementing component that do not exist on the object (i.e. extra columns)
|
|
188
|
+
cols = [
|
|
189
|
+
...cols,
|
|
190
|
+
..._.reject(activeItem.columns, (col) => _.contains(keys, col.name))
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
// Sort the columns by the index property
|
|
194
|
+
return _.sortBy(cols, 'index');
|
|
195
|
+
}, [activeItem]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Calls the "show" URL for the passed item. It's possible that the API does not implement a public "show" route. In
|
|
199
|
+
* this case and error is expected and the passed item will be set as the selected record.
|
|
200
|
+
*
|
|
201
|
+
* @type {(function(*): void)|*}
|
|
202
|
+
*/
|
|
203
|
+
const onItemSelection = useCallback((item) => {
|
|
204
|
+
if (activeItem) {
|
|
205
|
+
axios
|
|
206
|
+
.get(`${activeItem.url}/${item.id}`)
|
|
207
|
+
.then(({ data }) => {
|
|
208
|
+
const itemKey = _.first(_.keys(data));
|
|
209
|
+
setSelectedRecord(data[itemKey]);
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {
|
|
212
|
+
setSelectedRecord(item);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}, [activeItem]);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Calls the "index" URL to initialize the data set.
|
|
219
|
+
*
|
|
220
|
+
* @type {function(*): *}
|
|
221
|
+
*/
|
|
31
222
|
const onLoad = useCallback((params) => {
|
|
223
|
+
let promise;
|
|
224
|
+
|
|
32
225
|
if (activeItem) {
|
|
33
|
-
axios
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
226
|
+
promise = axios
|
|
227
|
+
.get(activeItem.url, { params })
|
|
228
|
+
.then((response) => {
|
|
229
|
+
const items = response.data[collectionName];
|
|
230
|
+
setColumns(mergeColumns(items));
|
|
231
|
+
|
|
232
|
+
return response;
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
promise = Promise.resolve();
|
|
37
236
|
}
|
|
237
|
+
|
|
238
|
+
return promise;
|
|
38
239
|
}, [activeItem]);
|
|
39
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Returns the value of the passed attribute on the passed item. This function handles use cases for special
|
|
243
|
+
* types of objects.
|
|
244
|
+
*
|
|
245
|
+
* @type {function(*, *): *}
|
|
246
|
+
*/
|
|
247
|
+
const resolveValue = useCallback((item, attribute) => {
|
|
248
|
+
let value = item[attribute];
|
|
249
|
+
|
|
250
|
+
if (_.isArray(value)) {
|
|
251
|
+
value = _.size(value);
|
|
252
|
+
} else if (_.isBoolean(value)) {
|
|
253
|
+
value = Boolean(value).toString();
|
|
254
|
+
} else if (_.isObject(value)) {
|
|
255
|
+
value = value[_.first(_.keys(value))];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return value;
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
40
261
|
/**
|
|
41
262
|
* Re-formats the passed items as a menu object. If we're grouping the menu items, items will be added as
|
|
42
263
|
* a child property of the group object.
|
|
@@ -66,7 +287,7 @@ const DataView = (props: Props) => {
|
|
|
66
287
|
*/
|
|
67
288
|
useEffect(() => {
|
|
68
289
|
if (props.items && props.items.length) {
|
|
69
|
-
setActiveItem(props.items
|
|
290
|
+
setActiveItem(_.first(props.items));
|
|
70
291
|
}
|
|
71
292
|
}, [props.items]);
|
|
72
293
|
|
|
@@ -79,6 +300,24 @@ const DataView = (props: Props) => {
|
|
|
79
300
|
}
|
|
80
301
|
}, [menuRef, props.layout]);
|
|
81
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Calculates the key value when the active item or columns are changed. This is done to force re-rendering
|
|
305
|
+
* of the List component when the columns are loaded via the API.
|
|
306
|
+
*/
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
const keys = [];
|
|
309
|
+
|
|
310
|
+
if (activeItem) {
|
|
311
|
+
keys.push(activeItem.id);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (columns && columns.length) {
|
|
315
|
+
keys.push(..._.pluck(columns, 'name'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setKey(keys.join(''));
|
|
319
|
+
}, [activeItem, columns]);
|
|
320
|
+
|
|
82
321
|
return (
|
|
83
322
|
<div
|
|
84
323
|
className='data-view'
|
|
@@ -100,26 +339,69 @@ const DataView = (props: Props) => {
|
|
|
100
339
|
items={menu}
|
|
101
340
|
/>
|
|
102
341
|
)}
|
|
103
|
-
<
|
|
342
|
+
<div
|
|
104
343
|
style={{
|
|
105
|
-
paddingLeft
|
|
344
|
+
marginLeft: paddingLeft
|
|
106
345
|
}}
|
|
107
346
|
>
|
|
108
347
|
{ activeItem && (
|
|
109
|
-
<
|
|
110
|
-
|
|
348
|
+
<List
|
|
349
|
+
actions={[{
|
|
350
|
+
name: 'details',
|
|
351
|
+
icon: 'info',
|
|
352
|
+
onClick: onItemSelection
|
|
353
|
+
}]}
|
|
354
|
+
collectionName={collectionName}
|
|
111
355
|
columns={columns}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}))}
|
|
356
|
+
key={key}
|
|
357
|
+
onLoad={onLoad}
|
|
358
|
+
perPageOptions={[10, 25, 50, 100]}
|
|
116
359
|
searchable
|
|
360
|
+
sorts={activeItem.sorts}
|
|
117
361
|
/>
|
|
118
362
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
363
|
+
</div>
|
|
364
|
+
{ selectedRecord && (
|
|
365
|
+
<Modal
|
|
366
|
+
as={Form}
|
|
367
|
+
centered={false}
|
|
368
|
+
className='data-view-modal'
|
|
369
|
+
closeIcon
|
|
370
|
+
onClose={() => setSelectedRecord(null)}
|
|
371
|
+
open
|
|
372
|
+
>
|
|
373
|
+
<Modal.Header
|
|
374
|
+
content={i18n.t('DataView.labels.details')}
|
|
375
|
+
/>
|
|
376
|
+
<Modal.Content>
|
|
377
|
+
<Grid
|
|
378
|
+
columns={3}
|
|
379
|
+
doubling
|
|
380
|
+
>
|
|
381
|
+
{ _.map(mergeColumns([selectedRecord]), (column) => (
|
|
382
|
+
<Grid.Column
|
|
383
|
+
as={Form.Field}
|
|
384
|
+
key={column.name}
|
|
385
|
+
>
|
|
386
|
+
<span
|
|
387
|
+
className='label'
|
|
388
|
+
>
|
|
389
|
+
{ column.label }
|
|
390
|
+
</span>
|
|
391
|
+
{ resolveValue(selectedRecord, column.name) }
|
|
392
|
+
</Grid.Column>
|
|
393
|
+
))}
|
|
394
|
+
</Grid>
|
|
395
|
+
</Modal.Content>
|
|
396
|
+
</Modal>
|
|
397
|
+
)}
|
|
121
398
|
</div>
|
|
122
399
|
);
|
|
123
400
|
};
|
|
124
401
|
|
|
402
|
+
DataView.defaultProps = {
|
|
403
|
+
columnCount: 5,
|
|
404
|
+
layout: 'left'
|
|
405
|
+
};
|
|
406
|
+
|
|
125
407
|
export default DataView;
|
|
@@ -24,8 +24,11 @@ const MenuBar = ({ header, items, ...props }: Props) => {
|
|
|
24
24
|
*
|
|
25
25
|
* @returns {JSX.Element}
|
|
26
26
|
*/
|
|
27
|
-
const renderDropdownItem = useCallback((item) => (
|
|
28
|
-
<Dropdown.Item
|
|
27
|
+
const renderDropdownItem = useCallback((item, index) => (
|
|
28
|
+
<Dropdown.Item
|
|
29
|
+
key={index}
|
|
30
|
+
{...item}
|
|
31
|
+
/>
|
|
29
32
|
), []);
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -33,8 +36,9 @@ const MenuBar = ({ header, items, ...props }: Props) => {
|
|
|
33
36
|
*
|
|
34
37
|
* @returns {JSX.Element}
|
|
35
38
|
*/
|
|
36
|
-
const renderMenuItem = useCallback((item) => (
|
|
39
|
+
const renderMenuItem = useCallback((item, index) => (
|
|
37
40
|
<Menu.Item
|
|
41
|
+
key={index}
|
|
38
42
|
{...item}
|
|
39
43
|
/>
|
|
40
44
|
), []);
|
|
@@ -44,9 +48,10 @@ const MenuBar = ({ header, items, ...props }: Props) => {
|
|
|
44
48
|
*
|
|
45
49
|
* @returns {JSX.Element}
|
|
46
50
|
*/
|
|
47
|
-
const renderDropdown = useCallback((item) => (
|
|
51
|
+
const renderDropdown = useCallback((item, index) => (
|
|
48
52
|
<DropdownMenu
|
|
49
53
|
item
|
|
54
|
+
key={index}
|
|
50
55
|
text={item.content}
|
|
51
56
|
>
|
|
52
57
|
{ _.map(item.items, (i) => (i.items ? renderDropdown(i) : renderDropdownItem(i)))}
|
|
@@ -4,24 +4,35 @@ import React, { useCallback } from 'react';
|
|
|
4
4
|
import {
|
|
5
5
|
Header,
|
|
6
6
|
Menu,
|
|
7
|
+
Ref,
|
|
7
8
|
type HeaderProps,
|
|
8
|
-
type MenuItemProps
|
|
9
|
+
type MenuItemProps
|
|
9
10
|
} from 'semantic-ui-react';
|
|
10
11
|
import _ from 'underscore';
|
|
11
12
|
|
|
12
13
|
type Props = {
|
|
14
|
+
contextRef: {
|
|
15
|
+
current: ?HTMLElement
|
|
16
|
+
},
|
|
13
17
|
header: HeaderProps,
|
|
14
18
|
items: Array<HeaderProps | MenuItemProps>
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
const MenuSidebar = ({
|
|
21
|
+
const MenuSidebar = ({
|
|
22
|
+
contextRef,
|
|
23
|
+
header,
|
|
24
|
+
items,
|
|
25
|
+
...props
|
|
26
|
+
}: Props) => {
|
|
18
27
|
/**
|
|
19
28
|
* Renders the passed item as a menu.
|
|
20
29
|
*
|
|
21
30
|
* @type {unknown}
|
|
22
31
|
*/
|
|
23
|
-
const renderMenu = useCallback((item) => (
|
|
24
|
-
<Menu.Item
|
|
32
|
+
const renderMenu = useCallback((item, index) => (
|
|
33
|
+
<Menu.Item
|
|
34
|
+
key={index}
|
|
35
|
+
>
|
|
25
36
|
<Menu.Header
|
|
26
37
|
{...item}
|
|
27
38
|
/>
|
|
@@ -36,8 +47,9 @@ const MenuSidebar = ({ header, items, ...props }: Props) => {
|
|
|
36
47
|
*
|
|
37
48
|
* @type {unknown}
|
|
38
49
|
*/
|
|
39
|
-
const renderMenuItem = useCallback((item) => (
|
|
50
|
+
const renderMenuItem = useCallback((item, index) => (
|
|
40
51
|
<Menu.Item
|
|
52
|
+
key={index}
|
|
41
53
|
{...item}
|
|
42
54
|
/>
|
|
43
55
|
), []);
|
|
@@ -51,7 +63,7 @@ const MenuSidebar = ({ header, items, ...props }: Props) => {
|
|
|
51
63
|
|
|
52
64
|
return (
|
|
53
65
|
<Ref
|
|
54
|
-
innerRef={
|
|
66
|
+
innerRef={contextRef}
|
|
55
67
|
>
|
|
56
68
|
<Menu
|
|
57
69
|
{...props}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { ReferenceCodesService } from '@performant-software/shared-components';
|
|
4
|
+
import React, {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react';
|
|
10
|
+
import _ from 'underscore';
|
|
11
|
+
import { Dropdown } from 'semantic-ui-react';
|
|
12
|
+
|
|
13
|
+
type Item = {
|
|
14
|
+
reference_table_id: number,
|
|
15
|
+
key: string
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
fluid?: boolean,
|
|
20
|
+
multiple?: boolean,
|
|
21
|
+
onChange: (item: Item) => void,
|
|
22
|
+
placeholder?: string,
|
|
23
|
+
referenceTable: string,
|
|
24
|
+
value: Item | Array<Item>
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ReferenceCodeDropdown = (props: Props) => {
|
|
28
|
+
const [loading, setLoading] = useState(false);
|
|
29
|
+
const [options, setOptions] = useState([]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts the passed ID to a reference code item.
|
|
33
|
+
*
|
|
34
|
+
* @type {function(*): {reference_code_id: *}}
|
|
35
|
+
*/
|
|
36
|
+
const toItem = useCallback((id) => ({
|
|
37
|
+
reference_code_id: id
|
|
38
|
+
}), []);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converts the passed reference code to a dropdown option.
|
|
42
|
+
*
|
|
43
|
+
* @type {function(*): {text: *, value: *, key: *}}
|
|
44
|
+
*/
|
|
45
|
+
const toOption = useCallback((referenceCode) => ({
|
|
46
|
+
key: referenceCode.id,
|
|
47
|
+
value: referenceCode.id,
|
|
48
|
+
text: referenceCode.name
|
|
49
|
+
}), []);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts the selected values and calls the onChange prop.
|
|
53
|
+
*
|
|
54
|
+
* @type {(function(*, {value: *}): void)|*}
|
|
55
|
+
*/
|
|
56
|
+
const onChange = useCallback((e, { value }) => {
|
|
57
|
+
let values;
|
|
58
|
+
|
|
59
|
+
if (props.multiple) {
|
|
60
|
+
values = value;
|
|
61
|
+
} else {
|
|
62
|
+
values = _.compact([value]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
props.onChange(_.map(values, toItem));
|
|
66
|
+
}, [toItem, props.multiple, props.onChange]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets the "value" variable for the Dropdown component.
|
|
70
|
+
*/
|
|
71
|
+
const value = useMemo(() => {
|
|
72
|
+
const v = _.pluck(_.filter(props.value, (x) => !x._destroy), 'reference_code_id');
|
|
73
|
+
return props.multiple ? v : _.first(v);
|
|
74
|
+
}, [props.multiple, props.value]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Loads the list of reference codes from the server.
|
|
78
|
+
*/
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
setLoading(true);
|
|
81
|
+
|
|
82
|
+
const params = {
|
|
83
|
+
per_page: 0,
|
|
84
|
+
reference_table: props.referenceTable,
|
|
85
|
+
sort_by: 'name'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
ReferenceCodesService
|
|
89
|
+
.fetchAll(params)
|
|
90
|
+
.then(({ data }) => setOptions(_.map(data.reference_codes, toOption)))
|
|
91
|
+
.finally(() => setLoading(false));
|
|
92
|
+
}, [toOption]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Dropdown
|
|
96
|
+
clearable
|
|
97
|
+
disabled={loading}
|
|
98
|
+
fluid={props.fluid}
|
|
99
|
+
loading={loading}
|
|
100
|
+
multiple={props.multiple}
|
|
101
|
+
onChange={onChange}
|
|
102
|
+
options={options}
|
|
103
|
+
placeholder={props.placeholder}
|
|
104
|
+
search
|
|
105
|
+
selection
|
|
106
|
+
selectOnBlur={false}
|
|
107
|
+
value={value}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
ReferenceCodeDropdown.defaultProps = {
|
|
113
|
+
fluid: true,
|
|
114
|
+
multiple: false,
|
|
115
|
+
placeholder: undefined
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default ReferenceCodeDropdown;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type { EditContainerProps } from '@performant-software/shared-components';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Form, Modal } from 'semantic-ui-react';
|
|
6
|
+
import i18n from '../i18n/i18n';
|
|
7
|
+
|
|
8
|
+
const ReferenceCodeModal = (props: EditContainerProps) => (
|
|
9
|
+
<Modal
|
|
10
|
+
as={Form}
|
|
11
|
+
centered={false}
|
|
12
|
+
open
|
|
13
|
+
>
|
|
14
|
+
<Modal.Header
|
|
15
|
+
content={props.item.id
|
|
16
|
+
? i18n.t('ReferenceCodeModal.title.edit')
|
|
17
|
+
: i18n.t('ReferenceCodeModal.title.add')}
|
|
18
|
+
/>
|
|
19
|
+
<Modal.Content>
|
|
20
|
+
<Form.Input
|
|
21
|
+
error={props.isError('name')}
|
|
22
|
+
label={i18n.t('ReferenceCodeModal.labels.name')}
|
|
23
|
+
onChange={props.onTextInputChange.bind(this, 'name')}
|
|
24
|
+
required={props.isRequired('name')}
|
|
25
|
+
value={props.item.name}
|
|
26
|
+
/>
|
|
27
|
+
</Modal.Content>
|
|
28
|
+
{ props.children }
|
|
29
|
+
</Modal>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export default ReferenceCodeModal;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type { EditContainerProps } from '@performant-software/shared-components';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Form, Header, Modal } from 'semantic-ui-react';
|
|
6
|
+
import EmbeddedList from './EmbeddedList';
|
|
7
|
+
import ReferenceCodeModal from './ReferenceCodeModal';
|
|
8
|
+
import i18n from '../i18n/i18n';
|
|
9
|
+
|
|
10
|
+
const ReferenceTableModal = (props: EditContainerProps) => (
|
|
11
|
+
<Modal
|
|
12
|
+
as={Form}
|
|
13
|
+
centered={false}
|
|
14
|
+
className='reference-table-modal'
|
|
15
|
+
open
|
|
16
|
+
>
|
|
17
|
+
<Modal.Header
|
|
18
|
+
content={props.item.id
|
|
19
|
+
? i18n.t('ReferenceTableModal.title.edit')
|
|
20
|
+
: i18n.t('ReferenceTableModal.title.add')}
|
|
21
|
+
/>
|
|
22
|
+
<Modal.Content>
|
|
23
|
+
<Form.Input
|
|
24
|
+
error={props.isError('name')}
|
|
25
|
+
label={i18n.t('ReferenceTableModal.labels.name')}
|
|
26
|
+
onChange={props.onTextInputChange.bind(this, 'name')}
|
|
27
|
+
required={props.isRequired('name')}
|
|
28
|
+
value={props.item.name}
|
|
29
|
+
/>
|
|
30
|
+
<Form.Input
|
|
31
|
+
error={props.isError('key')}
|
|
32
|
+
label={i18n.t('ReferenceTableModal.labels.key')}
|
|
33
|
+
onChange={props.onTextInputChange.bind(this, 'key')}
|
|
34
|
+
required={props.isRequired('key')}
|
|
35
|
+
value={props.item.key}
|
|
36
|
+
/>
|
|
37
|
+
<Header
|
|
38
|
+
content={i18n.t('ReferenceTableModal.labels.referenceCodes')}
|
|
39
|
+
/>
|
|
40
|
+
<EmbeddedList
|
|
41
|
+
actions={[{
|
|
42
|
+
name: 'edit'
|
|
43
|
+
}, {
|
|
44
|
+
name: 'copy'
|
|
45
|
+
}, {
|
|
46
|
+
name: 'delete'
|
|
47
|
+
}]}
|
|
48
|
+
columns={[{
|
|
49
|
+
name: 'name',
|
|
50
|
+
label: i18n.t('ReferenceTableModal.referenceCodes.columns.name')
|
|
51
|
+
}]}
|
|
52
|
+
items={props.item.reference_codes}
|
|
53
|
+
modal={{
|
|
54
|
+
component: ReferenceCodeModal,
|
|
55
|
+
props: {
|
|
56
|
+
required: ['name']
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
onDelete={props.onDeleteChildAssociation.bind(this, 'reference_codes')}
|
|
60
|
+
onSave={props.onSaveChildAssociation.bind(this, 'reference_codes')}
|
|
61
|
+
/>
|
|
62
|
+
</Modal.Content>
|
|
63
|
+
{ props.children }
|
|
64
|
+
</Modal>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export default ReferenceTableModal;
|