@performant-software/semantic-components 0.5.9 → 0.5.12

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
@@ -436,6 +436,10 @@ div.react-calendar {
436
436
  padding-bottom: 20%;
437
437
  text-align: center;
438
438
  }
439
+ .lazy-document .react-pdf__Page__canvas {
440
+ width: 100% !important;
441
+ height: 100% !important;
442
+ }
439
443
 
440
444
  .photo-viewer {
441
445
  padding: 30px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@performant-software/semantic-components",
3
- "version": "0.5.9",
3
+ "version": "0.5.12",
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.9",
15
+ "@performant-software/shared-components": "^0.5.12",
16
16
  "@react-google-maps/api": "^2.8.1",
17
17
  "axios": "^0.26.1",
18
18
  "i18next": "^19.4.4",
@@ -21,6 +21,7 @@
21
21
  "react-dnd": "^11.1.3",
22
22
  "react-dnd-html5-backend": "^11.1.3",
23
23
  "react-i18next": "^11.4.0",
24
+ "react-pdf": "^5.7.2",
24
25
  "react-syntax-highlighter": "^15.5.0",
25
26
  "react-uuid": "^1.0.2",
26
27
  "semantic-ui-less": "^2.4.1",
@@ -32,7 +33,7 @@
32
33
  "react-dom": ">= 16.13.1 < 18.0.0"
33
34
  },
