@onehat/ui 0.3.57 → 0.3.58

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.57",
3
+ "version": "0.3.58",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -12,9 +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';
15
16
  import withComponent from '../../../Hoc/withComponent.js';
16
17
  import withData from '../../../Hoc/withData.js';
17
- import withSelection from '../../../Hoc/withSelection.js';
18
18
  import withValue from '../../../Hoc/withValue.js';
19
19
  import emptyFn from '../../../../Functions/emptyFn.js';
20
20
  import { Grid, WindowedGridEditor } from '../../../Grid/Grid.js';
@@ -22,15 +22,10 @@ import IconButton from '../../../Buttons/IconButton.js';
22
22
  import CaretDown from '../../../Icons/CaretDown.js';
23
23
  import _ from 'lodash';
24
24
 
25
- // Combo requires the use of HOC withSelection() whenever it's used.
26
- // The default export is *with* the HOC. A separate *raw* component is
27
- // exported which can be combined with many HOCs for various functionality.
28
-
29
25
  export function ComboComponent(props) {
30
26
  const {
31
27
  additionalButtons,
32
28
  autoFocus = false,
33
- forceSelection = true,
34
29
  tooltipRef = null,
35
30
  tooltip = null,
36
31
  menuMinWidth = 150,
@@ -39,10 +34,14 @@ export function ComboComponent(props) {
39
34
  _input = {},
40
35
  isEditor = false,
41
36
  isDisabled = false,
37
+ tooltipPlacement = 'bottom',
38
+ onRowPress,
42
39
 
43
- // withValue
44
- value,
45
- setValue,
40
+ // withComponent
41
+ self,
42
+
43
+ // withAlert
44
+ confirm,
46
45
 
47
46
  // withData
48
47
  Repository,
@@ -50,32 +49,28 @@ export function ComboComponent(props) {
50
49
  idIx,
51
50
  displayIx,
52
51
 
53
- // withSelection
54
- disableWithSelection,
55
- selection,
56
- setSelection,
57
- selectionMode,
58
- selectNext,
59
- selectPrev,
60
- getDisplayValuesFromSelection,
61
-
62
- tooltipPlacement = 'bottom',
52
+ // withValue
53
+ value,
54
+ setValue,
63
55
  } = props,
64
56
  styles = UiGlobals.styles,
65
57
  inputRef = useRef(),
66
58
  triggerRef = useRef(),
67
59
  menuRef = useRef(),
68
- isManuallyEnteringText = useRef(false),
60
+ displayValueRef = useRef(null),
69
61
  savedSearch = useRef(null),
70
62
  typingTimeout = useRef(),
71
63
  [isMenuShown, setIsMenuShown] = useState(false),
72
64
  [isRendered, setIsRendered] = useState(false),
73
- [textValue, setTextValue] = useState(''),
65
+ [isReady, setIsReady] = useState(false),
66
+ [isSearchMode, setIsSearchMode] = useState(false),
67
+ [gridSelection, setGridSelection] = useState(null),
68
+ [textInputValue, setTextInputValue] = useState(''),
69
+ [newEntityDisplayValue, setNewEntityDisplayValue] = useState(null),
74
70
  [width, setWidth] = useState(0),
75
- [height, setHeight] = useState(null),
76
71
  [top, setTop] = useState(0),
77
72
  [left, setLeft] = useState(0),
78
- showMenu = () => {
73
+ showMenu = async () => {
79
74
  if (isMenuShown) {
80
75
  return;
81
76
  }
@@ -107,7 +102,7 @@ export function ComboComponent(props) {
107
102
  }
108
103
  }
109
104
  if (Repository && !Repository.isLoaded) {
110
- Repository.reload();
105
+ await Repository.load();
111
106
  }
112
107
  setIsMenuShown(true);
113
108
  },
@@ -117,11 +112,8 @@ export function ComboComponent(props) {
117
112
  }
118
113
  setIsMenuShown(false);
119
114
  },
120
- getIsManuallyEnteringText = () => {
121
- return isManuallyEnteringText.current;
122
- },
123
- setIsManuallyEnteringText = (bool) => {
124
- isManuallyEnteringText.current = bool;
115
+ toggleMenu = () => {
116
+ setIsMenuShown(!isMenuShown);
125
117
  },
126
118
  getSavedSearch = () => {
127
119
  return savedSearch.current;
@@ -129,8 +121,8 @@ export function ComboComponent(props) {
129
121
  setSavedSearch = (val) => {
130
122
  savedSearch.current = val;
131
123
  },
132
- toggleMenu = () => {
133
- setIsMenuShown(!isMenuShown);
124
+ resetInputTextValue = () => {
125
+ setTextInputValue(getDisplayValue());
134
126
  },
135
127
  onInputKeyPress = (e, inputValue) => {
136
128
  if (disableDirectEntry) {
@@ -138,6 +130,8 @@ export function ComboComponent(props) {
138
130
  }
139
131
  switch(e.key) {
140
132
  case 'Escape':
133
+ setIsSearchMode(false);
134
+ resetInputTextValue();
141
135
  hideMenu();
142
136
  break;
143
137
  case 'Enter':
@@ -145,24 +139,42 @@ export function ComboComponent(props) {
145
139
  if (_.isEmpty(inputValue) && !_.isNull(value)) {
146
140
  // User pressed Enter on an empty text field, but value is set to something
147
141
  // This means the user cleared the input and pressed enter, meaning he wants to clear the value
148
-
149
- // clear the value
150
142
  setValue(null);
151
- if (isMenuShown) {
152
- hideMenu();
153
- }
154
- } else {
155
- toggleMenu();
143
+ hideMenu();
144
+ return;
145
+ }
146
+
147
+ if (_.isEmpty(gridSelection)) {
148
+ confirm('You have nothing selected in the dropdown menu. Clear value?', doIt, true);
149
+ return;
150
+ }
151
+
152
+ doIt();
153
+
154
+ function doIt() {
155
+ setValue(gridSelection?.id);
156
+ hideMenu();
156
157
  }
157
158
  break;
158
- case 'ArrowDown':
159
- e.preventDefault();
160
- selectNext();
161
- break;
162
- case 'ArrowUp':
163
- e.preventDefault();
164
- selectPrev();
165
- break;
159
+ // case 'ArrowDown':
160
+ // e.preventDefault();
161
+ // showMenu();
162
+ // selectNext();
163
+ // setTimeout(() => {
164
+ // if (!self.children?.dropdownGrid?.selectPrev) {
165
+ // debugger;
166
+ // }
167
+ // self.children.dropdownGrid.selectNext();
168
+ // }, 10);
169
+ // break;
170
+ // case 'ArrowUp':
171
+ // e.preventDefault();
172
+ // showMenu();
173
+ // selectPrev();
174
+ // setTimeout(() => {
175
+ // self.children.dropdownGrid.selectPrev();
176
+ // }, 10);
177
+ // break;
166
178
  default:
167
179
  }
168
180
  },
@@ -170,140 +182,138 @@ export function ComboComponent(props) {
170
182
  if (disableDirectEntry) {
171
183
  return;
172
184
  }
173
- setTextValue(value);
174
185
 
175
- setIsManuallyEnteringText(true);
186
+ if (_.isEmpty(value)) {
187
+ // text input is cleared
188
+ hideMenu();
189
+ return;
190
+ }
191
+
192
+ setTextInputValue(value);
193
+ showMenu();
194
+
176
195
  clearTimeout(typingTimeout.current);
177
196
  typingTimeout.current = setTimeout(() => {
178
197
  searchForMatches(value);
179
198
  }, 300);
180
199
  },
200
+ onInputFocus = (e) => {
201
+ inputRef.current.select();
202
+ },
181
203
  onInputBlur = (e) => {
182
- const {
183
- relatedTarget
184
- } = e;
185
-
186
- setIsManuallyEnteringText(false);
187
-
188
- // If user focused on the trigger and text is blank, clear the selection and close the menu
189
- if ((triggerRef.current === relatedTarget || triggerRef.current.contains(relatedTarget)) && (_.isEmpty(textValue) || _.isNil(textValue))) {
190
- if (!disableWithSelection) {
191
- setSelection([]); // delete current selection
192
- }
193
- hideMenu();
194
- return;
195
- }
196
-
197
- // If user focused on the menu or trigger, ignore this blur
198
- if (triggerRef.current === relatedTarget ||
199
- triggerRef.current.contains(relatedTarget) ||
200
- menuRef.current=== relatedTarget ||
201
- menuRef.current?.contains(relatedTarget)) {
204
+ if (isEventStillInComponent(e)) {
205
+ // ignore the blur
202
206
  return;
203
207
  }
204
208
 
205
- if (!relatedTarget ||
206
- (
207
- !inputRef.current.contains(relatedTarget) &&
208
- triggerRef.current !== relatedTarget &&
209
- (!menuRef.current || !menuRef.current.contains(relatedTarget))
210
- )
211
- ) {
212
- hideMenu();
213
- }
214
- if (_.isEmpty(textValue) || _.isNil(textValue)) {
215
-
216
- if (!disableWithSelection) {
217
- setSelection([]); // delete current selection
218
- }
219
-
220
- } else if (getIsManuallyEnteringText()) {
221
- if (forceSelection) {
222
- if (!disableWithSelection) {
223
- setSelection([]); // delete current selection
224
- } else {
225
- setValue(textValue);
226
- }
227
- hideMenu();
228
- } else {
229
- setValue(textValue);
230
- }
231
- }
232
-
233
- if (!disableWithSelection) {
234
- if (_.isEmpty(selection)) {
235
- setTextValue('');
236
- }
237
- }
209
+ setIsSearchMode(false);
210
+ resetInputTextValue();
211
+ hideMenu();
238
212
  },
239
- onInputClick = (e) => {
213
+ onTriggerPress = (e) => {
240
214
  if (!isRendered) {
241
215
  return;
242
216
  }
217
+ clearGridFilters();
243
218
  showMenu();
244
219
  },
245
- onTriggerPress = (e) => {
246
- if (!isRendered) {
220
+ onTriggerBlur = (e) => {
221
+ if (!isMenuShown) {
247
222
  return;
248
223
  }
249
- if (isMenuShown) {
250
- hideMenu();
251
- } else {
252
- showMenu();
224
+
225
+ if (isEventStillInComponent(e)) {
226
+ // ignore the blur
227
+ return;
253
228
  }
254
- inputRef.current.focus();
229
+
230
+ setIsSearchMode(false);
231
+ resetInputTextValue();
232
+ hideMenu();
255
233
  },
256
- onTriggerBlur = (e) => {
234
+ isEventStillInComponent = (e) => {
257
235
  const {
258
236
  relatedTarget
259
237
  } = e;
260
-
261
- if (!disableWithSelection) {
262
- if (_.isEmpty(textValue) || _.isNil(textValue)) {
263
- setSelection([]); // delete current selection
238
+ return !relatedTarget ||
239
+ !menuRef.current ||
240
+ !triggerRef.current ||
241
+ triggerRef.current === relatedTarget ||
242
+ triggerRef.current.contains(relatedTarget) ||
243
+ menuRef.current === relatedTarget ||
244
+ menuRef.current?.contains(relatedTarget);
245
+ },
246
+ clearGridFilters = async () => {
247
+ if (Repository) {
248
+ if (Repository.isLoading) {
249
+ await Repository.waitUntilDoneLoading();
264
250
  }
265
- }
251
+
252
+ // clear filter
253
+ if (Repository.isRemote) {
254
+ let searchField = 'q';
255
+ const searchValue = null;
266
256
 
267
- if (!isMenuShown) {
268
- return;
269
- }
270
- if (!relatedTarget ||
271
- (!inputRef.current.contains(relatedTarget) && triggerRef.current !== relatedTarget && !menuRef.current.contains(relatedTarget))) {
272
- hideMenu();
257
+ // Check to see if displayField is a real field
258
+ const
259
+ schema = Repository.getSchema(),
260
+ displayFieldName = schema.model.displayProperty,
261
+ displayFieldDef = schema.getPropertyDefinition(displayFieldName);
262
+ if (!displayFieldDef.isVirtual) {
263
+ searchField = displayFieldName + ' LIKE';
264
+ }
265
+
266
+ Repository.clear();
267
+ await Repository.filter(searchField, searchValue);
268
+ if (!this.isAutoLoad) {
269
+ await Repository.reload();
270
+ }
271
+
272
+ } else {
273
+ throw Error('Not yet implemented');
274
+ }
275
+
276
+ setSavedSearch(null);
277
+
278
+ } else {
279
+ throw Error('Not yet implemented');
273
280
  }
274
281
  },
275
282
  searchForMatches = async (value) => {
276
-
277
- if (_.isEmpty(value)) {
278
- return;
283
+ if (!isMenuShown) {
284
+ showMenu();
279
285
  }
280
286
 
287
+ setIsSearchMode(true);
288
+
281
289
  let found;
282
290
  if (Repository) {
291
+ if (Repository.isLoading) {
292
+ await Repository.waitUntilDoneLoading();
293
+ }
283
294
 
284
295
  // Set filter
285
296
  let filter = {};
286
297
  if (Repository.isRemote) {
287
298
  let searchField = 'q';
299
+ const searchValue = _.isEmpty(value) ? null : value + '%';
288
300
 
289
301
  // Check to see if displayField is a real field
290
302
  const
291
303
  schema = Repository.getSchema(),
292
- displayFieldName = schema.model.displayProperty;
304
+ displayFieldName = schema.model.displayProperty,
293
305
  displayFieldDef = schema.getPropertyDefinition(displayFieldName);
294
306
  if (!displayFieldDef.isVirtual) {
295
307
  searchField = displayFieldName + ' LIKE';
296
308
  }
297
309
 
298
- value += '%';
299
-
300
- await Repository.filter(searchField, value);
310
+ await Repository.filter(searchField, searchValue);
301
311
  if (!this.isAutoLoad) {
302
312
  await Repository.reload();
303
313
  }
304
314
 
305
315
  } else {
306
- throw Error('Not sure if this works yet!');
316
+ throw Error('Not yet implemented');
307
317
 
308
318
  // Fuzzy search with getBy filter function
309
319
  filter = (entity) => {
@@ -316,18 +326,11 @@ export function ComboComponent(props) {
316
326
  }
317
327
 
318
328
  setSavedSearch(value);
319
- if (!disableWithSelection) {
320
- const numResults = Repository.entities.length;
321
- if (!numResults) {
322
- setSelection([]);
323
- } else if (numResults === 1) {
324
- const selection = Repository.entities[0];
325
- setSelection([selection]);
326
- setSavedSearch(null);
327
- }
328
- }
329
+ setNewEntityDisplayValue(value); // capture the search query so we can tell Grid what to use for a new entity's displayValue
329
330
 
330
331
  } else {
332
+ throw Error('Not yet implemented');
333
+
331
334
  // Search through data
332
335
  found = _.find(data, (item) => {
333
336
  if (_.isString(item[displayIx]) && _.isString(value)) {
@@ -335,23 +338,64 @@ export function ComboComponent(props) {
335
338
  }
336
339
  return item[displayIx] === value;
337
340
  });
338
- if (found) {
339
- const
340
- newSelection = [found],
341
- newTextValue = getDisplayValuesFromSelection(newSelection);
341
+ // if (found) {
342
+ // const
343
+ // newSelection = [found];
342
344
 
343
- setTextValue(newTextValue);
344
- if (!disableWithSelection) {
345
- setSelection(newSelection);
345
+ // setTextInputValue(newTextValue);
346
+ // }
347
+ }
348
+ },
349
+ getDisplayValue = () => {
350
+ return displayValueRef.current;
351
+ },
352
+ setDisplayValue = async (value) => {
353
+ let displayValue = '';
354
+ if (_.isNil(value)) {
355
+ // do nothing
356
+ } else if (_.isArray(value)) {
357
+ displayValue = [];
358
+ if (Repository) {
359
+ if (!Repository.isLoaded) {
360
+ throw Error('Not yet implemented'); // Would a Combo ever have multiple remote selections? Shouldn't that be a Tag field??
361
+ }
362
+ if (Repository.isLoading) {
363
+ await Repository.waitUntilDoneLoading();
346
364
  }
365
+ displayValue = _.each(value, (id) => {
366
+ const entity = Repository.getById(id);
367
+ if (entity) {
368
+ displayValue.push(entity.displayValue)
369
+ }
370
+ });
347
371
  } else {
348
- if (value === '') { // Text field was cleared, so clear selection
349
- if (!disableWithSelection) {
350
- setSelection([]);
372
+ displayValue = _.each(value, (id) => {
373
+ const item = _.find(data, (datum) => datum[idIx] === id);
374
+ if (item) {
375
+ displayValue.push(item[displayIx]);
376
+ }
377
+ });
378
+ }
379
+ displayValue = displayValue.join(', ');
380
+ } else {
381
+ if (Repository) {
382
+ let entity;
383
+ if (!Repository.isLoaded) {
384
+ entity = await Repository.getSingleEntityFromServer(value);
385
+ } else {
386
+ if (Repository.isLoading) {
387
+ await Repository.waitUntilDoneLoading();
351
388
  }
389
+ entity = Repository.getById(value);
352
390
  }
391
+ displayValue = entity?.displayValue || '';
392
+ } else {
393
+ const item = _.find(data, (datum) => datum[idIx] === id);
394
+ displayValue = (item && item[displayIx]) || '';
353
395
  }
354
396
  }
397
+
398
+ displayValueRef.current = displayValue;
355
399
  };
356
400
 
357
401
  useEffect(() => {
@@ -365,27 +409,40 @@ export function ComboComponent(props) {
365
409
 
366
410
  }, [isRendered]);
367
411
 
368
- if (!disableWithSelection) {
369
- useEffect(() => {
370
- if (getIsManuallyEnteringText() && getSavedSearch()) {
371
- return
412
+ useEffect(() => {
413
+ (async () => {
414
+ setIsSearchMode(false);
415
+ await setDisplayValue(value);
416
+ resetInputTextValue();
417
+ if (!isReady) {
418
+ setIsReady(true);
372
419
  }
420
+ })();
421
+ }, [value]);
373
422
 
374
- // Adjust text input to match selection
375
- let localTextValue = getDisplayValuesFromSelection(selection);
376
- if (!_.isEqual(localTextValue, textValue)) {
377
- setTextValue(localTextValue);
378
- }
379
- setIsManuallyEnteringText(false);
380
- }, [selection]);
423
+ if (!isReady) {
424
+ return null;
381
425
  }
382
426
 
383
-
384
427
  const refProps = {};
385
428
  if (tooltipRef) {
386
429
  refProps.ref = tooltipRef;
387
430
  }
388
431
 
432
+ const gridProps = _.pick(props, [
433
+ 'Editor',
434
+ 'model',
435
+ 'Repository',
436
+ 'data',
437
+ 'idIx',
438
+ 'displayIx',
439
+ 'value',
440
+ 'disableView',
441
+ 'disableCopy',
442
+ 'disableDuplicate',
443
+ 'disablePrint',
444
+ ]);
445
+
389
446
  const WhichGrid = isEditor ? WindowedGridEditor : Grid;
390
447
 
391
448
  let comboComponent = <Row {...refProps} justifyContent="center" alignItems="center" h={styles.FORM_COMBO_HEIGHT} flex={1} onLayout={() => setIsRendered(true)}>
@@ -424,17 +481,17 @@ export function ComboComponent(props) {
424
481
  _focus={{
425
482
  bg: styles.FORM_COMBO_INPUT_FOCUS_BG,
426
483
  }}
427
- >{textValue}</Text>
484
+ >{textInputValue}</Text>
428
485
  </Pressable> :
429
486
  <Input
430
487
  ref={inputRef}
431
- value={textValue}
488
+ value={textInputValue}
432
489
  autoSubmit={true}
433
490
  isDisabled={isDisabled}
434
491
  onChangeValue={onInputChangeText}
435
492
  onKeyPress={onInputKeyPress}
493
+ onFocus={onInputFocus}
436
494
  onBlur={onInputBlur}
437
- onClick={onInputClick}
438
495
  onLayout={(e) => {
439
496
  // On web, this is not needed, but on RN it might be, so leave it in for now
440
497
  const {
@@ -447,15 +504,6 @@ export function ComboComponent(props) {
447
504
  setTop(top + height);
448
505
  setLeft(left);
449
506
  }}
450
- // onFocus={(e) => {
451
- // if (isBlocked.current) {
452
- // return;
453
- // }
454
- // if (!isRendered) {
455
- // return;
456
- // }
457
- // showMenu();
458
- // }}
459
507
  flex={1}
460
508
  h="100%"
461
509
  m={0}
@@ -535,20 +583,66 @@ export function ComboComponent(props) {
535
583
  };
536
584
  }}
537
585
  autoAdjustPageSizeToHeight={false}
538
- {...props}
586
+ {...gridProps}
587
+ reference="dropdownGrid"
588
+ parent={self}
539
589
  h={styles.FORM_COMBO_MENU_HEIGHT + 'px'}
590
+ newEntityDisplayValue={newEntityDisplayValue}
540
591
  disablePresetButtons={!isEditor}
541
- setSelection={(selection) => {
542
- // Decorator fn to add local functionality
543
- // Close the menu when row is selected on grid
544
- setSelection(selection);
545
- if (hideMenuOnSelection) {
592
+ onChangeSelection={(selection) => {
593
+ if (selection[0]?.isPhantom) {
594
+ // do nothing
595
+ return;
596
+ }
597
+
598
+ setGridSelection(selection);
599
+
600
+ // When we first open the menu, we try to match the selection to the value, ignore this
601
+ if (selection[0]?.displayValue === getDisplayValue()) {
602
+ return;
603
+ }
604
+
605
+ // when user selected the record matching the current value, kill search mode
606
+ if (selection[0]?.id === value) {
607
+ setIsSearchMode(false);
608
+ resetInputTextValue();
609
+ if (hideMenuOnSelection) {
610
+ hideMenu();
611
+ }
612
+ return;
613
+ }
614
+
615
+ setValue(selection[0]?.id);
616
+
617
+ if (_.isEmpty(selection)) {
618
+ return;
619
+ }
620
+
621
+ if (hideMenuOnSelection && !isEditor) {
546
622
  hideMenu();
547
623
  }
624
+
548
625
  }}
549
- selectionMode={selectionMode}
550
- setValue={(value) => {
551
- setValue(value);
626
+ onSave={(selection) => {
627
+ const entity = selection[0];
628
+ if (entity?.id !== value) {
629
+ // Either a phantom record was just solidified into a real record, or a new (non-phantom) record was added.
630
+ // Select it and set the value of the combo.
631
+ setGridSelection([entity]);
632
+ const id = entity.id;
633
+ setValue(id);
634
+ }
635
+ }}
636
+ onRowPress={(item, e) => {
637
+ if (onRowPress) {
638
+ onRowPress(item, e);
639
+ return;
640
+ }
641
+ const id = Repository ? item.id : item[idIx];
642
+ if (id === value) {
643
+ hideMenu();
644
+ onInputFocus();
645
+ }
552
646
  }}
553
647
  />
554
648
  </Popover.Body>
@@ -565,9 +659,9 @@ export function ComboComponent(props) {
565
659
  }
566
660
 
567
661
  export const Combo = withComponent(
568
- withData(
569
- withValue(
570
- withSelection(
662
+ withAlert(
663
+ withData(
664
+ withValue(
571
665
  ComboComponent
572
666
  )
573
667
  )
@@ -25,7 +25,6 @@ function ValueBox(props) {
25
25
  onView,
26
26
  onDelete,
27
27
  } = props;
28
-
29
28
  return <Row
30
29
  borderWidth={1}
31
30
  borderColor="trueGray.400"
@@ -43,7 +42,7 @@ function ValueBox(props) {
43
42
  onPress={onView}
44
43
  h="100%"
45
44
  />
46
- <Text color="trueGray.600">{text}</Text>
45
+ <Text color="trueGray.600" mr={onDelete ? 0 : 2}>{text}</Text>
47
46
  {onDelete &&
48
47
  <IconButton
49
48
  _icon={{
@@ -83,9 +82,15 @@ function TagComponent(props) {
83
82
  const
84
83
  id = item.id,
85
84
  repository = propsToPass.Repository;
85
+ if (!repository.isLoaded) {
86
+ await repository.load();
87
+ }
88
+ if (repository.isLoading) {
89
+ await repository.waitUntilDoneLoading();
90
+ }
86
91
  let record = repository.getById(id); // first try to get from entities in memory
87
92
  if (!record && repository.getSingleEntityFromServer) {
88
- record = await repository.getSingleEntityFromServer(id); // TODO: Build this
93
+ record = await repository.getSingleEntityFromServer(id);
89
94
  }
90
95
 
91
96
  if (!record) {
@@ -135,24 +140,42 @@ function TagComponent(props) {
135
140
  }),
136
141
  WhichCombo = isEditor ? ComboEditor : Combo;
137
142
 
143
+ const sizeProps = {};
144
+ if (!props.flex && !props.w) {
145
+ sizeProps.flex = 1;
146
+ } else {
147
+ if (props.w) {
148
+ sizeProps.w = props.w;
149
+ }
150
+ if (props.flex) {
151
+ sizeProps.flex = props.flex;
152
+ }
153
+ }
154
+
138
155
  return <>
139
- <Column w="100%" flex={1}>
140
- {!_.isEmpty(valueBoxes) &&
141
- <Row
142
- w="100%"
143
- borderWidth={1}
144
- borderColor="trueGray.300"
145
- borderRadius="md"
146
- bg="trueGray.100"
147
- p={1}
148
- mb={1}
149
- flexWrap="wrap"
150
- >{valueBoxes}</Row>}
151
- <WhichCombo
152
- Repository={props.Repository}
153
- Editor={props.Editor}
154
- onRowPress={onAdd}
155
- />
156
+ <Column
157
+ {...props}
158
+ {...sizeProps}
159
+ px={0}
160
+ py={0}
161
+ >
162
+ <Row
163
+ w="100%"
164
+ borderWidth={1}
165
+ borderColor="trueGray.300"
166
+ borderRadius="md"
167
+ bg="trueGray.100"
168
+ p={1}
169
+ mb={1}
170
+ minHeight={10}
171
+ flexWrap="wrap"
172
+ >{valueBoxes}</Row>
173
+ {isEditor &&
174
+ <WhichCombo
175
+ Repository={props.Repository}
176
+ Editor={props.Editor}
177
+ onRowPress={onAdd}
178
+ />}
156
179
  </Column>
157
180
  {isViewerShown &&
158
181
  <Modal
@@ -162,6 +185,9 @@ function TagComponent(props) {
162
185
  <Editor
163
186
  editorType={EDITOR_TYPE__WINDOWED}
164
187
  {...propsToPass}
188
+ px={0}
189
+ py={0}
190
+ w="100%"
165
191
  parent={self}
166
192
  reference="viewer"
167
193
 
@@ -29,7 +29,7 @@ export default function FieldSet(props) {
29
29
  forceUpdate = useForceUpdate(),
30
30
  childRefs = useRef([]),
31
31
  isAllCheckedRef = useRef(false),
32
- [localIsCollapsed, setLocalIsCollapsed] = useState(isCollapsed),
32
+ [isLocalCollapsed, setIsLocalCollapsed] = useState(isCollapsed),
33
33
  getIsAllChecked = () => {
34
34
  return isAllCheckedRef.current;
35
35
  },
@@ -38,7 +38,7 @@ export default function FieldSet(props) {
38
38
  forceUpdate();
39
39
  },
40
40
  onToggleCollapse = () => {
41
- setLocalIsCollapsed(!localIsCollapsed);
41
+ setIsLocalCollapsed(!isLocalCollapsed);
42
42
  },
43
43
  onToggleAllChecked = () => {
44
44
  const bool = !getIsAllChecked();
@@ -82,6 +82,7 @@ export default function FieldSet(props) {
82
82
  bg={styles.FORM_FIELDSET_BG}
83
83
  mb={4}
84
84
  pb={1}
85
+ pr={4}
85
86
  {...propsToPass}
86
87
  >
87
88
  {title &&
@@ -119,7 +120,7 @@ export default function FieldSet(props) {
119
120
  </Row>}
120
121
  {isCollapsible && <IconButton
121
122
  _icon={{
122
- as: localIsCollapsed ? <CaretDown /> : <CaretUp />,
123
+ as: isLocalCollapsed ? <CaretDown /> : <CaretUp />,
123
124
  size: 'sm',
124
125
  color: 'trueGray.300',
125
126
  }}
@@ -127,7 +128,7 @@ export default function FieldSet(props) {
127
128
  />}
128
129
  </Row>}
129
130
  {helpText && <Text>{helpText}</Text>}
130
- {!localIsCollapsed && <FieldSetContext.Provider value={{ registerChild, onChangeValue, }}>
131
+ {!isLocalCollapsed && <FieldSetContext.Provider value={{ registerChild, onChangeValue, }}>
131
132
  {children}
132
133
  </FieldSetContext.Provider>}
133
134
  </Box>;
@@ -439,6 +439,9 @@ function Form(props) {
439
439
  name={name}
440
440
  value={value}
441
441
  onChangeValue={(newValue) => {
442
+ if (newValue === undefined) {
443
+ newValue = null; // React Hook Form doesn't respond well when setting value to undefined
444
+ }
442
445
  onChange(newValue);
443
446
  if (onEditorChange) {
444
447
  onEditorChange(newValue, formSetValue, formGetValues, formState);
@@ -452,21 +455,21 @@ function Form(props) {
452
455
  {...propsToPass}
453
456
  {...editorTypeProps}
454
457
  />;
455
- if (error) {
456
- if (editorType !== EDITOR_TYPE__INLINE) {
457
- let message = error.message;
458
+ if (editorType !== EDITOR_TYPE__INLINE) {
459
+ let message = null;
460
+ if (error) {
461
+ message = error.message;
458
462
  if (label) {
459
463
  message = message.replace(error.ref.name, label);
460
464
  }
461
- element = <Column pt={1} flex={1}>
462
- {element}
463
- <Text color="#f00">{message}</Text>
464
- </Column>;
465
- } else {
466
- debugger;
467
-
468
-
469
465
  }
466
+ if (message) {
467
+ message = <Text color="#f00">{message}</Text>;
468
+ }
469
+ element = <Column pt={1} flex={1}>
470
+ {element}
471
+ {message}
472
+ </Column>;
470
473
  }
471
474
 
472
475
  if (item.additionalEditButtons) {
@@ -573,17 +576,24 @@ function Form(props) {
573
576
 
574
577
  useEffect(() => {
575
578
  if (!Repository) {
576
- return () => {};
579
+ return () => {
580
+ if (!_.isNil(editorStateRef)) {
581
+ editorStateRef.current = null; // clean up the editorStateRef on unmount
582
+ }
583
+ };
577
584
  }
578
585
 
579
586
  Repository.ons(['changeData', 'change'], forceUpdate);
580
587
 
581
588
  return () => {
582
589
  Repository.offs(['changeData', 'change'], forceUpdate);
590
+ if (!_.isNil(editorStateRef)) {
591
+ editorStateRef.current = null; // clean up the editorStateRef on unmount
592
+ }
583
593
  };
584
594
  }, [Repository]);
585
595
 
586
- // if (Repository && (!record || _.isEmpty(record))) {
596
+ // if (Repository && (!record || _.isEmpty(record) || record.isDestroyed)) {
587
597
  // return null;
588
598
  // }
589
599
 
@@ -622,7 +632,14 @@ function Form(props) {
622
632
  additionalButtons,
623
633
  isSaveDisabled = false,
624
634
  isSubmitDisabled = false,
625
- savingProps = {};
635
+ savingProps = {},
636
+
637
+ showDeleteBtn = false,
638
+ showResetBtn = false,
639
+ showCloseBtn = false,
640
+ showCancelBtn = false,
641
+ showSaveBtn = false,
642
+ showSubmitBtn = false;
626
643
 
627
644
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
628
645
 
@@ -678,7 +695,7 @@ function Form(props) {
678
695
  isSaveDisabled = true;
679
696
  isSubmitDisabled = true;
680
697
  }
681
- if (_.isEmpty(formState.dirtyFields) && !record?.isRemotePhantom) {
698
+ if (_.isEmpty(formState.dirtyFields) && !record?.isPhantom) {
682
699
  isSaveDisabled = true;
683
700
  }
684
701
 
@@ -688,6 +705,34 @@ function Form(props) {
688
705
  footerProps.alignItems = 'flex-start';
689
706
  }
690
707
 
708
+ if (onDelete && editorMode === EDITOR_MODE__EDIT && isSingle) {
709
+ showDeleteBtn = true;
710
+ }
711
+ if (!isEditorViewOnly) {
712
+ showResetBtn = true;
713
+ }
714
+ if (editorType !== EDITOR_TYPE__SIDE) { // side editor won't show either close or cancel buttons!
715
+ // determine whether we should show the close or cancel button
716
+ if (isEditorViewOnly) {
717
+ showCloseBtn = true;
718
+ } else {
719
+ if (formState.isDirty || record?.isPhantom) {
720
+ if (isSingle && onCancel) {
721
+ showCancelBtn = true;
722
+ }
723
+ } else {
724
+ if (onClose) {
725
+ showCloseBtn = true;
726
+ }
727
+ }
728
+ }
729
+ }
730
+ if (!isEditorViewOnly && onSave) {
731
+ showSaveBtn = true;
732
+ }
733
+ if (!!onSubmit) {
734
+ showSubmitBtn = true;
735
+ }
691
736
  }
692
737
 
693
738
  return <Column {...sizeProps} onLayout={onLayoutDecorated} ref={formRef}>
@@ -712,6 +757,7 @@ function Form(props) {
712
757
 
713
758
  <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
714
759
  {onDelete && editorMode === EDITOR_MODE__EDIT && isSingle &&
760
+
715
761
  <Row flex={1} justifyContent="flex-start">
716
762
  <Button
717
763
  key="deleteBtn"
@@ -724,7 +770,7 @@ function Form(props) {
724
770
  >Delete</Button>
725
771
  </Row>}
726
772
 
727
- {!isEditorViewOnly &&
773
+ {showResetBtn &&
728
774
  <IconButton
729
775
  key="resetBtn"
730
776
  onPress={() => {
@@ -735,34 +781,38 @@ function Form(props) {
735
781
  }}
736
782
  icon={<Rotate color="#fff" />}
737
783
  />}
738
- {!isEditorViewOnly && isSingle && onCancel &&
784
+
785
+ {showCancelBtn &&
739
786
  <Button
740
787
  key="cancelBtn"
741
788
  variant="ghost"
742
789
  onPress={onCancel}
743
790
  color="#fff"
744
791
  >Cancel</Button>}
745
- {!isEditorViewOnly && onSave &&
792
+
793
+ {showCloseBtn &&
794
+ <Button
795
+ key="closeBtn"
796
+ variant="ghost"
797
+ onPress={onClose}
798
+ color="#fff"
799
+ >Close</Button>}
800
+
801
+ {showSaveBtn &&
746
802
  <Button
747
803
  key="saveBtn"
748
804
  onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
749
805
  isDisabled={isSaveDisabled}
750
806
  color="#fff"
751
807
  >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
752
- {onSubmit &&
808
+
809
+ {showSubmitBtn &&
753
810
  <Button
754
811
  key="submitBtn"
755
812
  onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
756
813
  isDisabled={isSubmitDisabled}
757
814
  color="#fff"
758
815
  >{submitBtnLabel || 'Submit'}</Button>}
759
-
760
- {isEditorViewOnly && onClose && editorType !== EDITOR_TYPE__SIDE &&
761
- <Button
762
- key="closeBtn"
763
- onPress={onClose}
764
- color="#fff"
765
- >Close</Button>}
766
816
 
767
817
  {additionalFooterButtons && _.map(additionalFooterButtons, (props) => {
768
818
  return <Button
@@ -35,6 +35,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
35
35
  },
36
36
  record,
37
37
  onChange,
38
+ onSave,
39
+ newEntityDisplayValue,
38
40
 
39
41
  // withComponent
40
42
  self,
@@ -57,6 +59,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
57
59
  } = props,
58
60
  listeners = useRef({}),
59
61
  editorStateRef = useRef(),
62
+ newEntityDisplayValueRef = useRef(),
60
63
  [currentRecord, setCurrentRecord] = useState(null),
61
64
  [isAdding, setIsAdding] = useState(false),
62
65
  [isSaving, setIsSaving] = useState(false),
@@ -81,6 +84,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
81
84
  listeners.current = obj;
82
85
  // forceUpdate(); // we don't want to get into an infinite loop of renders. Simply directly assign the listeners in every child render
83
86
  },
87
+ getNewEntityDisplayValue = () => {
88
+ return newEntityDisplayValueRef.current;
89
+ },
84
90
  onAdd = async () => {
85
91
  const defaultValues = Repository.getSchema().getDefaultValues();
86
92
  let addValues = _.clone(defaultValues);
@@ -89,6 +95,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
89
95
  addValues[selectorId] = selectorSelected.id;
90
96
  }
91
97
 
98
+ if (getNewEntityDisplayValue()) {
99
+ const displayPropertyName = Repository.getSchema().model.displayProperty;
100
+ addValues[displayPropertyName] = getNewEntityDisplayValue();
101
+ }
102
+
92
103
  if (getListeners().onBeforeAdd) {
93
104
  const listenerResult = await getListeners().onBeforeAdd();
94
105
  if (listenerResult === false) {
@@ -116,6 +127,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
116
127
  // Unmap the values, so we can input true originalData
117
128
  addValues = Repository.unmapData(addValues);
118
129
 
130
+
119
131
  setIsAdding(true);
120
132
  setIsSaving(true);
121
133
  const entity = await Repository.add(addValues, false, true);
@@ -260,6 +272,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
260
272
  entity = selection[0],
261
273
  id = entity.id;
262
274
  const result = await Repository._send('POST', Model + '/duplicate', { id });
275
+ if (!result) {
276
+ return;
277
+ }
263
278
  const {
264
279
  root,
265
280
  success,
@@ -273,15 +288,22 @@ export default function withEditor(WrappedComponent, isTree = false) {
273
288
 
274
289
  const duplicateId = root.id;
275
290
 
291
+ // TODO: I don't like this.
292
+ // Currently, we filter the repository by only the new Entity, then select the entity for editing.
293
+ // There is a 2-second delay between filtering and being able to select, and this is unacceptable.
294
+ // Why do we filter for just the new entity? Because it's not guaranteed to show up in the grid based on sorting.
295
+ // Can't we just manually add this record to the repository at the top and then edit it?
296
+
276
297
  // Filter the grid with only the duplicate's ID, and open it for editing.
277
298
  self.filterById(duplicateId, () => { // because of the way useFilters is made, we have to use a callback, not await a Promise.
278
299
 
279
300
  // Select the only node
280
301
  const duplicateEntity = Repository.getById(duplicateId);
281
- self.setSelection([duplicateEntity]);
302
+ setTimeout(() => {
303
+ setSelection([duplicateEntity]);
282
304
 
283
- // edit it
284
- onEdit();
305
+ onEdit();
306
+ }, 2000); // we need this delay!
285
307
 
286
308
  });
287
309
 
@@ -326,6 +348,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
326
348
  if (onChange) {
327
349
  onChange();
328
350
  }
351
+ if (onSave) {
352
+ onSave(what);
353
+ }
329
354
 
330
355
  return true;
331
356
  },
@@ -339,7 +364,6 @@ export default function withEditor(WrappedComponent, isTree = false) {
339
364
  }
340
365
 
341
366
  setIsAdding(false);
342
- setEditorMode(EDITOR_MODE__VIEW);
343
367
  setIsEditorShown(false);
344
368
  }
345
369
  const formState = editorStateRef.current;
@@ -350,6 +374,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
350
374
  }
351
375
  },
352
376
  onEditorClose = () => {
377
+ if (isAdding) {
378
+ onEditorCancel();
379
+ }
353
380
  setIsEditorShown(false);
354
381
  },
355
382
  onEditorDelete = async () => {
@@ -406,6 +433,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
406
433
  self.deleteChildren = onDeleteChildren;
407
434
  self.duplicate = onDuplicate;
408
435
  }
436
+ newEntityDisplayValueRef.current = newEntityDisplayValue;
409
437
 
410
438
  if (lastSelection !== selection) {
411
439
  // NOTE: If I don't calculate this on the fly for selection changes,
@@ -228,6 +228,9 @@ export default function withSelection(WrappedComponent) {
228
228
  conformSelectionToValue = async () => {
229
229
  let newSelection = [];
230
230
  if (Repository) {
231
+ if (Repository.isLoading) {
232
+ await Repository.waitUntilDoneLoading();
233
+ }
231
234
  // Get entity or entities that match value
232
235
  if ((_.isArray(value) && !_.isEmpty(value)) || !!value) {
233
236
  if (_.isArray(value)) {
@@ -236,16 +239,16 @@ export default function withSelection(WrappedComponent) {
236
239
  let found = Repository.getById(value);
237
240
  if (found) {
238
241
  newSelection.push(found);
239
- } else if (Repository?.isRemote && Repository?.entities.length) {
242
+ // } else if (Repository?.isRemote && Repository?.entities.length) {
240
243
 
241
- // Value cannot be found in Repository, but actually exists on server
242
- // Try to get this value from the server directly
243
- Repository.filter(Repository.schema.model.idProperty, value);
244
- await Repository.load();
245
- found = Repository.getById(value);
246
- if (found) {
247
- newSelection.push(found);
248
- }
244
+ // // Value cannot be found in Repository, but actually exists on server
245
+ // // Try to get this value from the server directly
246
+ // Repository.filter(Repository.schema.model.idProperty, value);
247
+ // await Repository.load();
248
+ // found = Repository.getById(value);
249
+ // if (found) {
250
+ // newSelection.push(found);
251
+ // }
249
252
 
250
253
  }
251
254
  }
@@ -278,9 +281,6 @@ export default function withSelection(WrappedComponent) {
278
281
  };
279
282
 
280
283
  useEffect(() => {
281
- if (isReady) {
282
- return () => {};
283
- }
284
284
 
285
285
  (async () => {
286
286
 
@@ -291,7 +291,7 @@ export default function withSelection(WrappedComponent) {
291
291
  await Repository.load();
292
292
  }
293
293
 
294
- if (usesWithValue && !_.isNil(value)) {
294
+ if (!_.isNil(value)) {
295
295
 
296
296
  await conformSelectionToValue();
297
297
 
@@ -314,7 +314,7 @@ export default function withSelection(WrappedComponent) {
314
314
 
315
315
  })();
316
316
 
317
- }, []);
317
+ }, [value]);
318
318
 
319
319
  if (self) {
320
320
  self.selection = localSelection;
@@ -109,7 +109,7 @@ export default function withValue(WrappedComponent) {
109
109
  setLocalValue(value);
110
110
  }
111
111
  }, [value]);
112
-
112
+
113
113
  if (fieldSetRegisterChild) {
114
114
  useEffect(() => {
115
115
  fieldSetRegisterChild({
@@ -69,6 +69,18 @@ export default function Pagination(props) {
69
69
  isDisabled={isDisabled}
70
70
  tooltip="Show More"
71
71
  >Show More</Button>);
72
+ if (!Repository.isLocal) {
73
+ items.push(<IconButton
74
+ key="reload"
75
+ parent={self}
76
+ reference="reloadPageBtn"
77
+ {...iconButtonProps}
78
+ icon={<Icon as={Rotate} {...iconProps} color="trueGray.600" />}
79
+ onPress={() => Repository.reload()}
80
+ tooltip="Reload"
81
+ ml={2}
82
+ />);
83
+ }
72
84
  } else {
73
85
  isDisabled = page === 1;
74
86
  items.push(<IconButton
@@ -183,6 +183,7 @@ function Viewer(props) {
183
183
  flex={1}
184
184
  h={350}
185
185
  canEditorViewOnly={true}
186
+ canCrud={false}
186
187
  uniqueRepository={true}
187
188
  parent={self}
188
189
  {...propsToPass}
@@ -41,7 +41,6 @@ import RadioGroup from './Form/Field/RadioGroup/RadioGroup.js';
41
41
  import SquareButton from './Buttons/SquareButton.js';
42
42
  import TabPanel from './Panel/TabPanel.js';
43
43
  import Tag from './Form/Field/Tag/Tag.js';
44
- import TagViewer from './Viewer/TagViewer.js';
45
44
  import TextArea from './Form/Field/TextArea.js';
46
45
  import Text from './Form/Field/Text.js';
47
46
  import TimezonesCombo from './Form/Field/Combo/TimezonesCombo.js';
@@ -94,7 +93,6 @@ const components = {
94
93
  SquareButton,
95
94
  TabPanel,
96
95
  Tag,
97
- TagViewer,
98
96
  Text,
99
97
  TextArea,
100
98
  TimezonesCombo,
@@ -1,30 +0,0 @@
1
- import {
2
- Text,
3
- } from 'native-base';
4
- import UiGlobals from '../../UiGlobals.js';
5
- import withComponent from '../Hoc/withComponent.js';
6
- import _ from 'lodash';
7
-
8
- function TagViewer(props) {
9
- const {
10
- value,
11
- } = props,
12
- parsedValue = value ? JSON.parse(value) : null,
13
- values = parsedValue ? _.map(parsedValue, (val) => {
14
- const ret = val?.text;
15
- return ret;
16
- }).join(', ') : [],
17
- styles = UiGlobals.styles;
18
-
19
- return <Text
20
- numberOfLines={1}
21
- ellipsizeMode="head"
22
- fontSize={styles.FORM_TEXT_FONTSIZE}
23
- minHeight='40px'
24
- px={3}
25
- py={2}
26
- {...props}
27
- >{values}</Text>;
28
- }
29
-
30
- export default withComponent(TagViewer);