@onehat/ui 0.3.0 → 0.3.3

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.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  "@k-renwick/colour-mixer": "^1.2.1",
31
31
  "@reduxjs/toolkit": "^1.9.5",
32
32
  "js-cookie": "^3.0.5",
33
+ "inflector-js": "^1.0.1",
33
34
  "native-base": "^3.4.28",
34
35
  "react-hook-form": "^7.45.0",
35
36
  "react-redux": "^8.1.2",
@@ -12,13 +12,9 @@ import {
12
12
  } from '../../../../Constants/UiModes.js';
13
13
  import UiGlobals from '../../../../UiGlobals.js';
14
14
  import Input from '../Input.js';
15
- import withAlert from '../../../Hoc/withAlert.js';
16
15
  import withData from '../../../Hoc/withData.js';
17
- import withEvents from '../../../Hoc/withEvents.js';
18
- import withPresetButtons from '../../../Hoc/withPresetButtons.js';
19
16
  import withSelection from '../../../Hoc/withSelection.js';
20
17
  import withValue from '../../../Hoc/withValue.js';
21
- import withWindowedEditor from '../../../Hoc/withWindowedEditor.js';
22
18
  import emptyFn from '../../../../Functions/emptyFn.js';
23
19
  import { Grid, WindowedGridEditor } from '../../../Grid/Grid.js';
24
20
  import IconButton from '../../../Buttons/IconButton.js';
@@ -53,9 +49,6 @@ export function ComboComponent(props) {
53
49
  idIx,
54
50
  displayIx,
55
51
 
56
- // withEvents
57
- onEvent,
58
-
59
52
  // withSelection
60
53
  selection,
61
54
  setSelection,
@@ -70,11 +63,11 @@ export function ComboComponent(props) {
70
63
  inputRef = useRef(),
71
64
  triggerRef = useRef(),
72
65
  menuRef = useRef(),
73
- // isTyping = useRef(false),
74
- // typingTimeout = useRef(),
66
+ isManuallyEnteringText = useRef(false),
67
+ savedSearch = useRef(null),
68
+ typingTimeout = useRef(),
75
69
  [isMenuShown, setIsMenuShown] = useState(false),
76
70
  [isRendered, setIsRendered] = useState(false),
77
- [isManuallyEnteringText, setIsManuallyEnteringText] = useState(false), // when typing a value, not using trigger/grid
78
71
  [textValue, setTextValue] = useState(''),
79
72
  [width, setWidth] = useState(0),
80
73
  [height, setHeight] = useState(null),
@@ -126,6 +119,18 @@ export function ComboComponent(props) {
126
119
  }
127
120
  setIsMenuShown(false);
128
121
  },
122
+ getIsManuallyEnteringText = () => {
123
+ return isManuallyEnteringText.current;
124
+ },
125
+ setIsManuallyEnteringText = (bool) => {
126
+ isManuallyEnteringText.current = bool;
127
+ },
128
+ getSavedSearch = () => {
129
+ return savedSearch.current;
130
+ },
131
+ setSavedSearch = (val) => {
132
+ savedSearch.current = val;
133
+ },
129
134
  toggleMenu = () => {
130
135
  setIsMenuShown(!isMenuShown);
131
136
  },
@@ -168,22 +173,20 @@ export function ComboComponent(props) {
168
173
  return;
169
174
  }
170
175
  setTextValue(value);
171
- // searchForMatches(value);
172
- // setIsManuallyEnteringText(true);
173
-
174
- // isTyping.current = true;
175
- // if (typingTimeout.current) {
176
- // clearTimeout(typingTimeout.current);
177
- // }
178
- // typingTimeout.current = setTimeout(() => {
179
- // isTyping.current = false;
180
- // }, 300);
176
+
177
+ setIsManuallyEnteringText(true);
178
+ clearTimeout(typingTimeout.current);
179
+ typingTimeout.current = setTimeout(() => {
180
+ searchForMatches(value);
181
+ }, 300);
181
182
  },
