@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.
Files changed (32) hide show
  1. package/build/index.js +1 -1
  2. package/build/index.js.map +1 -1
  3. package/build/main.css +5 -0
  4. package/package.json +5 -5
  5. package/src/components/DataList.js +10 -2
  6. package/src/components/DataTableColumnSelector.js +37 -33
  7. package/src/components/DataView.css +4 -0
  8. package/src/components/DataView.js +407 -0
  9. package/src/components/MenuBar.css +0 -0
  10. package/src/components/MenuBar.js +82 -0
  11. package/src/components/MenuSidebar.css +0 -0
  12. package/src/components/MenuSidebar.js +84 -0
  13. package/src/components/ReferenceCodeDropdown.css +0 -0
  14. package/src/components/ReferenceCodeDropdown.js +118 -0
  15. package/src/components/ReferenceCodeModal.css +0 -0
  16. package/src/components/ReferenceCodeModal.js +32 -0
  17. package/src/components/ReferenceTableModal.css +0 -0
  18. package/src/components/ReferenceTableModal.js +67 -0
  19. package/src/components/ReferenceTablesList.css +0 -0
  20. package/src/components/ReferenceTablesList.js +44 -0
  21. package/src/i18n/en.json +32 -0
  22. package/src/index.js +7 -0
  23. package/types/components/DataList.js.flow +10 -2
  24. package/types/components/DataTableColumnSelector.js.flow +37 -33
  25. package/types/components/DataView.js.flow +302 -20
  26. package/types/components/MenuBar.js.flow +9 -4
  27. package/types/components/MenuSidebar.js.flow +18 -6
  28. package/types/components/ReferenceCodeDropdown.js.flow +118 -0
  29. package/types/components/ReferenceCodeModal.js.flow +32 -0
  30. package/types/components/ReferenceTableModal.js.flow +67 -0
  31. package/types/components/ReferenceTablesList.js.flow +44 -0
  32. package/types/index.js.flow +5 -0
package/build/main.css CHANGED
@@ -231,6 +231,11 @@
231
231
  .data-table.ui.celled.table tr td.actions-cell {
232
232
  white-space: nowrap;
233
233
  }
