@onehat/ui 0.2.49 → 0.2.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,6 +5,7 @@ import {
5
5
  Spinner,
6
6
  Tooltip,
7
7
  } from 'native-base';
8
+ import styles from '../../Constants/Styles.js';
8
9
 
9
10
  const IconButton = React.forwardRef((props, ref) => {
10
11
  const {
@@ -28,6 +29,9 @@ const IconButton = React.forwardRef((props, ref) => {
28
29
  justifyContent="center"
29
30
  alignItems="center"
30
31
  p={2}
32
+ _disabled={{
33
+ bg: styles.ICON_BUTTON_BG_DISABLED,
34
+ }}
31
35
  {...props}
32
36
  >
33
37
  {icon}
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import {
3
+ Row,
4
+ Text,
5
+ } from 'native-base';
6
+ import IconButton from './IconButton.js';
7
+ import Plus from '../Icons/Plus.js';
8
+ import Minus from '../Icons/Minus.js';
9
+
10
+
11
+ const PlusMinusButton = React.forwardRef((props, ref) => {
12
+
13
+ const {
14
+ isPlusDisabled = false,
15
+ isMinusDisabled = false,
16
+ plusHandler = () => {},
17
+ minusHandler = () => {},
18
+ } = props;
19
+
20
+ return <Row {...props}>
21
+ <Row alignItems="center">
22
+ <IconButton
23
+ icon={<Minus color="#fff" />}
24
+ onPress={minusHandler}
25
+ bg="primary.200"
26
+ isDisabled={isMinusDisabled}
27
+ />
28
+ <IconButton
29
+ icon={<Plus color="#fff" />}
30
+ onPress={plusHandler}
31
+ bg="primary.200"
32
+ isDisabled={isPlusDisabled}
33
+ ml={1}
34
+ />
35
+ </Row>
36
+ </Row>;
37
+
38
+ });
39
+
40
+ export default PlusMinusButton;
@@ -74,6 +74,7 @@ import _ from 'lodash';
74
74
  minValue={minValue}
75
75
  maxValue={maxValue}
76
76
  tooltip={(tooltip ? tooltip + ' ' : '') + 'Low'}
77
+ maxWidth={120}
77
78
  />
78
79
  <Text px={2} userSelect="none">to</Text>
79
80
  <Number
@@ -83,6 +84,7 @@ import _ from 'lodash';
83
84
  minValue={minValue}
84
85
  maxValue={maxValue}
85
86
  tooltip={(tooltip ? tooltip + ' ' : '') + 'High'}
87
+ maxWidth={120}
86
88
  />
87
89
  </Row>;
88
90
  },
@@ -178,17 +178,32 @@ export function Combo(props) {
178
178
  const {
179
179
  relatedTarget
180
180
  } = e;
181
+
182
+ // If user focused on the trigger and text is blank, clear the selection and close the menu
183
+ if ((triggerRef.current === relatedTarget || triggerRef.current.contains(relatedTarget)) && (_.isEmpty(textValue) || _.isNil(textValue))) {
184
+ setSelection([]); // delete current selection
185
+ hideMenu();
186
+ return;
187
+ }
181
188
 
182
- // If user clicked on the menu or trigger, ignore this blur
183
- if (menuRef.current?.contains(relatedTarget) || triggerRef.current.contains(relatedTarget)) {
189
+ // If user focused on the menu or trigger, ignore this blur
190
+ if (triggerRef.current === relatedTarget ||
191
+ triggerRef.current.contains(relatedTarget) ||
192
+ menuRef.current=== relatedTarget ||
193
+ menuRef.current?.contains(relatedTarget)) {
184
194
  return;
185
195
  }
186
196
 
187
197
  if (!relatedTarget ||
188
- (!inputRef.current.contains(relatedTarget) && triggerRef.current !== relatedTarget && (!menuRef.current || !menuRef.current.contains(relatedTarget)))) {
198
+ (
199
+ !inputRef.current.contains(relatedTarget) &&
200
+ triggerRef.current !== relatedTarget &&
201
+ (!menuRef.current || !menuRef.current.contains(relatedTarget))
202
+ )
203
+ ) {
189
204
  hideMenu();
190
205
  }
191
- if (textValue === '') {
206
+ if (_.isEmpty(textValue) || _.isNil(textValue)) {
192
207
  setSelection([]); // delete current selection
193
208
 
194
209
  } else if (isManuallyEnteringText) {
@@ -221,12 +236,17 @@ export function Combo(props) {
221
236
  inputRef.current.focus();
222
237
  },
223
238
  onTriggerBlur = (e) => {
224
- if (!isMenuShown) {
225
- return;
226
- }
227
239
  const {
228
240
  relatedTarget
229
241
  } = e;
242
+
243
+ if (_.isEmpty(textValue) || _.isNil(textValue)) {
244
+ setSelection([]); // delete current selection
245
+ }
246
+
247
+ if (!isMenuShown) {
248
+ return;
249
+ }
230
250
  if (!relatedTarget ||
231
251
  (!inputRef.current.contains(relatedTarget) && triggerRef.current !== relatedTarget && !menuRef.current.contains(relatedTarget))) {
232
252
  hideMenu();
@@ -277,6 +277,7 @@ export function DateElement(props) {
277
277
  setTop(top + height);
278
278
  setLeft(left);
279
279
  }}
280
+ w={props.w || null}
280
281
  />
281
282
  {/* <Pressable
282
283
  flex={1}
@@ -119,7 +119,7 @@ function NumberElement(props) {
119
119
  isIncrementDisabled = typeof maxValue !== 'undefined' && value === maxValue,
120
120
  isDecrementDisabled = typeof minValue !== 'undefined' && (value === minValue || (!value && minValue === 0));
121
121
 
122
- return <Row flex={1} h="100%" p={0} borderWidth={1} borderColor="trueGray.400" borderRadius={6}>
122
+ return <Row flex={1} h="100%" p={0} borderWidth={1} borderColor="trueGray.400" borderRadius={6} {...props}>
123
123
  <IconButton
124
124
  icon={<Icon as={Minus} color={isDecrementDisabled ? 'disabled' : 'trueGray.500'} />}
125
125
  onPress={onDecrement}
@@ -62,7 +62,8 @@ function Form(props) {
62
62
  validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
63
63
  footerProps = {},
64
64
  buttonGroupProps = {}, // buttons in footer
65
- onBack,
65
+ onBack,
66
+ onReset,
66
67
  onViewMode,
67
68
  additionalButtons = [],
68
69
  ancillaryComponents = [],
@@ -360,7 +361,12 @@ function Form(props) {
360
361
  let element = <Element
361
362
  name={name}
362
363
  value={value}
363
- onChangeValue={onChange}
364
+ onChangeValue={(value) => {
365
+ onChange(value); // form onChange handler
366
+ if (propsToPass.onChange) {
367
+ propsToPass.onChange(value); // item onChange handler
368
+ }
369
+ }}
364
370
  onBlur={onBlur}
365
371
  selectorId={selectorId}
366
372
  selectorSelected={selectorSelected}
@@ -507,7 +513,12 @@ function Form(props) {
507
513
  <Button.Group space={2} {...buttonGroupProps}>
508
514
  {!isViewOnly && <IconButton
509
515
  key="resetBtn"
510
- onPress={() => reset()}
516
+ onPress={() => {
517
+ if (onReset) {
518
+ onReset();
519
+ }
520
+ reset();
521
+ }}
511
522
  icon={<Rotate color="#fff" />}
512
523
  />}
513
524
  {!isViewOnly && onCancel && <Button
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, } from 'react';
1
+ import { useState, useEffect, useId, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  Modal,
@@ -17,351 +17,425 @@ import _ from 'lodash';
17
17
 
18
18
  // Filters only work with Repository; not data array
19
19
 
20
+ // Yet to do:
21
+ // - Save user choice in cookie for next time this component loads
22
+ //
23
+ // Model defaultFilters should adjust to this new arrangement
24
+
20
25
  export default function withFilters(WrappedComponent) {
21
26
  return (props) => {
22
27
  const {
28
+ // config
23
29
  useFilters = false,
24
30
  searchAllText = true,
25
31
  showLabels = true,
26
- filter1StartingField = '',
27
- filter2StartingField = '',
28
- filter3StartingField = '',
29
- filter4StartingField = '',
30
- filter5StartingField = '',
31
- filter1StartingValue = null,
32
- filter2StartingValue = null,
33
- filter3StartingValue = null,
34
- filter4StartingValue = null,
35
- filter5StartingValue = null,
32
+ showFilterSelector = true,
33
+ defaultFilters = [], // likely a list of field names, possibly could be of shape below
34
+ customFilters = [], // of shape: { title, type, field, value, getRepoFilters(value) }
35
+ minFilters = 3,
36
+ maxFilters = 6,
37
+ getSaved,
38
+ setSaved,
36
39
 
37
40
  // withData
38
41
  Repository,
39
- } = props,
40
- styles = UiGlobals.styles;
42
+ } = props;
41
43
 
42
- let modal,
44
+ let modal = null,
43
45
  topToolbar = null;
44
46
 
45
47
  if (useFilters && Repository) {
48
+
46
49
  const
50
+ // aliases
51
+ {
52
+ defaultFilters: modelDefaultFilters,
53
+ filterTypes: modelFilterTypes,
54
+ titles: modelTitles,
55
+ virtualFields: modelVirtualFields,
56
+ excludeFields: modelExcludeFields,
57
+ filteringDisabled: modelFilteringDisabled,
58
+ } = Repository.getSchema().model,
59
+ id = useId(),
60
+
61
+ // determine the starting filters
62
+ startingFilters = !_.isEmpty(customFilters) ? customFilters : // custom filters override component filters
63
+ !_.isEmpty(defaultFilters) ? defaultFilters : // component filters override model filters
64
+ !_.isEmpty(modelDefaultFilters) ? modelDefaultFilters : [],
65
+ isUsingCustomFilters = startingFilters === customFilters,
47
66
  [isReady, setIsReady] = useState(false),
48
67
  [isFilterSelectorShown, setIsFilterSelectorShown] = useState(false),
49
- [filter1Field, setFilter1Field] = useState(filter1StartingField || Repository?.getSchema().model.defaultFilters?.[0] || null),
50
- [filter2Field, setFilter2Field] = useState(filter2StartingField || Repository?.getSchema().model.defaultFilters?.[1] || null),
51
- [filter3Field, setFilter3Field] = useState(filter3StartingField || Repository?.getSchema().model.defaultFilters?.[2] || null),
52
- [filter4Field, setFilter4Field] = useState(filter4StartingField || Repository?.getSchema().model.defaultFilters?.[3] || null),
53
- [filter5Field, setFilter5Field] = useState(filter5StartingField || Repository?.getSchema().model.defaultFilters?.[4] || null),
54
- [filter1FieldForModal, setFilter1FieldForModal] = useState(filter1Field),
55
- [filter2FieldForModal, setFilter2FieldForModal] = useState(filter2Field),
56
- [filter3FieldForModal, setFilter3FieldForModal] = useState(filter3Field),
57
- [filter4FieldForModal, setFilter4FieldForModal] = useState(filter4Field),
58
- [filter5FieldForModal, setFilter5FieldForModal] = useState(filter5Field),
59
- [filterQValue, setFilterQValue] = useState(null),
60
- [filter1Value, setFilter1Value] = useState(filter1StartingValue),
61
- [filter2Value, setFilter2Value] = useState(filter2StartingValue),
62
- [filter3Value, setFilter3Value] = useState(filter3StartingValue),
63
- [filter4Value, setFilter4Value] = useState(filter4StartingValue),
64
- [filter5Value, setFilter5Value] = useState(filter5StartingValue),
65
- [filterFields, setFilterFields] = useState([]),
66
- onFilterChange = (ix, value) => {
67
- switch(ix) {
68
- case 'q':
69
- setFilterQValue(value);
70
- break;
71
- case 0:
72
- setFilter1Value(value);
73
- break;
74
- case 1:
75
- setFilter2Value(value);
76
- break;
77
- case 2:
78
- setFilter3Value(value);
79
- break;
80
- case 3:
81
- setFilter4Value(value);
82
- break;
83
- case 4:
84
- setFilter5Value(value);
85
- break;
86
- default:
68
+ getFormattedFilter = (filter) => {
69
+ let formatted = null;
70
+ if (_.isString(filter)) {
71
+ const field = filter;
72
+ formatted = {
73
+ field,
74
+ title: modelTitles[field],
75
+ type: modelFilterTypes[field],
76
+ value: null, // value starts as null
77
+ };
78
+ } else if (_.isPlainObject(filter)) {
79
+ // already formatted
80
+ formatted = filter;
87
81
  }
88
- },
89
- getFilterType = (ix) => {
82
+ return formatted;
83
+ };
84
+
85
+ let formattedStartingFilters = [],
86
+ startingSlots = [];
87
+ if (!isReady) {
88
+ // Generate initial starting state
89
+ if (searchAllText) {
90
+ formattedStartingFilters.push({ field: 'q', title: 'Search all text fields', type: 'Input', value: null, });
91
+ }
92
+ _.each(startingFilters, (filter) => {
90
93
  const
91
- filterField = getFilterField(ix),
92
- filterTypeDefinition = Repository.getSchema().model.filterTypes[filterField];
93
- let filterType;
94
- if (_.isString(filterTypeDefinition)) {
95
- filterType = filterTypeDefinition;
96
- } else {
97
- filterType = filterTypeDefinition.type;
94
+ formattedFilter = getFormattedFilter(filter),
95
+ field = formattedFilter.field;
96
+ formattedStartingFilters.push(formattedFilter);
97
+ if (!isUsingCustomFilters) {
98
+ startingSlots.push(field);
99
+ }
100
+ });
101
+ if (startingSlots.length < minFilters) {
102
+ for (let i = startingSlots.length; i < minFilters; i++) {
103
+ startingSlots.push(null);
104
+ }
105
+ }
106
+ }
107
+
108
+ const
109
+ [filters, setFiltersRaw] = useState(formattedStartingFilters), // array of formatted filters
110
+ [slots, setSlots] = useState(startingSlots), // array of field names user is currently filtering on; blank slots have a null entry in array
111
+ [modalFilters, setModalFilters] = useState([]),
112
+ [modalSlots, setModalSlots] = useState([]),
113
+ [previousFilterNames, setPreviousFilterNames] = useState([]), // names of filters the repository used last query
114
+ canAddSlot = (() => {
115
+ let canAdd = true;
116
+ if (!!maxFilters && modalSlots.length >= maxFilters) {
117
+ canAdd = false; // maxFilters has been reached
118
+ }
119
+ if (canAdd) {
120
+ _.each(modalSlots, (field) => {
121
+ if (_.isNil(field)) {
122
+ canAdd = false; // at least one slot has no selected field to filter
123
+ return false;
124
+ }
125
+ });
126
+ }
127
+ return canAdd;
128
+ })(),
129
+ canDeleteSlot = modalSlots.length > minFilters,
130
+ onAddSlot = () => {
131
+ if (!canAddSlot) {
132
+ return;
98
133
  }
99
- return filterType;
134
+ const newSlots = _.clone(modalSlots);
135
+ newSlots.push(null);
136
+ setModalSlots(newSlots);
100
137
  },
101
- getFilterField = (ix) => {
102
- let field;
103
- switch(ix) {
104
- case 0:
105
- field = filter1Field;
106
- break;
107
- case 1:
108
- field = filter2Field;
109
- break;
110
- case 2:
111
- field = filter3Field;
112
- break;
113
- case 3:
114
- field = filter4Field;
115
- break;
116
- case 4:
117
- field = filter5Field;
118
- break;
119
- default:
138
+ onDeleteSlot = () => {
139
+ if (!canDeleteSlot) {
140
+ return;
120
141
  }
121
- return field;
142
+ const
143
+ newFilters = _.clone(modalFilters),
144
+ newSlots = _.clone(modalSlots);
145
+ newFilters.pop();
146
+ newSlots.pop();
147
+ setModalFilters(newFilters);
148
+ setModalSlots(newSlots);
122
149
  },
123
- getFilterValue = (ix) => {
124
- let value;
125
- switch(ix) {
126
- case 'q':
127
- value = filterQValue;
128
- break;
129
- case 0:
130
- value = filter1Value;
131
- break;
132
- case 1:
133
- value = filter2Value;
134
- break;
135
- case 2:
136
- value = filter3Value;
137
- break;
138
- case 3:
139
- value = filter4Value;
140
- break;
141
- case 4:
142
- value = filter5Value;
143
- break;
144
- default:
150
+ setFilters = (filters, doSetSlots = true, save = true) => {
151
+ setFiltersRaw(filters);
152
+
153
+ if (doSetSlots) {
154
+ const newSlots = [];
155
+ _.each(filters, (filter, ix) => {
156
+ if (searchAllText && ix === 0) {
157
+ return; // skip
158
+ }
159
+ newSlots.push(filter.field);
160
+ });
161
+ if (newSlots.length < minFilters) {
162
+ // Add more slots until we get to minFilters
163
+ for(let i = newSlots.length; i < minFilters; i++) {
164
+ newSlots.push(null);
165
+ }
166
+ }
167
+ setSlots(newSlots);
168
+ }
169
+ if (save && setSaved) {
170
+ setSaved(id + '-filters', filters);
145
171
  }
146
- return value;
147
172
  },
148
- getIsFilterRange = (ix) => {
149
- const filterType = getFilterType(0);
173
+ onFilterChangeValue = (field, value) => {
174
+ // handler for when a filter value changes
175
+ const newFilters = [];
176
+ _.each(filters, (filter) => {
177
+ if (filter.field === field) {
178
+ filter.value = value;
179
+ }
180
+ newFilters.push(filter);
181
+ });
182
+ setFilters(newFilters, false);
183
+ },
184
+ onClearFilters = () => {
185
+ // Clears values for all active filters
186
+ const newFilters = [];
187
+ _.each(filters, (filter) => {
188
+ filter.value = null;
189
+ newFilters.push(filter);
190
+ });
191
+ setFilters(newFilters, false);
192
+ },
193
+ getFilterByField = (field) => {
194
+ return _.find(filters, (filter) => {
195
+ return filter.field === field;
196
+ });
197
+ },
198
+ getFilterValue = (field) => {
199
+ const filter = getFilterByField(field);
200
+ return filter?.value;
201
+ },
202
+ getFilterType = (field) => {
203
+ // Finds filter type for the field name, from active filters
204
+ const filter = getFilterByField(field);
205
+ return filter?.type;
206
+ },
207
+ getIsFilterRange = (field) => {
208
+ // determines if filter is a "range" filter
209
+ const filterType = getFilterType(field);
150
210
  return inArray(filterType, ['NumberRange', 'DateRange']);
151
211
  },
152
212
  renderFilters = () => {
153
- if (!Repository) {
154
- return null;
155
- }
156
-
157
213
  const
158
- {
159
- titles = [],
160
- filterTypes = [],
161
- virtualFields = [],
162
- excludeFields = [],
163
- } = Repository.getSchema().model,
164
214
  filterProps = {
165
215
  mx: 1,
166
216
  },
167
- filterElements = [],
168
- addFilter = (fieldName, ix) => {
169
- if (ix === 'q') {
170
- // special case
171
- const Element = getComponentFromType('Input');
172
- filterElements.push(<Element
173
- key={ix}
174
- tooltip="Search all text fields"
175
- placeholder="All text fields"
176
- value={getFilterValue(ix)}
177
- autoSubmit={true}
178
- onChangeValue={(value) => onFilterChange(ix, value)}
179
- {...filterProps}
180
- />);
181
- return;
182
- }
183
- if (inArray(fieldName, virtualFields) || inArray(fieldName, excludeFields)) {
184
- return; // skip
185
- }
186
- const filterType = filterTypes[fieldName];
187
- let Element,
188
- modelProps = {};
189
- if (_.isString(filterType)) {
190
- Element = getComponentFromType(filterType);
191
- } else if (_.isPlainObject(filterType)) {
192
- const {
193
- type,
194
- ...p
195
- } = filterType;
196
- modelProps = p;
197
- Element = getComponentFromType(type);
198
- }
199
- if (!Element) {
200
- debugger;
217
+ filterElements = [];
218
+ _.each(filters, (filter, ix) => {
219
+ let Element,
220
+ elementProps = {};
221
+ const {
222
+ field,
223
+ type: filterType,
224
+ } = filter;
225
+
226
+ if (_.isString(filterType)) {
227
+ Element = getComponentFromType(filterType);
228
+ if (filterType === 'Input') {
229
+ elementProps.autoSubmit = true;
201
230
  }
202
- let filterElement = <Element
203
- key={'element-' + ix}
204
- tooltip={titles[fieldName]}
205
- placeholder={titles[fieldName]}
206
- value={getFilterValue(ix)}
207
- onChangeValue={(value) => onFilterChange(ix, value)}
208
- {...filterProps}
209
- {...modelProps}
210
- />;
211
- if (showLabels) {
212
- filterElement = <Row key={'label-' + ix} alignItems="center">
213
- <Text ml={2} mr={1} fontSize={styles.FILTER_LABEL_FONTSIZE}>{titles[fieldName]}</Text>
214
- {filterElement}
215
- </Row>;
231
+ } else if (_.isPlainObject(filterType)) {
232
+ const {
233
+ type,
234
+ ...p
235
+ } = filterType;
236
+ elementProps = p;
237
+ Element = getComponentFromType(type);
238
+ if (type === 'Input') {
239
+ elementProps.autoSubmit = true;
216
240
  }
217
- filterElements.push(filterElement);
218
- };
219
- if (searchAllText) {
220
- addFilter(null, 'q');
221
- }
222
- if (filter1Field) {
223
- addFilter(filter1Field, 0);
224
- }
225
- if (filter2Field) {
226
- addFilter(filter2Field, 1);
227
- }
228
- if (filter3Field) {
229
- addFilter(filter3Field, 2);
230
- }
231
- if (filter4Field) {
232
- addFilter(filter4Field, 3);
233
- }
234
- if (filter5Field) {
235
- addFilter(filter5Field, 4);
236
- }
237
-
241
+ }
242
+ if (field === 'q') {
243
+ elementProps.flex = 1;
244
+ elementProps.minWidth = 100;
245
+ }
246
+
247
+ const tooltip = filter.tooltip || filter.title || modelTitles[filter.field];
248
+ let filterElement = <Element
249
+ key={'filter-' + field}
250
+ tooltip={tooltip}
251
+ placeholder={tooltip}
252
+ value={getFilterValue(field)}
253
+ onChangeValue={(value) => onFilterChangeValue(field, value)}
254
+ {...filterProps}
255
+ {...elementProps}
256
+ />;
257
+ if (showLabels && field !== 'q') {
258
+ filterElement = <Row key={'label-' + ix} alignItems="center">
259
+ <Text ml={2} mr={1} fontSize={UiGlobals.styles.FILTER_LABEL_FONTSIZE}>{modelTitles[field]}</Text>
260
+ {filterElement}
261
+ </Row>;
262
+ }
263
+ filterElements.push(filterElement);
264
+ });
238
265
  return filterElements;
239
- },
240
- setFiltersOn = (ix, filters, newFilterFields) => {
241
- const
242
- filterIxField = getFilterField(ix),
243
- filterIxValue = getFilterValue(ix),
244
- isFilterRange = getIsFilterRange(ix);
245
- let highValue,
246
- lowValue,
247
- highField,
248
- lowField;
249
- if (isFilterRange && !!filterIxValue) {
250
- highValue = filterIxValue.high;
251
- lowValue = filterIxValue.low;
252
- highField = filterIxField + ' <=';
253
- lowField = filterIxField + ' >=';
254
-
255
- newFilterFields.push(highField);
256
- newFilterFields.push(lowField);
257
- filters.push({ name: highField, value: highValue, });
258
- filters.push({ name: lowField, value: lowValue, });
259
- } else {
260
- newFilterFields.push(filterIxField);
261
- filters.push({ name: filterIxField, value: filterIxValue, });
262
- }
263
- },
264
- onClearFilters = () => {
265
- setFilterQValue(null);
266
- setFilter1Value(null);
267
- setFilter2Value(null);
268
- setFilter3Value(null);
269
- setFilter4Value(null);
270
- setFilter5Value(null);
271
266
  };
272
267
 
273
268
  useEffect(() => {
274
- const
275
- filters = [],
276
- newFilterFields = [];
269
+ (async () => {
270
+
271
+ // Whenever the filters change in some way, make repository conform to these new filters
272
+ const newRepoFilters = [];
273
+ let filtersToUse = filters
277
274
 
278
- // For each filter field that is set, add a real filter for it
279
- if (filter1Field) {
280
- setFiltersOn(0, filters, newFilterFields);
281
- }
282
- if (filter2Field) {
283
- setFiltersOn(1, filters, newFilterFields);
284
- }
285
- if (filter3Field) {
286
- setFiltersOn(2, filters, newFilterFields);
287
- }
288
- if (filter4Field) {
289
- setFiltersOn(3, filters, newFilterFields);
290
- }
291
- if (filter5Field) {
292
- setFiltersOn(4, filters, newFilterFields);
293
- }
294
- if (searchAllText && !_.isEmpty(filterQValue)) {
295
- const q = 'q';
296
- newFilterFields.push(q);
297
- filters.push({ name: q, value: filterQValue, });
298
- if (Repository.searchAncillary && !Repository.hasBaseParam('searchAncillary')) {
299
- Repository.setBaseParam('searchAncillary', true);
275
+ if (!isReady && getSaved) {
276
+ const savedFilters = await getSaved(id + '-filters');
277
+ if (!_.isEmpty(savedFilters)) {
278
+ // load saved filters
279
+ filtersToUse = savedFilters;
280
+ setFilters(savedFilters, true, false); // false to skip save
281
+ }
300
282
  }
301
- }
302
- setFilterFields(newFilterFields);
303
283
 
304
- // Go through previous filterFields see if any are no longer used. If no longer used, set it to null so it'll be deleted
305
- _.each(filterFields, (filterField) => {
306
- if (!inArray(filterField, newFilterFields)) {
307
- filters.push({ name: filterField, value: null, });
284
+ if (isUsingCustomFilters) {
285
+ _.each(filtersToUse, (filter) => {
286
+ const repoFiltersFromFilter = filter.getRepoFilters(value);
287
+ _.each(repoFiltersFromFilter, (repoFilter) => { // one custom filter might generate multiple filters for the repository
288
+ newRepoFilters.push(repoFilter);
289
+ });
290
+ });
291
+ } else {
292
+ const newFilterNames = [];
293
+ _.each(filtersToUse, (filter) => {
294
+ const {
295
+ field,
296
+ value,
297
+ } = filter,
298
+ isFilterRange = getIsFilterRange(field);
299
+ if (isFilterRange) {
300
+ if (!!value) {
301
+ const
302
+ highField = field + ' <=',
303
+ lowField = field + ' >=',
304
+ highValue = value.high,
305
+ lowValue = value.low;
306
+ newFilterNames.push(highField);
307
+ newFilterNames.push(lowField);
308
+ newRepoFilters.push({ name: highField, value: highValue, });
309
+ newRepoFilters.push({ name: lowField, value: lowValue, });
310
+ }
311
+ } else {
312
+ newFilterNames.push(field);
313
+ newRepoFilters.push({ name: field, value, });
314
+ }
315
+ });
316
+
317
+ // Go through previousFilterNames and see if any are no longer used.
318
+ _.each(previousFilterNames, (name) => {
319
+ if (!inArray(name, newFilterNames)) {
320
+ newRepoFilters.push({ name, value: null, }); // no longer used, so set it to null so it'll be deleted
321
+ }
322
+ });
323
+ setPreviousFilterNames(newFilterNames);
308
324
  }
309
- });
310
-
311
- Repository.filter(filters, null, false); // false so other filters remain
312
325
 
313
- if (!isReady) {
314
- setIsReady(true);
315
- }
326
+ Repository.filter(newRepoFilters, null, false); // false so other filters remain
316
327
 
317
- }, [filter1Field, filter2Field, filter3Field, filter4Field, filter5Field,
318
- filter1Value, filter2Value, filter3Value, filter4Value, filter5Value,
319
- filterQValue,]);
328
+ if (searchAllText && Repository.searchAncillary && !Repository.hasBaseParam('searchAncillary')) {
329
+ Repository.setBaseParam('searchAncillary', true);
330
+ }
331
+
332
+ if (!isReady) {
333
+ setIsReady(true);
334
+ }
335
+ })();
336
+ }, [filters]);
320
337
 
321
338
  if (!isReady) {
322
339
  return null;
323
340
  }
324
341
 
342
+ const
343
+ renderedFilters = renderFilters(),
344
+ hasFilters = !!renderedFilters.length;
345
+ topToolbar = <Toolbar justifyContent="space-between" alignItems="center">
346
+ <Text pr={2} userSelect="none">Filters:{hasFilters ? '' : ' None'}</Text>
347
+ {renderedFilters}
348
+ <Row flex={hasFilters ? null : 1} justifyContent="flex-end">
349
+ <IconButton
350
+ key="clear"
351
+ _icon={{
352
+ as: Ban,
353
+ }}
354
+ ml={1}
355
+ onPress={onClearFilters}
356
+ tooltip="Clear all filters"
357
+ />
358
+ {showFilterSelector && !isUsingCustomFilters && <IconButton
359
+ key="gear"
360
+ _icon={{
361
+ as: Gear,
362
+ }}
363
+ ml={1}
364
+ onPress={() => {
365
+ const f = filters;
366
+ const s = slots;
367
+ setModalFilters(filters);
368
+ setModalSlots(slots);
369
+ setIsFilterSelectorShown(true);
370
+ }}
371
+ tooltip="Swap filters"
372
+ />}
373
+ </Row>
374
+ </Toolbar>;
325
375
 
326
- let filterComboProps = {};
327
- if (Repository?.getSchema().model.titles) {
328
- filterComboProps.data = [];
329
- const schemaModel = Repository.getSchema().model;
330
- _.each(schemaModel.titles, (title, fieldName) => {
331
- if (!inArray(fieldName, schemaModel.virtualFields) && !inArray(fieldName, schemaModel.excludeFields) && !inArray(fieldName, schemaModel.filteringDisabled)) {
332
- filterComboProps.data.push([fieldName, title]);
333
- }
334
- });
376
+ if (isFilterSelectorShown) { // this is always false when isUsingCustomFilters
377
+ // Build the modal to select the filters
335
378
  const
336
- renderedFilters = renderFilters(),
337
- hasFilters = !!renderedFilters.length;
338
- topToolbar = <Toolbar justifyContent="space-between" alignItems="center">
339
- <Text pr={2} userSelect="none">Filters:{hasFilters ? '' : ' None'}</Text>
340
- {renderedFilters}
341
- <Row flex={hasFilters ? null : 1} justifyContent="flex-end">
342
- <IconButton
343
- key="clear"
344
- _icon={{
345
- as: Ban,
346
- }}
347
- ml={1}
348
- onPress={onClearFilters}
349
- tooltip="Clear all filters"
350
- />
351
- <IconButton
352
- key="gear"
353
- _icon={{
354
- as: Gear,
355
- }}
356
- ml={1}
357
- onPress={() => setIsFilterSelectorShown(true)}
358
- tooltip="Swap filters"
359
- />
360
- </Row>
361
- </Toolbar>;
362
- }
379
+ modalFilterElements = [],
380
+ usedFields = _.filter(_.map(modalFilters, (filter) => {
381
+ return filter?.field;
382
+ }), el => !_.isNil(el)),
383
+ formStartingValues = {};
384
+
385
+ _.each(modalSlots, (field, ix) => {
386
+
387
+ // Create the data for the combobox.
388
+ const data = [];
389
+ _.each(modelFilterTypes, (filterType, filterField) => {
390
+ if (inArray(filterField, usedFields) && field !== filterField) { // Show all filters not yet applied, but include the current filter
391
+ return; // skip, since it's already been used
392
+ }
393
+ data.push([ filterField, modelTitles[filterField] ]);
394
+ });
395
+
396
+ const
397
+ ixPlusOne = (ix +1),
398
+ filterName = 'filter' + ixPlusOne;
399
+
400
+ modalFilterElements.push({
401
+ key: filterName,
402
+ name: filterName,
403
+ type: 'Combo',
404
+ label: 'Filter ' + ixPlusOne,
405
+ data,
406
+ onChange: (value) => {
407
+ const
408
+ newFilters = _.clone(modalFilters),
409
+ newSlots = _.clone(modalSlots),
410
+ i = searchAllText ? ixPlusOne : ix; // compensate for 'q' filter's possible presence
411
+
412
+ if (newFilters[i]?.value) {
413
+ newFilters[i].value = value;
414
+ } else {
415
+ newFilters[i] = getFormattedFilter(value);
416
+ }
417
+ newSlots[ix] = value;
418
+
419
+ setModalFilters(newFilters);
420
+ setModalSlots(newSlots);
421
+ },
422
+ });
423
+
424
+ formStartingValues[filterName] = field;
425
+ });
426
+
427
+ if (canAddSlot || canDeleteSlot) {
428
+ modalFilterElements.push({
429
+ type: 'PlusMinusButton',
430
+ name: 'plusMinusButton',
431
+ plusHandler: onAddSlot,
432
+ minusHandler: onDeleteSlot,
433
+ isPlusDisabled: !canAddSlot,
434
+ isMinusDisabled: !canDeleteSlot,
435
+ justifyContent: 'flex-end',
436
+ });
437
+ }
363
438
 
364
- if (isFilterSelectorShown) {
365
439
  modal = <Modal
366
440
  isOpen={true}
367
441
  onClose={() => setIsFilterSelectorShown(false)}
@@ -369,99 +443,48 @@ export default function withFilters(WrappedComponent) {
369
443
  <Column bg="#fff" w={500}>
370
444
  <FormPanel
371
445
  title="Filter Selector"
372
- instructions="Please select which fields to filter by. You may select up to five filters. Leave blank for no filter."
446
+ instructions="Please select which fields to filter by."
373
447
  flex={1}
374
- startingValues={{
375
- filter1: filter1Field,
376
- filter2: filter2Field,
377
- filter3: filter3Field,
378
- filter4: filter4Field,
379
- filter5: filter5Field,
380
- }}
448
+ startingValues={formStartingValues}
381
449
  items={[
382
450
  {
383
451
  type: 'Column',
384
452
  flex: 1,
385
- items: [
386
- {
387
- type: 'Combo',
388
- label: 'Filter 1',
389
- name: 'filter1',
390
- onChangeValue: (value) => {
391
- setFilter1FieldForModal(value);
392
- },
393
- ...filterComboProps,
394
- },
395
- {
396
- type: 'Combo',
397
- label: 'Filter 2',
398
- name: 'filter2',
399
- onChangeValue: (value) => {
400
- setFilter2FieldForModal(value);
401
- },
402
- ...filterComboProps,
403
- },
404
- {
405
- type: 'Combo',
406
- label: 'Filter 3',
407
- name: 'filter3',
408
- onChangeValue: (value) => {
409
- setFilter3FieldForModal(value);
410
- },
411
- ...filterComboProps,
412
- },
413
- {
414
- type: 'Combo',
415
- label: 'Filter 4',
416
- name: 'filter4',
417
- onChangeValue: (value) => {
418
- setFilter4FieldForModal(value);
419
- },
420
- ...filterComboProps,
421
- },
422
- {
423
- type: 'Combo',
424
- label: 'Filter 5',
425
- name: 'filter5',
426
- onChangeValue: (value) => {
427
- setFilter5FieldForModal(value);
428
- },
429
- ...filterComboProps,
430
- },
431
- ],
432
- }, // END Column
453
+ items: modalFilterElements,
454
+ },
433
455
  ]}
434
456
  onCancel={(e) => {
435
- setFilter1FieldForModal(filter1Field);
436
- setFilter2FieldForModal(filter2Field);
437
- setFilter3FieldForModal(filter3Field);
438
- setFilter4FieldForModal(filter4Field);
439
- setFilter5FieldForModal(filter5Field);
457
+ // Just close the modal
440
458
  setIsFilterSelectorShown(false);
441
459
  }}
442
460
  onSave={(data, e) => {
443
- if (filter1FieldForModal !== filter1Field) {
444
- setFilter1Field(filter1FieldForModal);
445
- setFilter1Value(null);
446
- }
447
- if (filter2FieldForModal !== filter2Field) {
448
- setFilter2Field(filter2FieldForModal);
449
- setFilter2Value(null);
450
- }
451
- if (filter3FieldForModal !== filter3Field) {
452
- setFilter3Field(filter3FieldForModal);
453
- setFilter3Value(null);
454
- }
455
- if (filter4FieldForModal !== filter4Field) {
456
- setFilter4Field(filter4FieldForModal);
457
- setFilter4Value(null);
458
- }
459
- if (filter5FieldForModal !== filter5Field) {
460
- setFilter5Field(filter5FieldForModal);
461
- setFilter5Value(null);
461
+ // Conform filters to this new choice of filters
462
+
463
+ const newFilters = [];
464
+
465
+ if (searchAllText) {
466
+ newFilters.push(filters[0]);
462
467
  }
468
+
469
+ // Conform the filters to the modal selection
470
+ _.each(data, (field, ix) => {
471
+ if (_.isEmpty(field) || !ix.match(/^filter/)) {
472
+ return;
473
+ }
474
+
475
+ const newFilter = getFormattedFilter(field);
476
+ newFilters.push(newFilter);
477
+ });
478
+
479
+ setFilters(newFilters);
480
+
481
+ // Close the modal
463
482
  setIsFilterSelectorShown(false);
464
483
  }}
484
+ onReset={() => {
485
+ setModalFilters(filters);
486
+ setModalSlots(slots);
487
+ }}
465
488
  />
466
489
  </Column>
467
490
  </Modal>;
@@ -35,6 +35,7 @@ import Number from './Form/Field/Number.js';
35
35
  import NumberRange from './Filter/NumberRange.js';
36
36
  import Panel from './Panel/Panel.js';
37
37
  // import Picker from '../Components/Panel/Picker.js';
38
+ import PlusMinusButton from './Buttons/PlusMinusButton.js';
38
39
  import RadioGroup from './Form/Field/RadioGroup/RadioGroup.js';
39
40
  import TabPanel from './Panel/TabPanel.js';
40
41
  import Tag from './Form/Field/Combo/Tag.js';
@@ -82,6 +83,7 @@ const components = {
82
83
  NumberRange,
83
84
  Panel,
84
85
  // Picker,
86
+ PlusMinusButton,
85
87
  RadioGroup,
86
88
  TabPanel,
87
89
  Tag,
@@ -61,6 +61,7 @@ const defaults = {
61
61
  GRID_TOOLBAR_ITEMS_COLOR: 'trueGray.800',
62
62
  GRID_TOOLBAR_ITEMS_DISABLED_COLOR: 'disabled',
63
63
  GRID_TOOLBAR_ITEMS_ICON_SIZE: 'sm',
64
+ ICON_BUTTON_BG_DISABLED: 'trueGray.200',
64
65
  PANEL_FOOTER_BG: 'primary.100', // :alpha.50
65
66
  PANEL_HEADER_BG: 'primary.100',
66
67
  PANEL_HEADER_BG_VERTICAL: 'primary.100',