@onehat/ui 0.2.49 → 0.2.50

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.50",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -28,6 +28,9 @@ const IconButton = React.forwardRef((props, ref) => {
28
28
  justifyContent="center"
29
29
  alignItems="center"
30
30
  p={2}
31
+ _disabled={{
32
+ bg: 'trueGray.300',
33
+ }}
31
34
  {...props}
32
35
  >
33
36
  {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();
@@ -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
@@ -17,351 +17,385 @@ 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,
36
37
 
37
38
  // withData
38
39
  Repository,
39
- } = props,
40
- styles = UiGlobals.styles;
40
+ } = props;
41
41
 
42
- let modal,
42
+ let modal = null,
43
43
  topToolbar = null;
44
44
 
45
45
  if (useFilters && Repository) {
46
+
46
47
  const
48
+ // aliases
49
+ {
50
+ defaultFilters: modelDefaultFilters,
51
+ filterTypes: modelFilterTypes,
52
+ titles: modelTitles,
53
+ virtualFields: modelVirtualFields,
54
+ excludeFields: modelExcludeFields,
55
+ filteringDisabled: modelFilteringDisabled,
56
+ } = Repository.getSchema().model,
57
+
58
+ // determine the starting filters
59
+ startingFilters = !_.isEmpty(customFilters) ? customFilters : // custom filters override component filters
60
+ !_.isEmpty(defaultFilters) ? defaultFilters : // component filters override model filters
61
+ !_.isEmpty(modelDefaultFilters) ? modelDefaultFilters : [],
62
+ isUsingCustomFilters = startingFilters === customFilters,
47
63
  [isReady, setIsReady] = useState(false),
48
64
  [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:
65
+ getFormattedFilter = (filter) => {
66
+ let formatted = null;
67
+ if (_.isString(filter)) {
68
+ const field = filter;
69
+ formatted = {
70
+ field,
71
+ title: modelTitles[field],
72
+ type: modelFilterTypes[field],
73
+ value: null, // value starts as null
74
+ };
75
+ } else if (_.isPlainObject(filter)) {
76
+ // already formatted
77
+ formatted = filter;
87
78
  }
88
- },
89
- getFilterType = (ix) => {
79
+ return formatted;
80
+ };
81
+
82
+ let formattedStartingFilters = [],
83
+ startingSlots = [];
84
+ if (!isReady) {
85
+ // Generate initial starting state
86
+ if (searchAllText) {
87
+ formattedStartingFilters.push({ field: 'q', title: 'Search all text fields', type: 'Input', value: null, });
88
+ }
89
+ _.each(startingFilters, (filter) => {
90
90
  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;
91
+ formattedFilter = getFormattedFilter(filter),
92
+ field = formattedFilter.field;
93
+ formattedStartingFilters.push(formattedFilter);
94
+ if (!isUsingCustomFilters) {
95
+ startingSlots.push(field);
98
96
  }
99
- return filterType;
100
- },
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:
97
+ });
98
+ if (startingSlots.length < minFilters) {
99
+ for (let i = startingSlots.length; i < minFilters; i++) {
100
+ startingSlots.push(null);
120
101
  }
121
- return field;
102
+ }
103
+ }
104
+
105
+ const
106
+ [filters, setFilters] = useState(formattedStartingFilters), // array of formatted filters
107
+ [slots, setSlots] = useState(startingSlots), // array of field names user is currently filtering on; blank slots have a null entry in array
108
+ [modalFilters, setModalFilters] = useState([]),
109
+ [modalSlots, setModalSlots] = useState([]),
110
+ [previousFilterNames, setPreviousFilterNames] = useState([]), // names of filters the repository used last query
111
+ canAddSlot = (() => {
112
+ let canAdd = true;
113
+ if (!!maxFilters && modalSlots.length >= maxFilters) {
114
+ canAdd = false; // maxFilters has been reached
115
+ }
116
+ if (canAdd) {
117
+ _.each(modalSlots, (field) => {
118
+ if (_.isNil(field)) {
119
+ canAdd = false; // at least one slot has no selected field to filter
120
+ return false;
121
+ }
122
+ });
123
+ }
124
+ return canAdd;
125
+ })(),
126
+ canDeleteSlot = modalSlots.length > minFilters,
127
+ onAddSlot = () => {
128
+ if (!canAddSlot) {
129
+ return;
130
+ }
131
+ const newSlots = _.clone(modalSlots);
132
+ newSlots.push(null);
133
+ setModalSlots(newSlots);
122
134
  },
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:
135
+ onDeleteSlot = () => {
136
+ if (!canDeleteSlot) {
137
+ return;
145
138
  }
146
- return value;
139
+ const
140
+ newFilters = _.clone(modalFilters),
141
+ newSlots = _.clone(modalSlots);
142
+ newFilters.pop();
143
+ newSlots.pop();
144
+ setModalFilters(newFilters);
145
+ setModalSlots(newSlots);
146
+ },
147
+ onFilterChangeValue = (field, value) => {
148
+ // handler for when a filter value changes
149
+ const newFilters = [];
150
+ _.each(filters, (filter) => {
151
+ if (filter.field === field) {
152
+ filter.value = value;
153
+ }
154
+ newFilters.push(filter);
155
+ });
156
+ setFilters(newFilters);
157
+ },
158
+ onClearFilters = () => {
159
+ // Clears values for all active filters
160
+ const newFilters = [];
161
+ _.each(filters, (filter) => {
162
+ filter.value = null;
163
+ newFilters.push(filter);
164
+ });
165
+ setFilters(newFilters);
147
166
  },
148
- getIsFilterRange = (ix) => {
149
- const filterType = getFilterType(0);
167
+ getFilterByField = (field) => {
168
+ return _.find(filters, (filter) => {
169
+ return filter.field === field;
170
+ });
171
+ },
172
+ getFilterValue = (field) => {
173
+ const filter = getFilterByField(field);
174
+ return filter?.value;
175
+ },
176
+ getFilterType = (field) => {
177
+ // Finds filter type for the field name, from active filters
178
+ const filter = getFilterByField(field);
179
+ return filter?.type;
180
+ },
181
+ getIsFilterRange = (field) => {
182
+ // determines if filter is a "range" filter
183
+ const filterType = getFilterType(field);
150
184
  return inArray(filterType, ['NumberRange', 'DateRange']);
151
185
  },
152
186
  renderFilters = () => {
153
- if (!Repository) {
154
- return null;
155
- }
156
-
157
187
  const
158
- {
159
- titles = [],
160
- filterTypes = [],
161
- virtualFields = [],
162
- excludeFields = [],
163
- } = Repository.getSchema().model,
164
188
  filterProps = {
165
189
  mx: 1,
166
190
  },
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;
191
+ filterElements = [];
192
+ _.each(filters, (filter, ix) => {
193
+ let Element,
194
+ elementProps = {};
195
+ const {
196
+ field,
197
+ type: filterType,
198
+ } = filter;
199
+
200
+ if (_.isString(filterType)) {
201
+ Element = getComponentFromType(filterType);
202
+ if (filterType === 'Input') {
203
+ elementProps.autoSubmit = true;
201
204
  }
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>;
205
+ } else if (_.isPlainObject(filterType)) {
206
+ const {
207
+ type,
208
+ ...p
209
+ } = filterType;
210
+ elementProps = p;
211
+ Element = getComponentFromType(type);
212
+ if (type === 'Input') {
213
+ elementProps.autoSubmit = true;
216
214
  }
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
-
215
+ }
216
+ if (field === 'q') {
217
+ elementProps.flex = 1;
218
+ elementProps.minWidth = 100;
219
+ }
220
+
221
+ const tooltip = filter.tooltip || filter.title || modelTitles[filter.field];
222
+ let filterElement = <Element
223
+ key={'filter-' + field}
224
+ tooltip={tooltip}
225
+ placeholder={tooltip}
226
+ value={getFilterValue(field)}
227
+ onChangeValue={(value) => onFilterChangeValue(field, value)}
228
+ {...filterProps}
229
+ {...elementProps}
230
+ />;
231
+ if (showLabels && field !== 'q') {
232
+ filterElement = <Row key={'label-' + ix} alignItems="center">
233
+ <Text ml={2} mr={1} fontSize={UiGlobals.styles.FILTER_LABEL_FONTSIZE}>{modelTitles[field]}</Text>
234
+ {filterElement}
235
+ </Row>;
236
+ }
237
+ filterElements.push(filterElement);
238
+ });
238
239
  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
240
  };
272
241
 
273
242
  useEffect(() => {
274
- const
275
- filters = [],
276
- newFilterFields = [];
243
+ // Whenever the filters change in some way, make repository conform to these new filters
244
+ const newRepoFilters = [];
277
245
 
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);
300
- }
246
+ if (isUsingCustomFilters) {
247
+ _.each(filters, (filter) => {
248
+ const repoFiltersFromFilter = filter.getRepoFilters(value);
249
+ _.each(repoFiltersFromFilter, (repoFilter) => { // one custom filter might generate multiple filters for the repository
250
+ newRepoFilters.push(repoFilter);
251
+ });
252
+ });
253
+ } else {
254
+ const newFilterNames = [];
255
+ _.each(filters, (filter) => {
256
+ const {
257
+ field,
258
+ value,
259
+ } = filter,
260
+ isFilterRange = getIsFilterRange(field);
261
+ if (isFilterRange) {
262
+ if (!!value) {
263
+ const
264
+ highField = field + ' <=',
265
+ lowField = field + ' >=',
266
+ highValue = value.high,
267
+ lowValue = value.low;
268
+ newFilterNames.push(highField);
269
+ newFilterNames.push(lowField);
270
+ newRepoFilters.push({ name: highField, value: highValue, });
271
+ newRepoFilters.push({ name: lowField, value: lowValue, });
272
+ }
273
+ } else {
274
+ newFilterNames.push(field);
275
+ newRepoFilters.push({ name: field, value, });
276
+ }
277
+ });
278
+
279
+ // Go through previousFilterNames and see if any are no longer used.
280
+ _.each(previousFilterNames, (name) => {
281
+ if (!inArray(name, newFilterNames)) {
282
+ newRepoFilters.push({ name, value: null, }); // no longer used, so set it to null so it'll be deleted
283
+ }
284
+ });
285
+ setPreviousFilterNames(newFilterNames);
301
286
  }
302
- setFilterFields(newFilterFields);
303
287
 
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, });
308
- }
309
- });
310
-
311
- Repository.filter(filters, null, false); // false so other filters remain
288
+ Repository.filter(newRepoFilters, null, false); // false so other filters remain
289
+
290
+ if (searchAllText && Repository.searchAncillary && !Repository.hasBaseParam('searchAncillary')) {
291
+ Repository.setBaseParam('searchAncillary', true);
292
+ }
312
293
 