182
183
  onInputBlur = (e) => {
183
184
  const {
184
185
  relatedTarget
185
186
  } = e;
186
187
 
188
+ setIsManuallyEnteringText(false);
189
+
187
190
  // If user focused on the trigger and text is blank, clear the selection and close the menu
188
191
  if ((triggerRef.current === relatedTarget || triggerRef.current.contains(relatedTarget)) && (_.isEmpty(textValue) || _.isNil(textValue))) {
189
192
  setSelection([]); // delete current selection
@@ -211,7 +214,7 @@ export function ComboComponent(props) {
211
214
  if (_.isEmpty(textValue) || _.isNil(textValue)) {
212
215
  setSelection([]); // delete current selection
213
216
 
214
- } else if (isManuallyEnteringText) {
217
+ } else if (getIsManuallyEnteringText()) {
215
218
  if (forceSelection) {
216
219
  setSelection([]); // delete current selection
217
220
  hideMenu();
@@ -257,43 +260,57 @@ export function ComboComponent(props) {
257
260
  hideMenu();
258
261
  }
259
262
  },
260
- searchForMatches = (value) => {
261
-
262
- // Do a search for this value
263
- // TODO: Do fuzzy seach for results
264
- // Would have to do differently for remote repositories
265
- // Narrow results in grid to those that match the filter.
266
- // If filter is cleared, show original results.
263
+ searchForMatches = async (value) => {
267
264
 
268
265
  let found;
269
266
  if (Repository) {
270
267
 
271
- debugger;
272
-
273
268
  // Set filter
274
269
  let filter = {};
275
- if (value !== '') {
270
+ if (Repository.isRemote) {
271
+ let searchField = 'q';
272
+
273
+ // Check to see if displayField is a real field
274
+ const
275
+ schema = Repository.getSchema(),
276
+ displayFieldName = schema.model.displayProperty;
277
+ displayFieldDef = schema.getPropertyDefinition(displayFieldName);
278
+ if (!displayFieldDef.isVirtual) {
279
+ searchField = displayFieldName + ' LIKE';
280
+ }
276
281
 
277
- // TODO: Want to build a search functionality that shows results in combo grid
282
+ if (!_.isEmpty(value)) {
283
+ value += '%';
284
+ }
278
285
 
279
- if (Repository.isRemote) {
280
- // 'q' fuzzy search from server
281
-
282
-
283
- } else {
284
- // Fuzzy search with getBy filter function
285
- filter = (entity) => {
286
- const
287
- displayValue = entity.displayValue,
288
- regex = new RegExp('^' + value);
289
- return displayValue.match(regex);
290
- };
286
+ await Repository.filter(searchField, value);
287
+ if (!this.isAutoLoad) {
288
+ await Repository.reload();
291
289
  }
292
- }
293
- Repository.filter(filter);
294
290
 
295
- // TODO: Auto-select if filter produces only one result
291
+ } else {
292
+ throw Error('Not sure if this works yet!');
293
+
294
+ // Fuzzy search with getBy filter function
295
+ filter = (entity) => {
296
+ const
297
+ displayValue = entity.displayValue,
298
+ regex = new RegExp('^' + value);
299
+ return displayValue.match(regex);
300
+ };
301
+ Repository.filter(filter);
302
+ }
296
303
 
304
+ setSavedSearch(value);
305
+ const numResults = Repository.entities.length;
306
+ if (!numResults) {
307
+ setSelection([]);
308
+ } else if (numResults === 1) {
309
+ const selection = Repository.entities[0];
310
+ setSelection([selection]);
311
+ setSavedSearch(null);
312
+ }
313
+
297
314
  } else {
298
315
  // Search through data
299
316
  found = _.find(data, (item) => {
@@ -333,6 +350,10 @@ export function ComboComponent(props) {
333
350
  }, [isRendered]);
334
351
 
335
352
  useEffect(() => {
353
+ if (getIsManuallyEnteringText() && getSavedSearch()) {
354
+ return
355
+ }
356
+
336
357
  // Adjust text input to match selection
337
358
  let localTextValue = getDisplayFromSelection(selection);
338
359
  if (!_.isEqual(localTextValue, textValue)) {
@@ -498,7 +519,6 @@ export function ComboComponent(props) {
498
519
  {...props}
499
520
  disablePresetButtons={!isEditor}
500
521
  disablePagination={disablePagination}
501
- fireEvent={onEvent}
502
522
  setSelection={(selection) => {
503
523
  // Decorator fn to add local functionality
504
524
  // Close the menu when row is selected on grid
@@ -15,9 +15,9 @@ const
15
15
  ellipsizeMode="head"
16
16
  flex={1}
17
17
  fontSize={styles.FORM_TEXT_FONTSIZE}
18
- bg={styles.FORM_TEXT_BG}
18
+ minHeight='40px'
19
19
  px={3}
20
- py={1}
20
+ py={2}
21
21
  {...props}
22
22
  >{props.value}</Text>;
23
23
  },
@@ -199,7 +199,8 @@ function Form(props) {
199
199
  } = fieldState;
200
200
  let editorProps = {};
201
201
  if (!editor) {
202
- editor = model.editorTypes[fieldName];
202
+ const propertyDef = fieldName && Repository?.getSchema().getPropertyDefinition(fieldName);
203
+ editor = propertyDef[fieldName].editoType;
203
204
  if (_.isPlainObject(editor)) {
204
205
  const {
205
206
  type,
@@ -259,14 +260,16 @@ function Form(props) {
259
260
  } = item;
260
261
  let editorTypeProps = {};
261
262
 
262
- const model = Repository?.getSchema().model;
263
- if (!type && Repository) {
264
- const
265
- editorTypes = model.editorTypes,
263
+ const propertyDef = name && Repository?.getSchema().getPropertyDefinition(name);
264
+ if (propertyDef?.isEditingDisabled) {
265
+ isEditable = false;
266
+ }
267
+ if (isEditable && !type && Repository) {
268
+ const
266
269
  {
267
270
  type: t,
268
271
  ...p
269
- } = editorTypes[name];
272
+ } = propertyDef.editorType;
270
273
  type = t;
271
274
  editorTypeProps = p;
272
275
  }
@@ -287,8 +290,8 @@ function Form(props) {
287
290
  return <Element key={ix} title={title} {...defaults} {...propsToPass} {...editorTypeProps}>{children}</Element>;
288
291
  }
289
292
 
290
- if (!label && Repository && model[name].title) {
291
- label = model[name].title;
293
+ if (!label && Repository && propertyDef.title) {
294
+ label = propertyDef.title;
292
295
  }
293
296
 
294
297
  if (isViewOnly || !isEditable) {
@@ -482,7 +485,6 @@ function Form(props) {
482
485
  let formComponents,
483
486
  editor;
484
487
  if (editorType === EDITOR_TYPE__INLINE) {
485
- // for inline editor
486
488
  formComponents = buildFromColumnsConfig();
487
489
  editor = <ScrollView
488
490
  horizontal={true}
@@ -494,8 +496,14 @@ function Form(props) {
494
496
  borderTopColor="primary.100"
495
497
  borderBottomColor="primary.100"
496
498
  >{formComponents}</ScrollView>;
499
+ } else if (editorType === EDITOR_TYPE__PLAIN) {
500
+ formComponents = buildFromItems();
501
+ const formAncillaryComponents = buildAncillary();
502
+ editor = <>
503
+ <Row>{formComponents}</Row>
504
+ <Column pt={4}>{formAncillaryComponents}</Column>
505
+ </>;
497
506
  } else {
498
- // for all other editor types
499
507
  formComponents = buildFromItems();
500
508
  const formAncillaryComponents = buildAncillary();
501
509
  editor = <ScrollView _web={{ height: 1 }} width="100%" pb={1}>
@@ -5,6 +5,13 @@ import {
5
5
  Row,
6
6
  Text,
7
7
  } from 'native-base';
8
+ import {
9
+ EDITOR_TYPE__PLAIN,
10
+ } from '../../Constants/Editor.js';
11
+ import {
12
+ FILTER_TYPE_ANCILLARY
13
+ } from '../../Constants/Filters.js';
14
+ import Inflector from 'inflector-js';
8
15
  import inArray from '../../Functions/inArray.js';
9
16
  import getComponentFromType from '../../Functions/getComponentFromType.js';
10
17
  import IconButton from '../Buttons/IconButton.js';
@@ -50,11 +57,7 @@ export default function withFilters(WrappedComponent) {
50
57
  // aliases
51
58
  {
52
59
  defaultFilters: modelDefaultFilters,
53
- filterTypes: modelFilterTypes,
54
- titles: modelTitles,
55
- virtualFields: modelVirtualFields,
56
- excludeFields: modelExcludeFields,
57
- filteringDisabled: modelFilteringDisabled,
60
+ ancillaryFilters: modelAncillaryFilters,
58
61
  } = Repository.getSchema().model,
59
62
  id = useId(),
60
63
 
@@ -63,16 +66,32 @@ export default function withFilters(WrappedComponent) {
63
66
  !_.isEmpty(defaultFilters) ? defaultFilters : // component filters override model filters
64
67
  !_.isEmpty(modelDefaultFilters) ? modelDefaultFilters : [],
65
68
  isUsingCustomFilters = startingFilters === customFilters,
69
+ modelFilterTypes = Repository.getSchema().getFilterTypes(),
66
70
  [isReady, setIsReady] = useState(false),
67
71
  [isFilterSelectorShown, setIsFilterSelectorShown] = useState(false),
68
72
  getFormattedFilter = (filter) => {
69
73
  let formatted = null;
70
74
  if (_.isString(filter)) {
71
- const field = filter;
75
+ const
76
+ field = filter,
77
+ propertyDef = Repository.getSchema().getPropertyDefinition(field);
78
+
79
+ let title, type;
80
+ if (propertyDef) {
81
+ title = propertyDef.title;
82
+ type = propertyDef.filterType;
83
+ } else {
84
+ if (!modelFilterTypes[field]) {
85
+ throw Error('not a propertyDef, and not an ancillaryFilter!');
86
+ }
87
+ const ancillaryFilter = modelFilterTypes[field];
88
+ title = ancillaryFilter.title;
89
+ type = FILTER_TYPE_ANCILLARY;
90
+ }
72
91
  formatted = {
73
92
  field,
74
- title: modelTitles[field],
75
- type: modelFilterTypes[field],
93
+ title,
94
+ type,
76
95
  value: null, // value starts as null
77
96
  };
78
97
  } else if (_.isPlainObject(filter)) {
@@ -221,12 +240,23 @@ export default function withFilters(WrappedComponent) {
221
240
  _.each(filters, (filter, ix) => {
222
241
  let Element,
223
242
  elementProps = {};
224
- const {
243
+ let {
225
244
  field,
226
245
  type: filterType,
227
- } = filter;
246
+ title,
247
+ } = filter,
248
+
249
+ propertyDef = Repository.getSchema().getPropertyDefinition(field);
250
+
251
+ if (!title) {
252
+ title = propertyDef?.title;
253
+ }
228
254
 
229
255
  if (_.isString(filterType)) {
256
+ if (filterType === FILTER_TYPE_ANCILLARY) {
257
+ title = modelFilterTypes[field].title;
258
+ filterType = Inflector.camelize(Inflector.pluralize(field)) + 'Combo'; // Convert field to PluralCamelCombo
259
+ }
230
260
  Element = getComponentFromType(filterType);
231
261
  if (filterType === 'Input') {
232
262
  elementProps.autoSubmit = true;
@@ -247,7 +277,7 @@ export default function withFilters(WrappedComponent) {
247
277
  elementProps.minWidth = 100;
248
278
  }
249
279
 
250
- const tooltip = filter.tooltip || filter.title || modelTitles[filter.field];
280
+ const tooltip = filter.tooltip || title;
251
281
  let filterElement = <Element
252
282
  key={'filter-' + field}
253
283
  tooltip={tooltip}
@@ -259,7 +289,7 @@ export default function withFilters(WrappedComponent) {
259
289
  />;
260
290
  if (showLabels && field !== 'q') {
261
291
  filterElement = <Row key={'label-' + ix} alignItems="center">
262
- <Text ml={2} mr={1} fontSize={UiGlobals.styles.FILTER_LABEL_FONTSIZE}>{modelTitles[field]}</Text>
292
+ <Text ml={2} mr={1} fontSize={UiGlobals.styles.FILTER_LABEL_FONTSIZE}>{title}</Text>
263
293
  {filterElement}
264
294
  </Row>;
265
295
  }
@@ -387,13 +417,23 @@ export default function withFilters(WrappedComponent) {
387
417
 
388
418
  _.each(modalSlots, (field, ix) => {
389
419
 
390
- // Create the data for the combobox.
420
+ // Create the data for the combobox. (i.e. List all the possible filters for this slot)
391
421
  const data = [];
392
422
  _.each(modelFilterTypes, (filterType, filterField) => {
393
423
  if (inArray(filterField, usedFields) && field !== filterField) { // Show all filters not yet applied, but include the current filter
394
424
  return; // skip, since it's already been used
395
425
  }
396
- data.push([ filterField, modelTitles[filterField] ]);
426
+
427
+ // Is it an ancillary filter?
428
+ const isAncillary = _.isPlainObject(filterType) && filterType.isAncillary;
429
+ if (isAncillary) {
430
+ data.push([ filterField, filterType.title ]);
431
+ return;
432
+ }
433
+
434
+ // basic property filter
435
+ const propertyDef = Repository.getSchema().getPropertyDefinition(filterField);
436
+ data.push([ filterField, propertyDef.title ]);
397
437
  });
398
438
 
399
439
  const
@@ -447,6 +487,7 @@ export default function withFilters(WrappedComponent) {
447
487
  <FormPanel
448
488
  title="Filter Selector"
449
489
  instructions="Please select which fields to filter by."
490
+ editorType={EDITOR_TYPE__PLAIN}
450
491
  flex={1}
451
492
  startingValues={formStartingValues}
452
493
  items={[
@@ -0,0 +1 @@
1
+ export const FILTER_TYPE_ANCILLARY = 'FILTER_TYPE_ANCILLARY';
@@ -38,7 +38,6 @@ const defaults = {
38
38
  FORM_LABEL_FONTSIZE: DEFAULT_FONTSIZE,
39
39
  FORM_NUMBER_FONTSIZE: DEFAULT_FONTSIZE,
40
40
  FORM_TEXT_FONTSIZE: DEFAULT_FONTSIZE,
41
- FORM_TEXT_BG: WHITE,
42
41
  FORM_TEXTAREA_BG: WHITE,
43
42
  FORM_TEXTAREA_FONTSIZE: DEFAULT_FONTSIZE,
44
43
  FORM_TEXTAREA_HEIGHT: 130,