234
+ .data-view-modal.ui.form .field span.label {
235
+ display: block;
236
+ font-weight: bold;
237
+ }
238
+
234
239
  .date-input.ui.icon.input > i.icon.right {
235
240
  cursor: pointer;
236
241
  pointer-events: inherit;
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@performant-software/semantic-components",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "A package of shared components based on the Semantic UI Framework.",
5
5
  "license": "MIT",
6
6
  "main": "./build/index.js",
7
7
  "style": "./build/styles.css",
8
8
  "scripts": {
9
- "build": "webpack --mode production && flow-copy-source -v src types",
10
- "postinstall": "yarn build"
9
+ "build": "webpack --mode production && flow-copy-source -v src types"
11
10
  },
12
11
  "dependencies": {
13
- "@performant-software/shared-components": "^0.5.1",
12
+ "@performant-software/shared-components": "^0.5.4",
14
13
  "@react-google-maps/api": "^2.8.1",
14
+ "axios": "^0.26.1",
15
15
  "flow-copy-source": "^2.0.9",
16
16
  "i18next": "^19.4.4",
17
17
  "react-calendar": "^3.3.0",
@@ -30,7 +30,7 @@
30
30
  "react-dom": ">= 16.13.1 < 18.0.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@performant-software/webpack-config": "^0.5.1",
33
+ "@performant-software/webpack-config": "^0.5.4",
34
34
  "react": "^17.0.2",
35
35
  "react-dom": "^17.0.2"
36
36
  }
@@ -19,10 +19,10 @@ type Props = {
19
19
  props?: any,
20
20
  onChange?: (filter: any) => Promise<any>
21
21
  },
22
- onDelete: (item: any) => Promise<any>,
22
+ onDelete?: (item: any) => Promise<any>,
23
23
  onDeleteAll?: () => Promise<any>,
24
24
  onLoad: (params: any) => Promise<any>,
25
- onSave: (item: any) => Promise<any>,
25
+ onSave?: (item: any) => Promise<any>,
26
26
  perPageOptions?: Array<number>,
27
27
  polling?: number,
28
28
  resolveErrors?: (error: any) => Array<string>,
@@ -251,6 +251,10 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
251
251
  * @returns {Q.Promise<any> | Promise<R> | Promise<any> | void | *}
252
252
  */
253
253
  onDelete(selectedItem: any) {
254
+ if (!this.props.onDelete) {
255
+ return Promise.resolve();
256
+ }
257
+
254
258
  return this.props
255
259
  .onDelete(selectedItem)
256
260
  .then(this.afterDelete.bind(this))
@@ -333,6 +337,10 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
333
337
  * @returns {Q.Promise<any> | Promise<R> | Promise<any> | void | *}
334
338
  */
335
339
  onSave(item: any) {
340
+ if (!this.props.onSave) {
341
+ return Promise.resolve();
342
+ }
343
+
336
344
  return Promise.resolve(this.props.onSave(item))
337
345
  .then(() => this.setState({ saved: true }, this.fetchData.bind(this)));
338
346
  }
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
 
3
- import React, { Component, type ComponentType } from 'react';
3
+ import React, { Component, type ComponentType, type Element } from 'react';
4
4
  import { Checkbox, Dropdown, Icon } from 'semantic-ui-react';
5
5
  import _ from 'underscore';
6
6
  import Draggable from './Draggable';
@@ -10,7 +10,8 @@ import type { Column } from './DataTable';
10
10
 
11
11
  type Props = {
12
12
  className: string,
13
- columns: Array<Column>
13
+ columns: Array<Column>,
14
+ renderListHeader?: () => Element<any>
14
15
  };
15
16
 
16
17
  type State = {
@@ -107,37 +108,40 @@ const useColumnSelector = (WrappedComponent: ComponentType<any>) => (
107
108
  */
108
109
  renderHeader() {
109
110
  return (
110
- <Dropdown
111
- basic
112
- button
113
- icon='cog'
114
- className='icon configure-button open-right'
115
- simple
116
- >
117
- <Dropdown.Menu>
118
- { this.state.columns
119
- .filter((c) => c.label && c.label.length)
120
- .map((c, index) => (
121
- <Draggable
122
- id={c.name}
123
- index={index}
124
- key={c.name}
125
- onDrag={this.onDrag.bind(this)}
126
- >
127
- <Dropdown.Item>
128
- <Icon
129
- name='bars'
130
- />
131
- <Checkbox
132
- checked={!c.hidden}
133
- label={c.label}
134
- onClick={this.onColumnCheckbox.bind(this, c)}
135
- />
136
- </Dropdown.Item>
137
- </Draggable>
138
- ))}
139
- </Dropdown.Menu>
140
- </Dropdown>
111
+ <>
112
+ { this.props.renderListHeader && this.props.renderListHeader() }
113
+ <Dropdown
114
+ basic
115
+ button
116
+ icon='cog'
117
+ className='icon configure-button open-right'
118
+ simple
119
+ >
120
+ <Dropdown.Menu>
121
+ { this.state.columns
122
+ .filter((c) => c.label && c.label.length)
123
+ .map((c, index) => (
124
+ <Draggable
125
+ id={c.name}
126
+ index={index}
127
+ key={c.name}
128
+ onDrag={this.onDrag.bind(this)}
129
+ >
130
+ <Dropdown.Item>
131
+ <Icon
132
+ name='bars'
133
+ />
134
+ <Checkbox
135
+ checked={!c.hidden}
136
+ label={c.label}
137
+ onClick={this.onColumnCheckbox.bind(this, c)}
138
+ />
139
+ </Dropdown.Item>
140
+ </Draggable>
141
+ ))}
142
+ </Dropdown.Menu>
143
+ </Dropdown>
144
+ </>
141
145
  );
142
146
  }
143
147
  }
@@ -0,0 +1,4 @@
1
+ .data-view-modal.ui.form .field span.label {
2
+ display: block;
3
+ font-weight: bold;
4
+ }
@@ -0,0 +1,407 @@
1
+ // @flow
2
+
3
+ import axios from 'axios';
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';
12
+ import _ from 'underscore';
13
+ import DataTable from './DataTable';
14
+ import DropdownButton from './DropdownButton';
15
+ import i18n from '../i18n/i18n';
16
+ import MenuBar from './MenuBar';
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
+ };
27
+
28
+ type Item = {
29
+ id: string,
30
+ label: string,
31
+ group?: string,
32
+ columns?: Array<Column>,
33
+ sorts?: Array<Sort>
34
+ };
35
+
36
+ type Props = {
37
+ columnCount?: number,
38
+ items: Array<Item>,
39
+ layout: 'top' | 'left',
40
+ title: string
41
+ };
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
+
127
+ const DataView = (props: Props) => {
128
+ const [activeItem, setActiveItem] = useState();
129
+ const [selectedRecord, setSelectedRecord] = useState();
130
+
131
+ const [columns, setColumns] = useState([]);
132
+ const [menu, setMenu] = useState([]);
133
+ const [paddingLeft, setPaddingLeft] = useState();
134
+ const [key, setKey] = useState();
135
+
136
+ const menuRef = useRef();
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
+ */
222
+ const onLoad = useCallback((params) => {
223
+ let promise;
224
+
225
+ if (activeItem) {
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();
236
+ }
237
+
238
+ return promise;
239
+ }, [activeItem]);
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
+
261
+ /**
262
+ * Re-formats the passed items as a menu object. If we're grouping the menu items, items will be added as
263
+ * a child property of the group object.
264
+ */
265
+ useEffect(() => {
266
+ const hasGroups = _.every(props.items, (item) => !!item.group);
267
+
268
+ if (!hasGroups) {
269
+ setMenu(props.items);
270
+ } else {
271
+ const menuItems = _.groupBy(props.items, 'group');
272
+ const headers = _.keys(menuItems);
273
+
274
+ setMenu(_.map(headers, (header) => ({
275
+ content: header,
276
+ items: _.map(menuItems[header], (item) => ({
277
+ active: activeItem && activeItem.id === item.id,
278
+ content: item.label,
279
+ onClick: () => setActiveItem(item)
280
+ }))
281
+ })));
282
+ }
283
+ }, [activeItem, props.items]);
284
+
285
+ /**
286
+ * Set the active item as the first item in the list.
287
+ */
288
+ useEffect(() => {
289
+ if (props.items && props.items.length) {
290
+ setActiveItem(_.first(props.items));
291
+ }
292
+ }, [props.items]);
293
+
294
+ /**
295
+ * If we're using the sidebar layout, set the padding so the fixed positioned menu does not overlap the content.
296
+ */
297
+ useEffect(() => {
298
+ if (props.layout === 'left' && menuRef && menuRef.current) {
299
+ setPaddingLeft(menuRef.current.offsetWidth);
300
+ }
301
+ }, [menuRef, props.layout]);
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
+
321
+ return (
322
+ <div
323
+ className='data-view'
324
+ >
325
+ { props.layout === 'top' && (
326
+ <MenuBar
327
+ header={{
328
+ content: props.title
329
+ }}
330
+ items={menu}
331
+ />
332
+ )}
333
+ { props.layout === 'left' && (
334
+ <MenuSidebar
335
+ contextRef={menuRef}
336
+ header={{
337
+ content: props.title
338
+ }}
339
+ items={menu}
340
+ />
341
+ )}
342
+ <div
343
+ style={{
344
+ marginLeft: paddingLeft
345
+ }}
346
+ >
347
+ { activeItem && (
348
+ <List
349
+ actions={[{
350
+ name: 'details',
351
+ icon: 'info',
352
+ onClick: onItemSelection
353
+ }]}
354
+ collectionName={collectionName}
355
+ columns={columns}
356
+ key={key}
357
+ onLoad={onLoad}
358
+ perPageOptions={[10, 25, 50, 100]}
359
+ searchable
360
+ sorts={activeItem.sorts}
361
+ />
362
+ )}
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
+ )}
398
+ </div>
399
+ );
400
+ };
401
+
402
+ DataView.defaultProps = {
403
+ columnCount: 5,
404
+ layout: 'left'
405
+ };
406
+
407
+ export default DataView;
File without changes
@@ -0,0 +1,82 @@
1
+ // @flow
2
+
3
+ import React, { useCallback } from 'react';
4
+ import {
5
+ Dropdown,
6
+ Menu,
7
+ type DropdownItemProps,
8
+ type HeaderProps,
9
+ type MenuProps
10
+ } from 'semantic-ui-react';
11
+ import _ from 'underscore';
12
+ import DropdownMenu from './DropdownMenu';
13
+
14
+ type Props = MenuProps & {
15
+ header: HeaderProps,
16
+ items: Array<DropdownItemProps | MenuProps>
17
+ };
18
+
19
+ const MenuBar = ({ header, items, ...props }: Props) => {
20
+ /**
21
+ * Renders the passed item as a dropdown item.
22
+ *
23
+ * @param item
24
+ *
25
+ * @returns {JSX.Element}
26
+ */
27
+ const renderDropdownItem = useCallback((item, index) => (
28
+ <Dropdown.Item
29
+ key={index}
30
+ {...item}
31
+ />
32
+ ), []);
33
+
34
+ /**
35
+ * Renders the passed item as a menu item.
36
+ *
37
+ * @returns {JSX.Element}
38
+ */
39
+ const renderMenuItem = useCallback((item, index) => (
40
+ <Menu.Item
41
+ key={index}
42
+ {...item}
43
+ />
44
+ ), []);
45
+
46
+ /**
47
+ * Renders the passed item as a dropdown menu.
48
+ *
49
+ * @returns {JSX.Element}
50
+ */
51
+ const renderDropdown = useCallback((item, index) => (
52
+ <DropdownMenu
53
+ item
54
+ key={index}
55
+ text={item.content}
56
+ >
57
+ { _.map(item.items, (i) => (i.items ? renderDropdown(i) : renderDropdownItem(i)))}
58
+ </DropdownMenu>
59
+ ), []);
60
+
61
+ /**
62
+ * Renders the passed item.
63
+ *
64
+ * @returns {JSX.Element}
65
+ */
66
+ const renderItem = useCallback((item) => (item.items ? renderDropdown(item) : renderMenuItem(item)), []);
67
+
68
+ return (
69
+ <Menu
70
+ {...props}
71
+ className='menu-bar'
72
+ >
73
+ <Menu.Item
74
+ {...header}
75
+ header
76
+ />
77
+ { _.map(items, (item) => renderItem(item)) }
78
+ </Menu>
79
+ );
80
+ };
81
+
82
+ export default MenuBar;
File without changes