313
294
  if (!isReady) {
314
295
  setIsReady(true);
315
296
  }
316
297
 
317
- }, [filter1Field, filter2Field, filter3Field, filter4Field, filter5Field,
318
- filter1Value, filter2Value, filter3Value, filter4Value, filter5Value,
319
- filterQValue,]);
298
+ }, [filters]);
320
299
 
321
300
  if (!isReady) {
322
301
  return null;
323
302
  }
324
303
 
304
+ const
305
+ renderedFilters = renderFilters(),
306
+ hasFilters = !!renderedFilters.length;
307
+ topToolbar = <Toolbar justifyContent="space-between" alignItems="center">
308
+ <Text pr={2} userSelect="none">Filters:{hasFilters ? '' : ' None'}</Text>
309
+ {renderedFilters}
310
+ <Row flex={hasFilters ? null : 1} justifyContent="flex-end">
311
+ <IconButton
312
+ key="clear"
313
+ _icon={{
314
+ as: Ban,
315
+ }}
316
+ ml={1}
317
+ onPress={onClearFilters}
318
+ tooltip="Clear all filters"
319
+ />
320
+ {showFilterSelector && !isUsingCustomFilters && <IconButton
321
+ key="gear"
322
+ _icon={{
323
+ as: Gear,
324
+ }}
325
+ ml={1}
326
+ onPress={() => {
327
+ setModalFilters(filters);
328
+ setModalSlots(slots);
329
+ setIsFilterSelectorShown(true);
330
+ }}
331
+ tooltip="Swap filters"
332
+ />}
333
+ </Row>
334
+ </Toolbar>;
325
335
 
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
- });
336
+ if (isFilterSelectorShown) { // this is always false when isUsingCustomFilters
337
+ // Build the modal to select the filters
335
338
  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
- }
339
+ modalFilterElements = [],
340
+ usedFields = _.filter(_.map(modalFilters, (filter) => {
341
+ return filter?.field;
342
+ }), el => !_.isNil(el)),
343
+ formStartingValues = {};
344
+
345
+ _.each(modalSlots, (field, ix) => {
346
+
347
+ // Create the data for the combobox.
348
+ const data = [];
349
+ _.each(modelFilterTypes, (filterType, filterField) => {
350
+ if (inArray(filterField, usedFields) && field !== filterField) { // Show all filters not yet applied, but include the current filter
351
+ return; // skip, since it's already been used
352
+ }
353
+ data.push([ filterField, modelTitles[filterField] ]);
354
+ });
355
+
356
+ const
357
+ ixPlusOne = (ix +1),
358
+ filterName = 'filter' + ixPlusOne;
359
+
360
+ modalFilterElements.push({
361
+ key: filterName,
362
+ name: filterName,
363
+ type: 'Combo',
364
+ label: 'Filter ' + ixPlusOne,
365
+ data,
366
+ onChange: (value) => {
367
+ const
368
+ newFilters = _.clone(modalFilters),
369
+ newSlots = _.clone(modalSlots),
370
+ i = searchAllText ? ixPlusOne : ix; // compensate for 'q' filter's possible presence
371
+
372
+ if (newFilters[i]?.value) {
373
+ newFilters[i].value = value;
374
+ } else {
375
+ newFilters[i] = getFormattedFilter(value);
376
+ }
377
+ newSlots[ix] = value;
378
+
379
+ setModalFilters(newFilters);
380
+ setModalSlots(newSlots);
381
+ },
382
+ });
383
+
384
+ formStartingValues[filterName] = field;
385
+ });
386
+
387
+ if (canAddSlot || canDeleteSlot) {
388
+ modalFilterElements.push({
389
+ type: 'PlusMinusButton',
390
+ name: 'plusMinusButton',
391
+ plusHandler: onAddSlot,
392
+ minusHandler: onDeleteSlot,
393
+ isPlusDisabled: !canAddSlot,
394
+ isMinusDisabled: !canDeleteSlot,
395
+ justifyContent: 'flex-end',
396
+ });
397
+ }
363
398
 
