@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 +2 -1
- package/src/Components/Form/Field/Combo/Combo.js +68 -48
- package/src/Components/Form/Field/Text.js +2 -2
- package/src/Components/Form/Form.js +18 -10
- package/src/Components/Hoc/withFilters.js +55 -14
- package/src/Constants/Filters.js +1 -0
- package/src/Constants/Styles.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onehat/ui",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
282
|
+
if (!_.isEmpty(value)) {
|
|
283
|
+
value += '%';
|
|
284
|
+
}
|
|
278
285
|
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
|
@@ -199,7 +199,8 @@ function Form(props) {
|
|
|
199
199
|
} = fieldState;
|
|
200
200
|
let editorProps = {};
|
|
201
201
|
if (!editor) {
|
|
202
|
-
|
|
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
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
} =
|
|
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 &&
|
|
291
|
-
label =
|
|
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
|
-
|
|
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
|
|
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
|
|
75
|
-
type
|
|
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
|
-
|
|
243
|
+
let {
|
|
225
244
|
field,
|
|
226
245
|
type: filterType,
|
|
227
|
-
|
|
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 ||
|
|
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}>{
|
|
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
|
-
|
|
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';
|
package/src/Constants/Styles.js
CHANGED
|
@@ -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,
|