@performant-software/semantic-components 1.0.10-beta.0 → 1.0.10-beta.2

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
@@ -58,6 +58,61 @@
58
58
  margin-right: 25px;
59
59
  }
60
60
 
61
+ .association-dropdown {
62
+ display: flex;
63
+ align-items: center;
64
+ flex-wrap: wrap;
65
+ width: 100%;
66
+ }
67
+ .association-dropdown .buttons {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ flex-grow: 0.1;
71
+ }
72
+ .association-dropdown .dropdown-container {
73
+ flex-grow: 1;
74
+ }
75
+ .association-dropdown .inline-dropdown {
76
+ width: 100%;
77
+ }
78
+ .association-dropdown .ui.search.selection.dropdown > input.search {
79
+ width: 100%;
80
+ }
81
+ .association-dropdown div[role="listbox"] div[role="option"] span.text {
82
+ display: block;
83
+ }
84
+ .association-dropdown div[role="alert"] {
85
+ max-width: 100%;
86
+ }
87
+ .ui.form.field .ui.input .association-dropdown input.dropdown-search-input {
88
+ width: 100%;
89
+ }
90
+ .ui.form .wide.field .ui.input .association-dropdown input.dropdown-search-input {
91
+ width: 100%;
92
+ }
93
+ .ui.dropdown:not(.button) > .default.text {
94
+ color: rgba(95, 95, 95, 0.86);
95
+ }
96
+
97
+ .date-input.ui.icon.input > i.icon.right {
98
+ cursor: pointer;
99
+ pointer-events: inherit;
100
+ left: auto;
101
+ right: 1px;
102
+ }
103
+
104
+ .fuzzy-date-modal .accuracy-container .ui.radio.checkbox {
105
+ padding-right: 10px;
106
+ }
107
+ .fuzzy-date-modal .button-container {
108
+ margin-left: 8px;
109
+ margin-top: 23px;
110
+ }
111
+
112
+ .filter-labels {
113
+ margin-top: 1em;
114
+ }
115
+
61
116
  .header .flex-end-menu {
62
117
  justify-content: flex-end;
63
118
  }
@@ -65,6 +120,12 @@
65
120
  .header .per-page-menu {
66
121
  flex-shrink: 0;
67
122
  }
123
+ .header .ui.grid.filter-labels > .row:first-child {
124
+ padding-bottom: 0px;
125
+ }
126
+ .header .ui.grid > .row:nth-child(2) {
127
+ padding-top: 0px;
128
+ }
68
129
 
69
130
  .accordion-data-list {
70
131
  margin-bottom: 1em;
@@ -110,42 +171,6 @@
110
171
  padding-right: 15px;
111
172
  }
112
173
 
113
- .association-dropdown {
114
- display: flex;
115
- align-items: center;
116
- flex-wrap: wrap;
117
- width: 100%;
118
- }
119
- .association-dropdown .buttons {
120
- display: flex;
121
- flex-wrap: wrap;
122
- flex-grow: 0.1;
123
- }
124
- .association-dropdown .dropdown-container {
125
- flex-grow: 1;
126
- }
127
- .association-dropdown .inline-dropdown {
128
- width: 100%;
129
- }
130
- .association-dropdown .ui.search.selection.dropdown > input.search {
131
- width: 100%;
132
- }
133
- .association-dropdown div[role="listbox"] div[role="option"] span.text {
134
- display: block;
135
- }
136
- .association-dropdown div[role="alert"] {
137
- max-width: 100%;
138
- }
139
- .ui.form.field .ui.input .association-dropdown input.dropdown-search-input {
140
- width: 100%;
141
- }
142
- .ui.form .wide.field .ui.input .association-dropdown input.dropdown-search-input {
143
- width: 100%;
144
- }
145
- .ui.dropdown:not(.button) > .default.text {
146
- color: rgba(95, 95, 95, 0.86);
147
- }
148
-
149
174
  .audio-player.ui.modal > .content > audio {
150
175
  width: 100%;
151
176
  }
@@ -252,13 +277,6 @@
252
277
  margin-top: 20px;
