@performant-software/semantic-components 0.5.5 → 0.5.6

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/main.css CHANGED
@@ -215,6 +215,10 @@
215
215
  font-weight: bold;
216
216
  }
217
217
 
218
+ .list-table .table-container {
219
+ margin-top: 20px;
220
+ }
221
+
218
222
  .date-input.ui.icon.input > i.icon.right {
219
223
  cursor: pointer;
220
224
  pointer-events: inherit;
@@ -488,10 +492,6 @@ div.react-calendar {
488
492
  z-index: 999;
489
493
  }
490
494
 
491
- .list-table .table-container {
492
- margin-top: 20px;
493
- }
494
-
495
495
  .login-modal .ui.grid > .column > .row {
496
496
  margin: 15px 150px 0px 150px;
497
497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@performant-software/semantic-components",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "A package of shared components based on the Semantic UI Framework.",
5
5
  "license": "MIT",
6
6
  "main": "./build/index.js",
@@ -12,7 +12,7 @@
12
12
  "build": "webpack --mode production && flow-copy-source -v src types"
13
13
  },
14
14
  "dependencies": {
15
- "@performant-software/shared-components": "^0.5.5",
15
+ "@performant-software/shared-components": "^0.5.6",
16
16
  "@react-google-maps/api": "^2.8.1",
17
17
  "axios": "^0.26.1",
18
18
  "i18next": "^19.4.4",
@@ -32,7 +32,7 @@
32
32
  "react-dom": ">= 16.13.1 < 18.0.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@performant-software/webpack-config": "^0.5.5",
35
+ "@performant-software/webpack-config": "^0.5.6",
36
36
  "flow-copy-source": "^2.0.9",
37
37
  "less": "^4.1.2",
38
38
  "less-loader": "^11.0.0",
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export { default as ColorPickerModal } from './components/ColorPickerModal';
16
16
  export { default as useDataList } from './components/DataList';
17
17
  export { default as DataTable } from './components/DataTable';
18
18
  export { default as DataView } from './components/DataView';
19
+ export { default as DatabaseView } from './components/DatabaseView';
19
20
  export { default as DateInput } from './components/DateInput';
20
21
  export { default as DatePicker } from './components/DatePicker';
21
22
  export { default as DescriptorField } from './components/DescriptorField';
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
 
3
- import { Timer, Utility } from '@performant-software/shared-components';
3
+ import { Object as ObjectUtils, Timer } from '@performant-software/shared-components';
4
4
  import React, { Component } from 'react';
5
5
  import {
6
6
  Button,
@@ -154,7 +154,7 @@ class AccordionList extends Component<Props, State> {
154
154
  let copy;
155
155
  if (this.props.onCopy) {
156
156
  copy = this.props.onCopy(selectedItem);
157
- if (Utility.isPromise(copy)) {
157
+ if (ObjectUtils.isPromise(copy)) {
158
158
  copy.then((item) => {
159
159
  this.setState({ selectedItem: item, modalAdd: true });
160
160
  });
@@ -15,6 +15,7 @@ import {
15
15
  import _ from 'underscore';
16
16
  import i18n from '../i18n/i18n';
17
17
  import EditModal from './EditModal';
18
+ import ModalContext from '../context/ModalContext';
18
19
  import NestedAccordion from './NestedAccordion';
19
20
  import SelectizeHeader from './SelectizeHeader';
20
21
  import Toaster from './Toaster';
@@ -39,7 +40,6 @@ type Props = {
39
40
  renderItem: (item: any) => string | Element<any>,
40
41
  selectedItems?: Array<any>,
41
42
  showToggle: (item: any) => boolean,
42
- t: (key: string) => string,
43
43
  title?: string
44
44
  };
45
45
 
@@ -187,93 +187,100 @@ class AccordionSelector extends Component<Props, State> {
187
187
  */
188
188
  render() {
189
189
  return (
190
- <Modal
191
- className='accordion-selector'
192
- open={this.props.open}
193
- size='small'
194
- >
195
- <Modal.Header>
196
- <Grid
197
- columns={2}
198
- verticalAlign='middle'
190
+ <ModalContext.Consumer>
191
+ { (mountNode) => (
192
+ <Modal
193
+ className='accordion-selector'
194
+ mountNode={mountNode}
195
+ open={this.props.open}
196
+ size='small'
199
197
  >
200
- <Grid.Column
201
- textAlign='left'
202
- >
203
- <Header
204
- content={this.props.title
205
- ? this.props.title
206
- : i18n.t('AccordionSelector.title')}
198
+ <Modal.Header>
199
+ <Grid
200
+ columns={2}
201
+ verticalAlign='middle'
202
+ >
203
+ <Grid.Column
204
+ textAlign='left'
205
+ width={7}
206
+ >
207
+ <Header
208
+ content={this.props.title
209
+ ? this.props.title
210
+ : i18n.t('AccordionSelector.title')}
211
+ />
212
+ </Grid.Column>
213
+ <Grid.Column
214
+ textAlign='right'
215
+ width={9}
216
+ >
217
+ <Input
218
+ aria-label='Search'
219
+ autoFocus
220
+ icon='search'
221
+ onKeyDown={Timer.clearSearchTimer.bind(this)}
222
+ onKeyUp={Timer.setSearchTimer.bind(this, this.onSearch.bind(this))}
223
+ onChange={this.onSearchChange.bind(this)}
224
+ size='mini'
225
+ type='text'
226
+ value={this.state.searchQuery}
227
+ />
228
+ { this.renderAddButton() }
229
+ </Grid.Column>
230
+ </Grid>
231
+ </Modal.Header>
232
+ <Modal.Content>
233
+ <SelectizeHeader
234
+ isSelected={(item) => this.state.selectedItem === item}
235
+ items={this.state.selectedItems}
236
+ onItemClick={this.onItemSelection.bind(this)}
237
+ renderItem={this.props.renderItem.bind(this)}
207
238
  />
208
- </Grid.Column>
209
- <Grid.Column
210
- textAlign='right'
211
- >
212
- <Input
213
- autoFocus
214
- icon='search'
215
- onKeyDown={Timer.clearSearchTimer.bind(this)}
216
- onKeyUp={Timer.setSearchTimer.bind(this, this.onSearch.bind(this))}
217
- onChange={this.onSearchChange.bind(this)}
218
- size='mini'
219
- type='text'
220
- value={this.state.searchQuery}
239
+ <NestedAccordion
240
+ getChildItems={this.props.getChildItems.bind(this, this.state.items)}
241
+ onItemClick={this.onItemClick.bind(this)}
242
+ onItemToggle={this.onItemToggle.bind(this)}
243
+ renderItem={this.props.renderItem.bind(this)}
244
+ renderRight={this.renderRight.bind(this)}
245
+ rootItems={this.props.getRootItems(this.state.items)}
246
+ showToggle={this.props.showToggle.bind(this)}
221
247
  />
222
- { this.renderAddButton() }
223
- </Grid.Column>
224
- </Grid>
225
- </Modal.Header>
226
- <Modal.Content>
227
- <SelectizeHeader
228
- isSelected={(item) => this.state.selectedItem === item}
229
- items={this.state.selectedItems}
230
- onItemClick={this.onItemSelection.bind(this)}
231
- renderItem={this.props.renderItem.bind(this)}
232
- />
233
- <NestedAccordion
234
- getChildItems={this.props.getChildItems.bind(this, this.state.items)}
235
- onItemClick={this.onItemClick.bind(this)}
236
- onItemToggle={this.onItemToggle.bind(this)}
237
- renderItem={this.props.renderItem.bind(this)}
238
- renderRight={this.renderRight.bind(this)}
239
- rootItems={this.props.getRootItems(this.state.items)}
240
- showToggle={this.props.showToggle.bind(this)}
241
- />
242
- { this.renderAddModal() }
243
- { this.state.saved && (
244
- <Toaster
245
- onDismiss={() => this.setState({ saved: false })}
246
- type={Toaster.MessageTypes.positive}
247
- >
248
- <Message.Header
249
- content={i18n.t('Common.messages.save.header')}
250
- />
251
- <Message.Content
252
- content={i18n.t('Common.messages.save.content')}
253
- />
254
- </Toaster>
255
- )}
256
- </Modal.Content>
257
- <Modal.Actions>
258
- <Button
259
- onClick={this.props.onSave.bind(this, this.state.selectedItems)}
260
- primary
261
- size='medium'
262
- type='submit'
263
- >
264
- { this.props.t('Common.buttons.save') }
265
- </Button>
266
- <Button
267
- inverted
268
- onClick={this.props.onClose.bind(this)}
269
- primary
270
- size='medium'
271
- type='button'
272
- >
273
- { this.props.t('Common.buttons.cancel') }
274
- </Button>
275
- </Modal.Actions>
276
- </Modal>
248
+ { this.renderAddModal() }
249
+ { this.state.saved && (
250
+ <Toaster
251
+ onDismiss={() => this.setState({ saved: false })}
252
+ type={Toaster.MessageTypes.positive}
253
+ >
254
+ <Message.Header
255
+ content={i18n.t('Common.messages.save.header')}
256
+ />
257
+ <Message.Content
258
+ content={i18n.t('Common.messages.save.content')}
259
+ />
260
+ </Toaster>
261
+ )}
262
+ </Modal.Content>
263
+ <Modal.Actions>
264
+ <Button
265
+ onClick={this.props.onSave.bind(this, this.state.selectedItems)}
266
+ primary
267
+ size='medium'
268
+ type='submit'
269
+ >
270
+ { i18n.t('Common.buttons.save') }
271
+ </Button>
272
+ <Button
273
+ basic
274
+ onClick={this.props.onClose.bind(this)}
275
+ size='medium'
276
+ type='button'
277
+ >
278
+ { i18n.t('Common.buttons.cancel') }
279
+ </Button>
280
+ </Modal.Actions>
281
+ </Modal>
282
+ )}
283
+ </ModalContext.Consumer>
277
284
  );
278
285
  }
279
286
 
@@ -291,7 +298,7 @@ class AccordionSelector extends Component<Props, State> {
291
298
  <Button
292
299
  basic
293
300
  className='add-button'
294
- content={this.props.t('Common.buttons.add')}
301
+ content={i18n.t('Common.buttons.add')}
295
302
  icon='plus'
296
303
  onClick={() => this.setState({ modalAdd: true })}
297
304
  />
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
4
4
  import { SketchPicker } from 'react-color';
5
5
  import { Button, Modal } from 'semantic-ui-react';
6
6
  import i18n from '../i18n/i18n';
7
+ import ModalContext from '../context/ModalContext';
7
8
  import './ColorPickerModal.css';
8
9
 
9
10
  type Props = {
@@ -38,38 +39,42 @@ class ColorPickerModal extends Component<Props, State> {
38
39
  */
39
40
  render() {
40
41
  return (
41
- <Modal
42
- className='color-picker-modal'
43
- onClose={this.props.onClose.bind(this)}
44
- open={this.props.open}
45
- >
46
- <Modal.Content>
47
- <SketchPicker
48
- color={this.state.selectedColor}
49
- disableAlpha={false}
50
- onChangeComplete={(selectedColor) => this.setState({ selectedColor })}
51
- />
52
- </Modal.Content>
53
- <Modal.Actions>
54
- <Button
55
- onClick={this.props.onSave.bind(this, this.state.selectedColor)}
56
- primary
57
- size='medium'
58
- type='submit'
42
+ <ModalContext.Consumer>
43
+ { (mountNode) => (
44
+ <Modal
45
+ className='color-picker-modal'
46
+ mountNode={mountNode}
47
+ onClose={this.props.onClose.bind(this)}
48
+ open={this.props.open}
59
49
  >
60
- { i18n.t('Common.buttons.save') }
61
- </Button>
62
- <Button
63
- inverted
64
- onClick={this.props.onClose.bind(this)}
65
- primary
66
- size='medium'
67
- type='button'
68
- >
69
- { i18n.t('Common.buttons.cancel') }
70
- </Button>
71
- </Modal.Actions>
72
- </Modal>
50
+ <Modal.Content>
51
+ <SketchPicker
52
+ color={this.state.selectedColor}
53
+ disableAlpha={false}
54
+ onChangeComplete={(selectedColor) => this.setState({ selectedColor })}
55
+ />
56
+ </Modal.Content>
57
+ <Modal.Actions>
58
+ <Button
59
+ onClick={this.props.onSave.bind(this, this.state.selectedColor)}
60
+ primary
61
+ size='medium'
62
+ type='submit'
63
+ >
64
+ { i18n.t('Common.buttons.save') }
65
+ </Button>
66
+ <Button
67
+ basic
68
+ onClick={this.props.onClose.bind(this)}
69
+ size='medium'
70
+ type='button'
71
+ >
72
+ { i18n.t('Common.buttons.cancel') }
73
+ </Button>
74
+ </Modal.Actions>
75
+ </Modal>
76
+ )}
77
+ </ModalContext.Consumer>
73
78
  );
74
79
  }
75
80
  }
@@ -43,6 +43,17 @@ const useColumnSelector = (WrappedComponent: ComponentType<any>) => (
43
43
  };
44
44
  }
45
45
 
46
+ /**
47
+ * Reset the columns on the state when the props change.
48
+ *
49
+ * @param prevProps
50
+ */
51
+ componentDidUpdate(prevProps: Props): * {
52
+ if (prevProps.columns !== this.props.columns) {
53
+ this.setState({ columns: this.props.columns });
54
+ }
55
+ }
56
+
46
57
  /**
47
58
  * Toggles the hidden property for the passed column.
48
59
  *
@@ -15,6 +15,7 @@ import DropdownButton from './DropdownButton';
15
15
  import i18n from '../i18n/i18n';
16
16
  import MenuBar from './MenuBar';
17
17
  import MenuSidebar from './MenuSidebar';
18
+ import ModalContext from '../context/ModalContext';
18
19
  import useDataList, { SORT_ASCENDING, SORT_DESCENDING } from './DataList';
19
20
  import './DataView.css';
20
21
 
@@ -362,38 +363,43 @@ const DataView = (props: Props) => {
362
363
  )}
363
364
  </div>
364
365
  { 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
366
+ <ModalContext.Consumer>
367
+ { (mountNode) => (
368
+ <Modal
369
+ as={Form}
370
+ centered={false}
371
+ className='data-view-modal'
372
+ closeIcon
373
+ mountNode={mountNode}
374
+ onClose={() => setSelectedRecord(null)}
375
+ open
380
376
  >
381
- { _.map(mergeColumns([selectedRecord]), (column) => (
382
- <Grid.Column
383
- as={Form.Field}
384
- key={column.name}
377
+ <Modal.Header
378
+ content={i18n.t('DataView.labels.details')}
379
+ />
380
+ <Modal.Content>
381
+ <Grid
382
+ columns={3}
383
+ doubling
385
384
  >
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>
385
+ { _.map(mergeColumns([selectedRecord]), (column) => (
386
+ <Grid.Column
387
+ as={Form.Field}
388
+ key={column.name}
389
+ >
390
+ <span
391
+ className='label'
392
+ >
393
+ { column.label }
394
+ </span>
395
+ { resolveValue(selectedRecord, column.name) }
396
+ </Grid.Column>
397
+ ))}
398
+ </Grid>
399
+ </Modal.Content>
400
+ </Modal>
401
+ )}
402
+ </ModalContext.Consumer>
397
403
  )}
398
404
  </div>
399
405
  );
@@ -0,0 +1,126 @@
1
+ // @flow
2
+
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState
9
+ } from 'react';
10
+ import axios from 'axios';
11
+ import _ from 'underscore';
12
+ import ListTable from './ListTable';
13
+ import MenuSidebar from './MenuSidebar';
14
+
15
+ type Props = {
16
+ columnCount?: number,
17
+ title: string,
18
+ url: string
19
+ };
20
+
21
+ const DatabaseView = (props: Props) => {
22
+ const [columns, setColumns] = useState([]);
23
+ const [selectedTable, setSelectedTable] = useState();
24
+ const [tables, setTables] = useState([]);
25
+
26
+ const menuRef = useRef();
27
+
28
+ const service = useMemo(() => ({
29
+ getColumns: (params) => axios.get(`${props.url}/api/columns`, { params }),
30
+ getData: (params) => axios.post(`${props.url}/api/search`, params),
31
+ getTables: () => axios.get(`${props.url}/api/tables`)
32
+ }), [props.url]);
33
+
34
+ const resolveValue = useCallback((column, item) => {
35
+ let value = item[column.column_name];
36
+
37
+ const { data_type: dataType } = column;
38
+
39
+ if (value) {
40
+ switch (dataType) {
41
+ case 'timestamp without time zone':
42
+ value = new Date(value).toLocaleDateString();
43
+ break;
44
+
45
+ default:
46
+ // Value is already set
47
+ }
48
+ }
49
+
50
+ return value;
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (service) {
55
+ service
56
+ .getTables()
57
+ .then(({ data }) => {
58
+ setTables(data);
59
+ setSelectedTable(_.first(data));
60
+ });
61
+ }
62
+ }, [service]);
63
+
64
+ useEffect(() => {
65
+ if (selectedTable) {
66
+ service
67
+ .getColumns({ table_name: selectedTable.table_name })
68
+ .then(({ data }) => setColumns(data));
69
+ }
70
+ }, [selectedTable, service]);
71
+
72
+ const test = useMemo(() => _.map(columns, (column, index) => ({
73
+ name: column.column_name,
74
+ label: column.column_name,
75
+ resolve: resolveValue.bind(this, column),
76
+ sortable: true,
77
+ hidden: index > props.columnCount
78
+ })), [columns, resolveValue, props.columnCount]);
79
+
80
+ return (
81
+ <div
82
+ className='database-view'
83
+ >
84
+ <MenuSidebar
85
+ contextRef={menuRef}
86
+ header={{
87
+ content: props.title,
88
+ inverted: true
89
+ }}
90
+ inverted
91
+ items={[{
92
+ items: _.map(tables, (table) => ({
93
+ active: selectedTable === table,
94
+ content: table.table_name,
95
+ onClick: () => setSelectedTable(table)
96
+ }))
97
+ }]}
98
+ style={{
99
+ overflow: 'auto',
100
+ width: '250px'
101
+ }}
102
+ />
103
+ <div
104
+ style={{
105
+ marginLeft: '250px'
106
+ }}
107
+ >
108
+ { selectedTable && (
109
+ <ListTable
110
+ collectionName='items'
111
+ columns={test}
112
+ onLoad={(params) => service.getData({ ...params, table_name: selectedTable.table_name })}
113
+ perPageOptions={[10, 25, 50, 100]}
114
+ searchable
115
+ />
116
+ )}
117
+ </div>
118
+ </div>
119
+ );
120
+ };
121
+
122
+ DatabaseView.defaultProps = {
123
+ columnCount: Number.MAX_SAFE_INTEGER
124
+ };
125
+
126
+ export default DatabaseView;
@@ -11,7 +11,8 @@ import { Dropdown, Ref } from 'semantic-ui-react';
11
11
 
12
12
  type Props = {
13
13
  children: Node,
14
- onClick?: () => void
14
+ onClick?: () => void,
15
+ role?: string
15
16
  };
16
17
 
17
18
  const DropdownMenu = (props: Props) => {
@@ -38,6 +39,15 @@ const DropdownMenu = (props: Props) => {
38
39
  };
39
40
  }, [ref]);
40
41
 
42
+ /**
43
+ * Sets the "role" aria attribute on the current element if provided.
44
+ */
45
+ useEffect(() => {
46
+ if (ref.current && props.role) {
47
+ ref.current.setAttribute('role', props.role);
48
+ }
49
+ }, [ref, props.role]);
50
+
41
51
  return (
42
52
  <Ref
43
53
  innerRef={ref}