34
35
  "devDependencies": {
35
- "@performant-software/webpack-config": "^0.5.9",
36
+ "@performant-software/webpack-config": "^0.5.12",
36
37
  "flow-copy-source": "^2.0.9",
37
38
  "less": "^4.1.2",
38
39
  "less-loader": "^11.0.0",
@@ -63,6 +63,7 @@ class AssociatedDropdown extends Component<Props, State> {
63
63
  items: [],
64
64
  loading: false,
65
65
  modalAdd: false,
66
+ modalEdit: false,
66
67
  options: [],
67
68
  saved: false,
68
69
  searchQuery: props.searchQuery || '',
@@ -208,7 +209,7 @@ class AssociatedDropdown extends Component<Props, State> {
208
209
  { this.renderAddButton() }
209
210
  { this.renderClearButton() }
210
211
  </Button.Group>
211
- { this.renderAddModal() }
212
+ { this.renderModal() }
212
213
  { this.state.saved && (
213
214
  <Toaster
214
215
  onDismiss={() => this.setState({ saved: false })}
@@ -247,6 +248,27 @@ class AssociatedDropdown extends Component<Props, State> {
247
248
  );
248
249
  }
249
250
 
251
+ /**
252
+ * Renders the clear button.
253
+ *
254
+ * @returns {*}
255
+ */
256
+ renderClearButton() {
257
+ if (this.props.required) {
258
+ return null;
259
+ }
260
+
261
+ return (
262
+ <Button
263
+ basic
264
+ content={i18n.t('Common.buttons.clear')}
265
+ icon='times'
266
+ onClick={this.onClear.bind(this)}
267
+ type='button'
268
+ />
269
+ );
270
+ }
271
+
250
272
  /**
251
273
  * Renders the edit button (if applicable).
252
274
  *
@@ -262,7 +284,7 @@ class AssociatedDropdown extends Component<Props, State> {
262
284
  basic
263
285
  content={i18n.t('Common.buttons.edit')}
264
286
  icon='pencil'
265
- onClick={() => this.setState({ modalAdd: true })}
287
+ onClick={() => this.setState({ modalEdit: true })}
266
288
  type='button'
267
289
  />
268
290
  );
@@ -273,51 +295,37 @@ class AssociatedDropdown extends Component<Props, State> {
273
295
  *
274
296
  * @returns {null|*}
275
297
  */
276
- renderAddModal() {
277
- if (!(this.state.modalAdd && this.props.modal)) {
298
+ renderModal() {
299
+ if (!((this.state.modalAdd || this.state.modalEdit) && this.props.modal)) {
278
300
  return null;
279
301
  }
280
302
 
281
- const {
282
- component, props, onSave
283
- } = this.props.modal;
303
+ const { component, props, onSave } = this.props.modal;
304
+
305
+ // If we're editing the existing record, pass the ID to the modal in order to retrieve the full record.
306
+ let item;
307
+
308
+ if (this.state.modalEdit) {
309
+ item = {
310
+ id: this.state.value
311
+ };
312
+ }
284
313
 
285
314
  return (
286
315
  <EditModal
287
316
  component={component}
288
- item={{ id: this.state.value }}
289
- onClose={() => this.setState({ modalAdd: false })}
290
- onSave={(item) => onSave(item)
317
+ item={item}
318
+ onClose={() => this.setState({ modalAdd: false, modalEdit: false })}
319
+ onSave={(data) => onSave(data)
291
320
  .then((record) => {
292
321
  this.props.onSelection(record);
293
- this.setState({ modalAdd: false, saved: true });
322
+ this.setState({ modalAdd: false, modalEdit: false, saved: true });
294
323
  })}
295
324
  {...props}
296
325
  />
297
326
  );
298
327
  }
299
328
 
300
- /**
301
- * Renders the clear button.
302
- *
303
- * @returns {*}
304
- */
305
- renderClearButton() {
306
- if (this.props.required) {
307
- return null;
308
- }
309
-
310
- return (
311
- <Button
312
- basic
313
- content={i18n.t('Common.buttons.clear')}
314
- icon='times'
315
- onClick={this.onClear.bind(this)}
316
- type='button'
317
- />
318
- );
319
- }
320
-
321
329
  /**
322
330
  * Sets the search timer.
323
331
  */
@@ -437,7 +437,7 @@ class DataTable extends Component<Props, State> {
437
437
  <Table.HeaderCell
438
438
  key={column.name}
439
439
  sorted={this.props.sortColumn === column.name ? this.props.sortDirection : null}
440
- onClick={this.props.onColumnClick.bind(this, column)}
440
+ onClick={() => this.props.onColumnClick(column)}
441
441
  >
442
442
  { column.label }
443
443
  <ColumnResize
@@ -25,6 +25,8 @@ type Props = {
25
25
  className?: string,
26
26
  columns: Array<Column>,
27
27
  configurable: boolean,
28
+ defaultSort?: string,
29
+ defaultSortDirection?: string,
28
30
  items: Array<any>,
29
31
  modal?: {
30
32
  component: ComponentType<any>,
@@ -77,10 +79,16 @@ class EmbeddedList extends Component<Props, State> {
77
79
  * Sorts the table by the first column.
78
80
  */
79
81
  componentDidMount() {
80
- const column = _.find(this.props.columns, (c) => c.sortable !== false);
82
+ let column;
83
+
84
+ if (this.props.defaultSort) {
85
+ column = _.findWhere(this.props.columns, { name: this.props.defaultSort });
86
+ } else {
87
+ column = _.find(this.props.columns, (c) => c.sortable !== false);
88
+ }
81
89
 
82
90
  if (column) {
83
- this.onColumnClick(column);
91
+ this.onColumnClick(column, this.props.defaultSortDirection);
84
92
  }
85
93
  }
86
94
 
@@ -101,7 +109,7 @@ class EmbeddedList extends Component<Props, State> {
101
109
  *
102
110
  * @param column
103
111
  */
104
- onColumnClick(column: Column) {
112
+ onColumnClick(column: Column, direction: string = SORT_ASCENDING) {
105
113
  /*
106
114
  * We'll disable the column sorting if the table rows are draggable. Making the table rows draggable implies
107
115
  * that the sorting will be done manually. Allowing column click sorting could make things confusing.
@@ -119,7 +127,7 @@ class EmbeddedList extends Component<Props, State> {
119
127
  }
120
128
 
121
129
  const sortColumn = column.name;
122
- let sortDirection = SORT_ASCENDING;
130
+ let sortDirection = direction || SORT_ASCENDING;
123
131
 
124
132
  if (column.name === this.state.sortColumn) {
125
133
  sortDirection = this.state.sortDirection === SORT_ASCENDING ? SORT_DESCENDING : SORT_ASCENDING;
@@ -276,3 +284,8 @@ EmbeddedList.defaultProps = {
276
284
  };
277
285
 
278
286
  export default EmbeddedList;
287
+
288
+ export {
289
+ SORT_ASCENDING,
290
+ SORT_DESCENDING
291
+ };
@@ -0,0 +1,79 @@
1
+ // @flow
2
+
3
+ import React, { type ComponentType } from 'react';
4
+ import { Button, Grid, Input } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import i18n from '../i18n/i18n';
7
+ import withBatchEdit, { type BatchEditProps } from '../hooks/BatchEdit';
8
+
9
+ type Item = {
10
+ key: string,
11
+ value: string
12
+ };
13
+
14
+ type Props = BatchEditProps & {
15
+ items: Array<Item>,
16
+ onChange: (items: Array<Item>) => void
17
+ };
18
+
19
+ const KeyValuePairs: ComponentType<any> = withBatchEdit((props: Props) => (
20
+ <div>
21
+ <Button
22
+ basic
23
+ content={i18n.t('Common.buttons.add')}
24
+ icon='plus'
25
+ onClick={props.onAddItem.bind(this)}
26
+ type='button'
27
+ />
28
+ <Grid
29
+ padded='vertically'
30
+ >
31
+ { _.map(props.items, (item, index) => (
32
+ <Grid.Row
33
+ columns={3}
34
+ >
35
+ <Grid.Column
36
+ width={8}
37
+ >
38
+ <Input
39
+ fluid
40
+ onChange={props.onUpdateItem.bind(this, index, 'key')}
41
+ placeholder={i18n.t('KeyValuePairs.labels.key')}
42
+ value={item.key}
43
+ />
44
+ </Grid.Column>
45
+ <Grid.Column
46
+ width={7}
47
+ >
48
+ <Input
49
+ fluid
50
+ onChange={props.onUpdateItem.bind(this, index, 'value')}
51
+ placeholder={i18n.t('KeyValuePairs.labels.value')}
52
+ value={item.value}
53
+ />
54
+ </Grid.Column>
55
+ <Grid.Column
56
+ width={1}
57
+ >
58
+ <Button
59
+ color='red'
60
+ icon='trash'
61
+ onClick={props.onRemoveItem.bind(this, index)}
62
+ />
63
+ </Grid.Column>
64
+ </Grid.Row>
65
+ ))}
66
+ { _.isEmpty(props.items) && (
67
+ <Grid.Row
68
+ columns={1}
69
+ >
70
+ <Grid.Column>
71
+ { i18n.t('Common.labels.noRecords') }
72
+ </Grid.Column>
73
+ </Grid.Row>
74
+ )}
75
+ </Grid>
76
+ </div>
77
+ ));
78
+
79
+ export default KeyValuePairs;
@@ -18,4 +18,9 @@
18
18
  padding-top: 20%;
19
19
  padding-bottom: 20%;
20
20
  text-align: center;
21
- }
21
+ }
22
+
23
+ .lazy-document .react-pdf__Page__canvas {
24
+ width: 100% !important;
25
+ height: 100% !important;
26
+ }
@@ -1,6 +1,7 @@
1
1
  // @flow
2
2
 
3
- import React, { useState, type Node } from 'react';
3
+ import React, { useState, useEffect, type Node } from 'react';
4
+ import { Document, Page } from 'react-pdf/dist/esm/entry.webpack';
4
5
  import {
5
6
  Dimmer,
6
7
  Icon,
@@ -27,6 +28,15 @@ type Props = {
27
28
  const LazyDocument = (props: Props) => {
28
29
  const [visible, setVisible] = useState(false);
29
30
  const [dimmer, setDimmer] = useState(false);
31
+ const [contentType, setContentType] = useState('');
32
+
33
+ useEffect(() => {
34
+ if (props.src && !props.preview) {
35
+ fetch(props.src)
36
+ .then((response) => response.blob())
37
+ .then((blob) => setContentType(blob.type));
38
+ }
39
+ }, [props.preview, props.src]);
30
40
 
31
41
  if (!visible) {
32
42
  return (
@@ -65,7 +75,21 @@ const LazyDocument = (props: Props) => {
65
75
  size={props.size}
66
76
  />
67
77
  )}
68
- { !props.preview && (
78
+ { !props.preview && props.src && contentType === 'application/pdf' && (
79
+ <Image
80
+ {...props.image}
81
+ size={props.size}
82
+ >
83
+ <Document
84
+ file={props.src}
85
+ >
86
+ <Page
87
+ pageNumber={1}
88
+ />
89
+ </Document>
90
+ </Image>
91
+ )}
92
+ { !props.preview && (!props.src || contentType !== 'application/pdf') && (
69
93
  <Image
70
94
  {...props.image}
71
95
  className='placeholder-image'
@@ -70,7 +70,17 @@ const LazyVideo = (props: Props) => {
70
70
  size={props.size}
71
71
  />
72
72
  )}
73
- { !props.preview && (
73
+ { !props.preview && props.src && (
74
+ <Image
75
+ {...props.image}
76
+ size={props.size}
77
+ >
78
+ <video
79
+ src={props.src}
80
+ />
81
+ </Image>
82
+ )}
83
+ { !props.preview && !props.src && (
74
84
  <Image
75
85
  {...props.image}
76
86
  className='placeholder-image'
@@ -0,0 +1,81 @@
1
+ // @flow
2
+
3
+ import { ReferenceTablesService } from '@performant-software/shared-components';
4
+ import React, { type ComponentType, useEffect, useState } from 'react';
5
+ import { Form } from 'semantic-ui-react';
6
+ import EditModal from './EditModal';
7
+ import ReferenceCodeDropdown from './ReferenceCodeDropdown';
8
+ import ReferenceCodeFormLabel from './ReferenceCodeFormLabel';
9
+ import ReferenceTableModal from './ReferenceTableModal';
10
+
11
+ type Props = {
12
+ error?: boolean,
13
+ label?: string,
14
+ required?: boolean,
15
+ referenceTable: string
16
+ };
17
+
18
+ const ReferenceCodeFormDropdown: ComponentType<any> = (props: Props) => {
19
+ const {
20
+ error,
21
+ label,
22
+ required,
23
+ referenceTable: key,
24
+ ...rest
25
+ } = props;
26
+
27
+ const [modal, setModal] = useState(false);
28
+ const [dropdownKey, setDropdownKey] = useState(0);
29
+ const [referenceTable, setReferenceTable] = useState({ key });
30
+
31
+ /**
32
+ * Looks up the existing reference table base on the passed key.
33
+ */
34
+ useEffect(() => (
35
+ ReferenceTablesService
36
+ .fetchByKey(key)
37
+ .then(({ data }) => setReferenceTable((prevTable) => ({
38
+ ...prevTable,
39
+ ...data.reference_table
40
+ })))
41
+ ), [key]);
42
+
43
+ return (
44
+ <>
45
+ <Form.Input
46
+ error={error}
47
+ label={(
48
+ <ReferenceCodeFormLabel
49
+ label={label}
50
+ onClick={() => setModal(true)}
51
+ referenceTable={referenceTable.key}
52
+ />
53
+ )}
54
+ required={required}
55
+ >
56
+ <ReferenceCodeDropdown
57
+ {...rest}
58
+ id={referenceTable}
59
+ referenceTable={referenceTable.key}
60
+ key={dropdownKey}
61
+ />
62
+ </Form.Input>
63
+ { modal && (
64
+ <EditModal
65
+ component={ReferenceTableModal}
66
+ item={referenceTable}
67
+ onClose={() => setModal(false)}
68
+ onSave={(record) => (
69
+ ReferenceTablesService
70
+ .save(record)
71
+ .then(({ data }) => data.reference_table)
72
+ .then(() => setDropdownKey((prevKey) => prevKey + 1))
73
+ .finally(() => setModal(false))
74
+ )}
75
+ />
76
+ )}
77
+ </>
78
+ );
79
+ };
80
+
81
+ export default ReferenceCodeFormDropdown;
@@ -0,0 +1,51 @@
1
+ // @flow
2
+
3
+ import React, { type ComponentType } from 'react';
4
+ import { withTranslation } from 'react-i18next';
5
+ import {
6
+ Button,
7
+ Header,
8
+ Icon,
9
+ Popup
10
+ } from 'semantic-ui-react';
11
+ import i18n from '../i18n/i18n';
12
+
13
+ type Props = {
14
+ label: string,
15
+ onClick: () => void,
16
+ referenceTable: string
17
+ };
18
+
19
+ const ReferenceCodeFormLabel: ComponentType<any> = withTranslation()((props: Props) => (
20
+ <div>
21
+ <label
22
+ htmlFor={props.referenceTable}
23
+ >
24
+ { props.label }
25
+ </label>
26
+ <Popup
27
+ hoverable
28
+ trigger={(
29
+ <Icon
30
+ name='info circle'
31
+ style={{
32
+ marginLeft: '0.3em'
33
+ }}
34
+ />
35
+ )}
36
+ >
37
+ <Header
38
+ content={props.label}
39
+ />
40
+ <p>{ i18n.t('ReferenceCodeFormLabel.content', { name: props.label })}</p>
41
+ <Button
42
+ content={i18n.t('Common.buttons.edit')}
43
+ icon='edit'
44
+ primary
45
+ onClick={props.onClick}
46
+ />
47
+ </Popup>
48
+ </div>
49
+ ));
50
+
51
+ export default ReferenceCodeFormLabel;
@@ -0,0 +1,57 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, type ComponentType } from 'react';
4
+ import _ from 'underscore';
5
+
6
+ type Props = {
7
+ items: Array<any>,
8
+ onChange: (items: Array<any>) => void
9
+ };
10
+
11
+ const withBatchEdit = (WrappedComponent: ComponentType<any>): any => (props: Props) => {
12
+ /**
13
+ * Adds a new item to the list.
14
+ *
15
+ * @type {(function(): void)|*}
16
+ */
17
+ const onAddItem = useCallback(() => {
18
+ props.onChange([...props.items, {}]);
19
+ }, [props.items]);
20
+
21
+ /**
22
+ * Removes the item at the passed index from the list.
23
+ *
24
+ * @type {(function(*): void)|*}
25
+ */
26
+ const onRemoveItem = useCallback((findIndex) => {
27
+ props.onChange(_.reject(props.items, (item, index) => index === findIndex));
28
+ }, [props.items]);
29
+
30
+ /**
31
+ * Updates the passed attribute of the item at the passed index.
32
+ *
33
+ * @type {(function(number, string, ?Event, {value: *}): void)|*}
34
+ */
35
+ const onUpdateItem = useCallback((findIndex: number, attribute: string, e: ?Event, { value }) => {
36
+ props.onChange(_.map(props.items, (item, index) => (
37
+ index !== findIndex ? item : ({ ...item, [attribute]: value })
38
+ )));
39
+ }, [props.items]);
40
+
41
+ return (
42
+ <WrappedComponent
43
+ {...props}
44
+ onAddItem={onAddItem}
45
+ onRemoveItem={onRemoveItem}
46
+ onUpdateItem={onUpdateItem}
47
+ />
48
+ );
49
+ };
50
+
51
+ export default withBatchEdit;
52
+
53
+ export type BatchEditProps = {
54
+ onAddItem: () => void,
55
+ onRemoveItem: (index: number) => void,
56
+ onUpdateItem: (index: number, attribute: string, e: Event, data: any) => void
57
+ };
package/src/i18n/en.json CHANGED
@@ -21,6 +21,9 @@
21
21
  "errors": {
22
22
  "title": "Oops!"
23
23
  },
24
+ "labels": {
25
+ "noRecords": "No records."
26
+ },
24
27
  "messages": {
25
28
  "error": {
26
29
  "header": "Oops!"
@@ -126,6 +129,12 @@
126
129
  "showKeyboard": "Show Keyboard"
127
130
  }
128
131
  },
132
+ "KeyValuePairs": {
133
+ "labels": {
134
+ "key": "Key",
135
+ "value": "Value"
136
+ }
137
+ },
129
138
  "LazyDocument": {
130
139
  "buttons": {
131
140
  "download": "Download"
@@ -184,6 +193,9 @@
184
193
  "loginErrorHeader": "Invalid Credentials",
185
194
  "password": "Password"
186
195
  },
196
+ "ReferenceCodeFormLabel": {
197
+ "content": "The values in this list can be edited via the {{name}} reference table."
198
+ },
187
199
  "ReferenceCodeModal": {
188
200
  "labels": {
189
201
  "name": "Name"
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ export { default as ItemCollection } from './components/ItemCollection';
38
38
  export { default as ItemList } from './components/ItemList';
39
39
  export { default as Items } from './components/Items';
40
40
  export { default as KeyboardField } from './components/KeyboardField';
41
+ export { default as KeyValuePairs } from './components/KeyValuePairs';
41
42
  export { default as LazyDocument } from './components/LazyDocument';
42
43
  export { default as LazyImage } from './components/LazyImage';
43
44
  export { default as LazyVideo } from './components/LazyVideo';
@@ -59,6 +60,8 @@ export { default as NestedAccordion } from './components/NestedAccordion';
59
60
  export { default as PlayButton } from './components/PlayButton';
60
61
  export { default as PhotoViewer } from './components/PhotoViewer';
61
62
  export { default as ReferenceCodeDropdown } from './components/ReferenceCodeDropdown';
63
+ export { default as ReferenceCodeFormDropdown } from './components/ReferenceCodeFormDropdown';
64
+ export { default as ReferenceCodeFormLabel } from './components/ReferenceCodeFormLabel';
62
65
  export { default as ReferenceCodeModal } from './components/ReferenceCodeModal';
63
66
  export { default as ReferenceTableModal } from './components/ReferenceTableModal';
64
67
  export { default as ReferenceTablesList } from './components/ReferenceTablesList';
@@ -77,10 +80,14 @@ export { default as VideoPlayer } from './components/VideoPlayer';
77
80
  export { default as VideoPlayerButton } from './components/VideoPlayerButton';
78
81
  export { default as ViewXML } from './components/ViewXML';
79
82
 
83
+ // Hooks
84
+ export { default as BatchEdit } from './hooks/BatchEdit';
85
+
80
86
  // Types
81
87
  export type { EditPageProps } from './components/EditPage';
82
88
  export type { FileUploadProps } from './components/FileUploadModal';
83
89
  export type { Props as ListProps } from './components/List';
90
+ export type { BatchEditProps } from './hooks/BatchEdit';
84
91
 
85
92
  // Constants
86
93
  export { SORT_ASCENDING, SORT_DESCENDING } from './components/DataList';