253
278
  }
254
279
 
255
- .date-input.ui.icon.input > i.icon.right {
256
- cursor: pointer;
257
- pointer-events: inherit;
258
- left: auto;
259
- right: 1px;
260
- }
261
-
262
280
  .react-calendar {
263
281
  width: 350px;
264
282
  max-width: 100%;
@@ -504,14 +522,6 @@ div.react-calendar {
504
522
  margin-bottom: 0;
505
523
  }
506
524
 
507
- .fuzzy-date-modal .accuracy-container .ui.radio.checkbox {
508
- padding-right: 10px;
509
- }
510
- .fuzzy-date-modal .button-container {
511
- margin-left: 8px;
512
- margin-top: 23px;
513
- }
514
-
515
525
  .horizontal-cards .ui.cards {
516
526
  display: flex;
517
527
  flex-direction: row;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@performant-software/semantic-components",
3
- "version": "1.0.10-beta.0",
3
+ "version": "1.0.10-beta.2",
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": "^1.0.10-beta.0",
15
+ "@performant-software/shared-components": "^1.0.10-beta.2",
16
16
  "@react-google-maps/api": "^2.8.1",
17
17
  "axios": "^0.26.1",
18
18
  "i18next": "^19.4.4",
@@ -51,7 +51,8 @@ type Props = {
51
51
  component: ComponentType<any>,
52
52
  defaults?: any,
53
53
  props?: any,
54
- onChange?: (filter: any) => Promise<any>
54
+ onChange?: (filter: any) => Promise<any>,
55
+ showLabels?: boolean
55
56
  },
56
57
 
57
58
  /**
@@ -530,7 +531,7 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
530
531
  */
531
532
  render() {
532
533
  const { filters = {} } = this.props;
533
- const { component, props } = filters;
534
+ const { component, props, showLabels } = filters;
534
535
 
535
536
  return (
536
537
  <>
@@ -541,6 +542,7 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
541
542
  active: this.isFilterActive(),
542
543
  component,
543
544
  onChange: this.onFilterChange.bind(this),
545
+ showLabels,
544
546
  props: {
545
547
  ...props,
546
548
  onCreateFilter: this.onCreateFilter.bind(this),
@@ -28,6 +28,11 @@ type Props = {
28
28
  */
29
29
  closeOnComplete?: boolean,
30
30
 
31
+ /**
32
+ * Component to render at top of modal.
33
+ */
34
+ headerComponent?: ComponentType<any>,
35
+
31
36
  /**
32
37
  * Component to render within the modal.
33
38
  */
@@ -343,6 +348,13 @@ const FileUploadModal: ComponentType<any> = (props: Props) => {
343
348
  );
344
349
  }, [statuses, props.strategy]);
345
350
 
351
+ /**
352
+ * Memoization and case correction for the <code>headerComponent</code> prop.
353
+ *
354
+ * @type {React$AbstractComponent<*, *>}
355
+ */
356
+ const HeaderComponent = useMemo(() => props.headerComponent, [props.headerComponent]);
357
+
346
358
  /**
347
359
  * Memoization and case correction for the <code>itemComponent</code> prop.
348
360
  *
@@ -404,6 +416,13 @@ const FileUploadModal: ComponentType<any> = (props: Props) => {
404
416
  <FileUpload
405
417
  onFilesAdded={onAddFiles}
406
418
  />
419
+ { HeaderComponent && (
420
+ <HeaderComponent
421
+ items={items}
422
+ onSetItems={setItems}
423
+ uploading={uploading}
424
+ />
425
+ )}
407
426
  <Item.Group
408
427
  as={Form}
409
428
  divided
@@ -0,0 +1,3 @@
1
+ .filter-labels {
2
+ margin-top: 1em;
3
+ }
@@ -0,0 +1,64 @@
1
+ // @flow
2
+
3
+ import React, { useCallback } from 'react';
4
+ import { Button, Label } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import i18n from '../i18n/i18n';
7
+ import { FilterOperatorOptions, type Filter } from './ListFilters';
8
+ import './FilterLabels.css';
9
+
10
+ type Props = {
11
+ filters: Array<Filter>,
12
+ onClear?: () => void,
13
+ onClick: (filter: Filter) => void
14
+ };
15
+
16
+ const FilterLabels = (props: Props) => {
17
+ /**
18
+ * Returns the content string for the passed filter.
19
+ *
20
+ * @type {function(*): string}
21
+ */
22
+ const getContent = useCallback((filter) => {
23
+ // Content will always container the label
24
+ const content = [filter.label];
25
+
26
+ // If an option can be found, use the text from the operator
27
+ const option = _.findWhere(FilterOperatorOptions, { key: filter.operator });
28
+ if (option) {
29
+ content.push(option.text);
30
+ }
31
+
32
+ // Append the value in quotes, if present
33
+ if (filter.value) {
34
+ content.push(`"${filter.value}"`);
35
+ }
36
+
37
+ return content.join(' ');
38
+ }, []);
39
+
40
+ return (
41
+ <Label.Group
42
+ className='filter-labels'
43
+ >
44
+ { _.map(props.filters, (filter) => (
45
+ <Label
46
+ basic
47
+ content={getContent(filter)}
48
+ onRemove={() => props.onClick(filter)}
49
+ />
50
+ ))}
51
+ { props.onClear && (
52
+ <Label
53
+ as={Button}
54
+ color='red'
55
+ content={i18n.t('FilterLabels.buttons.clear')}
56
+ icon='times'
57
+ onClick={props.onClear}
58
+ />
59
+ )}
60
+ </Label.Group>
61
+ );
62
+ };
63
+
64
+ export default FilterLabels;
@@ -6,3 +6,11 @@
6
6
  .header .per-page-menu {
7
7
  flex-shrink: 0;
8
8
  }
9
+
10
+ .header .ui.grid.filter-labels > .row:first-child {
11
+ padding-bottom: 0px;
12
+ }
13
+
14
+ .header .ui.grid > .row:nth-child(2) {
15
+ padding-top: 0px;
16
+ }
@@ -15,6 +15,7 @@ import _ from 'underscore';
15
15
  import i18n from '../i18n/i18n';
16
16
  import DropdownButton from './DropdownButton';
17
17
  import EditModal from './EditModal';
18
+ import FilterLabels from './FilterLabels';
18
19
  import './List.css';
19
20
 
20
21
  type Action = {
@@ -105,7 +106,8 @@ type Props = {
105
106
  component: Component<{}>,
106
107
  props?: any,
107
108
  state?: any,
108
- onChange: (params: any) => Promise<any>
109
+ onChange: (params: any) => Promise<any>,
110
+ showLabels?: boolean
109
111
  },
110
112
 
111
113
  /**
@@ -383,6 +385,18 @@ const useList = (WrappedComponent: ComponentType<any>) => (
383
385
  this.setState({ modalFilter: true });
384
386
  }
385
387
 
388
+ /**
389
+ * Calls the filter onChange function with the passed filter removed.
390
+ *
391
+ * @param filter
392
+ *
393
+ * @returns {*}
394
+ */
395
+ onRemoveFilter(filter) {
396
+ const { onChange, props: { item } } = this.props.filters;
397
+ return onChange({ filters: _.filter(item.filters, (f) => f.uid !== filter.uid) });
398
+ }
399
+
386
400
  /**
387
401
  * Saves the passed item and closes the add/edit modal.
388
402
  *
@@ -754,6 +768,8 @@ const useList = (WrappedComponent: ComponentType<any>) => (
754
768
  renderHeader = true;
755
769
  }
756
770
 
771
+ const hasLabels = filters && filters.showLabels && !_.isEmpty(filters.props.item.filters);
772
+
757
773
  if (!renderHeader) {
758
774
  return null;
759
775
  }
@@ -763,41 +779,58 @@ const useList = (WrappedComponent: ComponentType<any>) => (
763
779
  className='header'
764
780
  >
765
781
  <Grid
766
- columns={2}
767
- verticalAlign='bottom'
782
+ className={hasLabels ? 'filter-labels' : undefined}
783
+ verticalAlign='top'
768
784
  >
769
- <Grid.Column
770
- textAlign='left'
785
+ <Grid.Row
786
+ columns={2}
771
787
  >
772
- { _.map(buttons, this.renderButton.bind(this)) }
773
- </Grid.Column>
774
- <Grid.Column
775
- textAlign='right'
776
- >
777
- <Menu
778
- compact
779
- borderless
780
- secondary
781
- className='flex-end-menu'
788
+ <Grid.Column
789
+ textAlign='left'
790
+ >
791
+ { _.map(buttons, this.renderButton.bind(this)) }
792
+ </Grid.Column>
793
+ <Grid.Column
794
+ textAlign='right'
782
795
  >
783
- { renderListHeader && (
784
- <Menu.Menu className='list-header-menu'>
785
- { renderListHeader() }
796
+ <Menu
797
+ compact
798
+ borderless
799
+ secondary
800
+ className='flex-end-menu'
801
+ >
802
+ { renderListHeader && (
803
+ <Menu.Menu className='list-header-menu'>
804
+ { renderListHeader() }
805
+ </Menu.Menu>
806
+ )}
807
+ <Menu.Menu>
808
+ { filters && this.renderFilterButton() }
786
809
  </Menu.Menu>
787
- )}
788
- <Menu.Menu>
789
- { filters && this.renderFilterButton() }
790
- </Menu.Menu>
791
- { perPageOptions && (
792
- <Menu.Menu className='per-page-menu'>
793
- { this.renderPerPage() }
810
+ { perPageOptions && (
811
+ <Menu.Menu className='per-page-menu'>
812
+ { this.renderPerPage() }
813
+ </Menu.Menu>
814
+ )}
815
+ <Menu.Menu>
816
+ { renderSearch && renderSearch() }
794
817
  </Menu.Menu>
795
- )}
796
- <Menu.Menu>
797
- { renderSearch && renderSearch() }
798
- </Menu.Menu>
799
- </Menu>
800
- </Grid.Column>
818
+ </Menu>
819
+ </Grid.Column>
820
+ </Grid.Row>
821
+ { hasLabels && (
822
+ <Grid.Row
823
+ columns={1}
824
+ >
825
+ <Grid.Column>
826
+ <FilterLabels
827
+ filters={filters.props.item.filters}
828
+ onClear={() => filters.onChange({ filters: [] })}
829
+ onClick={(filter) => this.onRemoveFilter(filter)}
830
+ />
831
+ </Grid.Column>
832
+ </Grid.Row>
833
+ )}
801
834
  </Grid>
802
835
  </div>
803
836
  );
@@ -75,6 +75,40 @@ const FilterOperators = {
75
75
  lessThan: 'less_than'
76
76
  };
77
77
 
78
+ const FilterOperatorOptions = [{
79
+ key: FilterOperators.equal,
80
+ value: FilterOperators.equal,
81
+ text: i18n.t('ListFilters.operators.equal')
82
+ }, {
83
+ key: FilterOperators.notEqual,
84
+ value: FilterOperators.notEqual,
85
+ text: i18n.t('ListFilters.operators.notEqual')
86
+ }, {
87
+ key: FilterOperators.contain,
88
+ value: FilterOperators.contain,
89
+ text: i18n.t('ListFilters.operators.contain')
90
+ }, {
91
+ key: FilterOperators.notContain,
92
+ value: FilterOperators.notContain,
93
+ text: i18n.t('ListFilters.operators.notContain')
94
+ }, {
95
+ key: FilterOperators.empty,
96
+ value: FilterOperators.empty,
97
+ text: i18n.t('ListFilters.operators.empty')
98
+ }, {
99
+ key: FilterOperators.notEmpty,
100
+ value: FilterOperators.notEmpty,
101
+ text: i18n.t('ListFilters.operators.notEmpty')
102
+ }, {
103
+ key: FilterOperators.greaterThan,
104
+ value: FilterOperators.greaterThan,
105
+ text: i18n.t('ListFilters.operators.greaterThan')
106
+ }, {
107
+ key: FilterOperators.lessThan,
108
+ value: FilterOperators.lessThan,
109
+ text: i18n.t('ListFilters.operators.lessThan')
110
+ }];
111
+
78
112
  const OperatorsByType = {
79
113
  [FilterTypes.boolean]: [
80
114
  FilterOperators.equal
@@ -110,40 +144,6 @@ const OperatorsByType = {
110
144
  ]
111
145
  };
112
146
 
113
- const OperatorOptions = [{
114
- key: FilterOperators.equal,
115
- value: FilterOperators.equal,
116
- text: i18n.t('ListFilters.operators.equal')
117
- }, {
118
- key: FilterOperators.notEqual,
119
- value: FilterOperators.notEqual,
120
- text: i18n.t('ListFilters.operators.notEqual')
121
- }, {
122
- key: FilterOperators.contain,
123
- value: FilterOperators.contain,
124
- text: i18n.t('ListFilters.operators.contain')
125
- }, {
126
- key: FilterOperators.notContain,
127
- value: FilterOperators.notContain,
128
- text: i18n.t('ListFilters.operators.notContain')
129
- }, {
130
- key: FilterOperators.empty,
131
- value: FilterOperators.empty,
132
- text: i18n.t('ListFilters.operators.empty')
133
- }, {
134
- key: FilterOperators.notEmpty,
135
- value: FilterOperators.notEmpty,
136
- text: i18n.t('ListFilters.operators.notEmpty')
137
- }, {
138
- key: FilterOperators.greaterThan,
139
- value: FilterOperators.greaterThan,
140
- text: i18n.t('ListFilters.operators.greaterThan')
141
- }, {
142
- key: FilterOperators.lessThan,
143
- value: FilterOperators.lessThan,
144
- text: i18n.t('ListFilters.operators.lessThan')
145
- }];
146
-
147
147
  const ListFilters = (props: Props) => {
148
148
  /**
149
149
  * Returns the available operators for the passed filter type.
@@ -152,7 +152,7 @@ const ListFilters = (props: Props) => {
152
152
  */
153
153
  const getOperatorsByType = useCallback((type: string) => {
154
154
  const operators = OperatorsByType[type];
155
- return _.filter(OperatorOptions, (option) => !operators || _.contains(operators, option.key));
155
+ return _.filter(FilterOperatorOptions, (option) => !operators || _.contains(operators, option.key));
156
156
  }, []);
157
157
 
158
158
  /**
@@ -400,5 +400,10 @@ export default ListFilters;
400
400
 
401
401
  export {
402
402
  FilterTypes,
403
- FilterOperators
403
+ FilterOperators,
404
+ FilterOperatorOptions
405
+ };
406
+
407
+ export type {
408
+ Filter
404
409
  };
package/src/i18n/en.json CHANGED
@@ -140,6 +140,11 @@
140
140
  "loader": "Uploading files...",
141
141
  "title": "Upload Files"
142
142
  },
143
+ "FilterLabels": {
144
+ "buttons": {
145
+ "clear": "Clear all"
146
+ }
147
+ },
143
148
  "FuzzyDate": {
144
149
  "accuracy": {
145
150
  "date": "Date",
package/src/index.js CHANGED
@@ -96,4 +96,4 @@ export type { BatchEditProps } from './hooks/BatchEdit';
96
96
  // Constants
97
97
  export { Views as ItemViews } from './components/ItemsToggle';
98
98
  export { SORT_ASCENDING, SORT_DESCENDING } from './components/DataList';
99
- export { FilterTypes, FilterOperators } from './components/ListFilters';
99
+ export { FilterTypes, FilterOperators, FilterOperatorOptions } from './components/ListFilters';
@@ -51,7 +51,8 @@ type Props = {
51
51
  component: ComponentType<any>,
52
52
  defaults?: any,
53
53
  props?: any,
54
- onChange?: (filter: any) => Promise<any>
54
+ onChange?: (filter: any) => Promise<any>,
55
+ showLabels?: boolean
55
56
  },
56
57
 
57
58
  /**
@@ -530,7 +531,7 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
530
531
  */
531
532
  render() {
532
533
  const { filters = {} } = this.props;
533
- const { component, props } = filters;
534
+ const { component, props, showLabels } = filters;
534
535
 
535
536
  return (
536
537
  <>
@@ -541,6 +542,7 @@ const useDataList = (WrappedComponent: ComponentType<any>) => (
541
542
  active: this.isFilterActive(),
542
543
  component,
543
544
  onChange: this.onFilterChange.bind(this),
545
+ showLabels,
544
546
  props: {
545
547
  ...props,
546
548
  onCreateFilter: this.onCreateFilter.bind(this),
@@ -28,6 +28,11 @@ type Props = {
28
28
  */
29
29
  closeOnComplete?: boolean,
30
30
 
31
+ /**
32
+ * Component to render at top of modal.
33
+ */
34
+ headerComponent?: ComponentType<any>,
35
+
31
36
  /**
32
37
  * Component to render within the modal.
33
38
  */
@@ -343,6 +348,13 @@ const FileUploadModal: ComponentType<any> = (props: Props) => {
343
348
  );
344
349
  }, [statuses, props.strategy]);
345
350
 
351
+ /**
352
+ * Memoization and case correction for the <code>headerComponent</code> prop.
353
+ *
354
+ * @type {React$AbstractComponent<*, *>}
355
+ */
356
+ const HeaderComponent = useMemo(() => props.headerComponent, [props.headerComponent]);
357
+
346
358
  /**
347
359
  * Memoization and case correction for the <code>itemComponent</code> prop.
348
360
  *
@@ -404,6 +416,13 @@ const FileUploadModal: ComponentType<any> = (props: Props) => {
404
416
  <FileUpload
405
417
  onFilesAdded={onAddFiles}
406
418
  />
419
+ { HeaderComponent && (
420
+ <HeaderComponent
421
+ items={items}
422
+ onSetItems={setItems}
423
+ uploading={uploading}
424
+ />
425
+ )}
407
426
  <Item.Group
408
427
  as={Form}
409
428
  divided
@@ -0,0 +1,64 @@
1
+ // @flow
2
+
3
+ import React, { useCallback } from 'react';
4
+ import { Button, Label } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import i18n from '../i18n/i18n';
7
+ import { FilterOperatorOptions, type Filter } from './ListFilters';
8
+ import './FilterLabels.css';
9
+
10
+ type Props = {
11
+ filters: Array<Filter>,
12
+ onClear?: () => void,
13
+ onClick: (filter: Filter) => void
14
+ };
15
+
16
+ const FilterLabels = (props: Props) => {
17
+ /**
18
+ * Returns the content string for the passed filter.
19
+ *
20
+ * @type {function(*): string}
21
+ */
22
+ const getContent = useCallback((filter) => {
23
+ // Content will always container the label
24
+ const content = [filter.label];
25
+
26
+ // If an option can be found, use the text from the operator
27
+ const option = _.findWhere(FilterOperatorOptions, { key: filter.operator });
28
+ if (option) {
29
+ content.push(option.text);
30
+ }
31
+
32
+ // Append the value in quotes, if present
33
+ if (filter.value) {
34
+ content.push(`"${filter.value}"`);
35
+ }
36
+
37
+ return content.join(' ');
38
+ }, []);
39
+
40
+ return (
41
+ <Label.Group
42
+ className='filter-labels'
43
+ >
44
+ { _.map(props.filters, (filter) => (
45
+ <Label
46
+ basic
47
+ content={getContent(filter)}
48
+ onRemove={() => props.onClick(filter)}
49
+ />
50
+ ))}
51
+ { props.onClear && (
52
+ <Label
53
+ as={Button}
54
+ color='red'
55
+ content={i18n.t('FilterLabels.buttons.clear')}
56
+ icon='times'
57
+ onClick={props.onClear}
58
+ />
59
+ )}
60
+ </Label.Group>
61
+ );
62
+ };
63
+
64
+ export default FilterLabels;