364
- if (isFilterSelectorShown) {
365
399
  modal = <Modal
366
400
  isOpen={true}
367
401
  onClose={() => setIsFilterSelectorShown(false)}
@@ -369,99 +403,60 @@ export default function withFilters(WrappedComponent) {
369
403
  <Column bg="#fff" w={500}>
370
404
  <FormPanel
371
405
  title="Filter Selector"
372
- instructions="Please select which fields to filter by. You may select up to five filters. Leave blank for no filter."
406
+ instructions="Please select which fields to filter by."
373
407
  flex={1}
374
- startingValues={{
375
- filter1: filter1Field,
376
- filter2: filter2Field,
377
- filter3: filter3Field,
378
- filter4: filter4Field,
379
- filter5: filter5Field,
380
- }}
408
+ startingValues={formStartingValues}
381
409
  items={[
382
410
  {
383
411
  type: 'Column',
384
412
  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
413
+ items: modalFilterElements,
414
+ },
433
415
  ]}
434
416
  onCancel={(e) => {
435
- setFilter1FieldForModal(filter1Field);
436
- setFilter2FieldForModal(filter2Field);
437
- setFilter3FieldForModal(filter3Field);
438
- setFilter4FieldForModal(filter4Field);
439
- setFilter5FieldForModal(filter5Field);
417
+ // Just close the modal
440
418
  setIsFilterSelectorShown(false);
441
419
  }}
442
420
  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);
421
+ // Conform filters to this new choice of filters
422
+
423
+ const
424
+ newFilters = [],
425
+ newSlots = [];
426
+
427
+ if (searchAllText) {
428
+ newFilters.push(filters[0]);
458
429
  }
459
- if (filter5FieldForModal !== filter5Field) {
460
- setFilter5Field(filter5FieldForModal);
461
- setFilter5Value(null);
430
+
431
+ // Conform the filters to the modal selection
432
+ _.each(data, (field, ix) => {
433
+ if (_.isEmpty(field) || !ix.match(/^filter/)) {
434
+ return;
435
+ }
436
+
437
+ const newFilter = getFormattedFilter(field);
438
+
439
+ newFilters.push(newFilter);
440
+ newSlots.push(field);
441
+ });
442
+
443
+ if (newSlots.length < minFilters) {
444
+ // Add more slots until we get to minFilters
445
+ for(let i = newSlots.length; i < minFilters; i++) {
446
+ newSlots.push(null);
447
+ }
462
448
  }
449
+
450
+ setFilters(newFilters);
451
+ setSlots(newSlots);
452
+
453
+ // Close the modal
463
454
  setIsFilterSelectorShown(false);
464
455
  }}
456
+ onReset={() => {
457
+ setModalFilters(filters);
458
+ setModalSlots(slots);
459
+ }}
465
460
  />
466
461
  </Column>
467
462
  </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,