@onehat/ui 0.4.34 → 0.4.35

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.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -15,7 +15,9 @@ function Editor(props) {
15
15
  onEditorSave: onSave,
16
16
  onEditorClose: onClose,
17
17
  onEditorDelete: onDelete,
18
- getEditorMode,
18
+ getEditorMode = () => {
19
+ return props.editorMode;
20
+ },
19
21
  onEditMode,
20
22
  canRecordBeEdited,
21
23
  _viewer = {},
@@ -65,8 +65,7 @@ import _ from 'lodash';
65
65
  // Form is embedded on screen in some other way. Mainly use startingValues, items, validator
66
66
 
67
67
  function Form(props) {
68
- const
69
- {
68
+ const {
70
69
  editorType = EDITOR_TYPE__WINDOWED, // EDITOR_TYPE__INLINE | EDITOR_TYPE__WINDOWED | EDITOR_TYPE__SIDE | EDITOR_TYPE__SMART | EDITOR_TYPE__PLAIN
71
70
  startingValues = {},
72
71
  items = [], // Columns, FieldSets, Fields, etc to define the form
@@ -114,7 +113,7 @@ function Form(props) {
114
113
  // withEditor
115
114
  isEditorViewOnly = false,
116
115
  isSaving = false,
117
- getEditorMode,
116
+ getEditorMode = () => {},
118
117
  onCancel,
119
118
  onSave,
120
119
  onClose,
@@ -247,16 +246,14 @@ function Form(props) {
247
246
  } else {
248
247
  // editor is not defined, fall back to property definition
249
248
  if (isEditable) {
250
- const
251
- {
249
+ const {
252
250
  type: t,
253
251
  ...p
254
252
  } = propertyDef?.editorType;
255
253
  type = t;
256
254
  editorTypeProps = p;
257
255
  } else if (propertyDef?.viewerType) {
258
- const
259
- {
256
+ const {
260
257
  type: t,
261
258
  ...p
262
259
  } = propertyDef?.viewerType;
@@ -504,16 +501,14 @@ function Form(props) {
504
501
  }
505
502
  if (!type) {
506
503
  if (isEditable) {
507
- const
508
- {
504
+ const {
509
505
  type: t,
510
506
  ...p
511
507
  } = propertyDef?.editorType;
512
508
  type = t;
513
509
  editorTypeProps = p;
514
510
  } else if (propertyDef?.viewerType) {
515
- const
516
- {
511
+ const {
517
512
  type: t,
518
513
  ...p
519
514
  } = propertyDef?.viewerType;
@@ -391,6 +391,7 @@ function GridComponent(props) {
391
391
 
392
392
  let rowComponent =
393
393
  <Pressable
394
+ dataSet={{ ix: index }}
394
395
  {...testProps(getRowTestId ? getRowTestId(row) : ((Repository ? Repository.schema.name : 'GridRow') + '-' + item?.id))}
395
396
  onPress={(e) => {
396
397
  if (e.preventDefault && e.cancelable) {
@@ -467,7 +468,7 @@ function GridComponent(props) {
467
468
  onContextMenu(item, e, selection);
468
469
  }
469
470
  }}
470
- className="flex-row grow">
471
+ className="Pressable Row flex-row grow">
471
472
  {({
472
473
  hovered,
473
474
  focused,
@@ -1182,7 +1183,7 @@ function GridComponent(props) {
1182
1183
  initialNumToRender={initialNumToRender}
1183
1184
  initialScrollIndex={0}
1184
1185
  renderItem={renderRow}
1185
- className="FlatList bg-grey-100"
1186
+ className="bg-grey-100"
1186
1187
  {...flatListProps}
1187
1188
  />;
1188
1189
 
@@ -319,7 +319,7 @@ function GridRow(props) {
319
319
  rowClassName += ' border-4 border-[#0ff]';
320
320
  }
321
321
  return <HStackNative
322
- {...testProps('row' + (isSelected ? '-selected' : ''))}
322
+ {...testProps('Row ' + (isSelected ? 'row-selected' : ''))}
323
323
  {...rowProps}
324
324
  key={hash}
325
325
  className={rowClassName}
@@ -13,6 +13,7 @@ import {
13
13
  EDITOR_TYPE__SIDE,
14
14
  EDITOR_TYPE__INLINE,
15
15
  } from '../../../Constants/Editor.js';
16
+ import useForceUpdate from '../../../Hooks/useForceUpdate.js'
16
17
  import Button from '../../Buttons/Button.js';
17
18
  import UiGlobals from '../../../UiGlobals.js';
18
19
  import _ from 'lodash';
@@ -27,7 +28,6 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
27
28
  return <WrappedComponent {...props} ref={ref} isTree={isTree} />;
28
29
  }
29
30
 
30
- let [secondaryEditorMode, secondarySetEditorMode] = useState(EDITOR_MODE__VIEW); // Can change below, so use 'let'
31
31
  const {
32
32
  secondaryUserCanEdit = true, // not permissions, but capability
33
33
  secondaryUserCanView = true,
@@ -72,6 +72,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
72
72
 
73
73
  // withSecondarySelection
74
74
  secondarySelection,
75
+ secondaryGetSelection,
75
76
  secondarySetSelection,
76
77
 
77
78
  // withAlert
@@ -79,17 +80,25 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
79
80
  confirm,
80
81
  hideAlert,
81
82
  } = props,
83
+ forceUpdate = useForceUpdate(),
82
84
  secondaryListeners = useRef({}),
83
85
  secondaryEditorStateRef = useRef(),
84
86
  secondaryNewEntityDisplayValueRef = useRef(),
87
+ secondaryEditorModeRef = useRef(EDITOR_MODE__VIEW),
88
+ secondaryIsIgnoreNextSelectionChangeRef = useRef(false),
85
89
  secondaryModel = SecondaryRepository?.schema?.name,
86
90
  [secondaryCurrentRecord, secondarySetCurrentRecord] = useState(null),
87
91
  [secondaryIsAdding, setIsAdding] = useState(false),
88
92
  [secondaryIsSaving, setIsSaving] = useState(false),
89
93
  [secondaryIsEditorShown, secondarySetIsEditorShownRaw] = useState(false),
90
94
  [secondaryIsEditorViewOnly, setIsEditorViewOnly] = useState(secondaryCanEditorViewOnly), // current state of whether editor is in view-only mode
91
- [secondaryIsIgnoreNextSelectionChange, setSecondaryIsIgnoreNextSelectionChange] = useState(false),
92
95
  [secondaryLastSelection, setLastSelection] = useState(),
96
+ secondarySetIsIgnoreNextSelectionChange = (bool) => {
97
+ secondaryIsIgnoreNextSelectionChangeRef.current = bool;
98
+ },
99
+ secondaryGetIsIgnoreNextSelectionChange = () => {
100
+ return secondaryIsIgnoreNextSelectionChangeRef.current;
101
+ },
93
102
  secondarySetIsEditorShown = (bool) => {
94
103
  secondarySetIsEditorShownRaw(bool);
95
104
  if (!bool && secondaryOnEditorClose) {
@@ -100,8 +109,10 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
100
109
  function doIt() {
101
110
  secondarySetSelection(newSelection);
102
111
  }
103
- const formState = secondaryEditorStateRef.current;
104
- if (!_.isEmpty(formState?.dirtyFields) && newSelection !== secondarySelection && secondaryEditorMode === EDITOR_MODE__EDIT) {
112
+ const
113
+ formState = secondaryEditorStateRef.current,
114
+ secondarySelection = secondaryGetSelection();
115
+ if (!_.isEmpty(formState?.dirtyFields) && newSelection !== secondarySelection && secondaryGetEditorMode() === EDITOR_MODE__EDIT) {
105
116
  confirm('This record has unsaved changes. Are you sure you want to cancel editing? Changes will be lost.', doIt);
106
117
  } else if (secondarySelection && secondarySelection[0] && !secondarySelection[0].isDestroyed && (secondarySelection[0]?.isPhantom || secondarySelection[0]?.isRemotePhantom)) {
107
118
  confirm('This new record is unsaved. Are you sure you want to cancel editing? Changes will be lost.', async () => {
@@ -119,6 +130,15 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
119
130
  secondaryListeners.current = obj;
120
131
  // forceUpdate(); // we don't want to get into an infinite loop of renders. Simply directly assign the secondaryListeners in every child render
121
132
  },
133
+ secondaryGetEditorMode = () => {
134
+ return secondaryEditorModeRef.current;
135
+ },
136
+ secondarySetEditorMode = (mode) => {
137
+ if (secondaryEditorModeRef.current !== mode) {
138
+ secondaryEditorModeRef.current = mode;
139
+ forceUpdate();
140
+ }
141
+ },
122
142
  getNewEntityDisplayValue = () => {
123
143
  return secondaryNewEntityDisplayValueRef.current;
124
144
  },
@@ -128,6 +148,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
128
148
  return;
129
149
  }
130
150
 
151
+ const secondarySelection = secondaryGetSelection();
131
152
  let addValues = values;
132
153
 
133
154
  if (SecondaryRepository?.isLoading) {
@@ -202,7 +223,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
202
223
  setIsSaving(true);
203
224
  const entity = await SecondaryRepository.add(addValues, false, true);
204
225
  setIsSaving(false);
205
- setSecondaryIsIgnoreNextSelectionChange(true);
226
+ secondarySetIsIgnoreNextSelectionChange(true);
206
227
  secondarySetSelection([entity]);
207
228
  if (getListeners().onAfterAdd) {
208
229
  await getListeners().onAfterAdd(entity);
@@ -225,6 +246,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
225
246
  showPermissionsError(EDIT, secondaryModel);
226
247
  return;
227
248
  }
249
+ const secondarySelection = secondaryGetSelection();
228
250
  if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
229
251
  return;
230
252
  }
@@ -247,6 +269,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
247
269
  if (_.isFunction(args)) {
248
270
  cb = args;
249
271
  }
272
+ const secondarySelection = secondaryGetSelection();
250
273
  if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
251
274
  return;
252
275
  }
@@ -305,6 +328,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
305
328
  showPermissionsError(DELETE, secondaryModel);
306
329
  return;
307
330
  }
331
+ const secondarySelection = secondaryGetSelection();
308
332
  if (getListeners().onBeforeDelete) {
309
333
  const listenerResult = await getListeners().onBeforeDelete(secondarySelection);
310
334
  if (listenerResult === false) {
@@ -345,6 +369,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
345
369
 
346
370
  // check permissions for view
347
371
 
372
+ const secondarySelection = secondaryGetSelection();
348
373
  if (secondarySelection.length !== 1) {
349
374
  return;
350
375
  }
@@ -367,7 +392,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
367
392
 
368
393
  // check permissions for duplicate
369
394
 
370
-
395
+ const secondarySelection = secondaryGetSelection();
371
396
  if (secondarySelection.length !== 1) {
372
397
  return;
373
398
  }
@@ -381,29 +406,32 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
381
406
  rawValues = _.omit(entity.getOriginalData(), idProperty);
382
407
  rawValues.id = null; // unset the id of the duplicate
383
408
  const duplicate = await SecondaryRepository.add(rawValues, false, true);
384
- setSecondaryIsIgnoreNextSelectionChange(true);
409
+ secondarySetIsIgnoreNextSelectionChange(true);
385
410
  secondarySetSelection([duplicate]);
386
411
  secondarySetEditorMode(EDITOR_MODE__EDIT);
387
412
  secondarySetIsEditorShown(true);
388
413
  },
389
414
  onRemoteDuplicate = async () => {
390
415
  const
416
+ secondarySelection = secondaryGetSelection(),
391
417
  entity = secondarySelection[0],
392
418
  duplicateEntity = await SecondaryRepository.remoteDuplicate(entity);
393
419
 
394
- setSecondaryIsIgnoreNextSelectionChange(true);
420
+ secondarySetIsIgnoreNextSelectionChange(true);
395
421
  secondarySetSelection([duplicateEntity]);
396
422
  secondaryDoEdit();
397
423
  },
398
424
  secondaryDoEditorSave = async (data, e) => {
399
- let mode = secondaryEditorMode === EDITOR_MODE__ADD ? ADD : EDIT;
425
+ let mode = secondaryGetEditorMode() === EDITOR_MODE__ADD ? ADD : EDIT;
400
426
  if (canUser && !canUser(mode, secondaryModel)) {
401
427
  showPermissionsError(mode, secondaryModel);
402
428
  return;
403
429
  }
404
430
 
405
431
  // NOTE: The Form submits onSave for both adds (when not isAutoSsave) and edits.
406
- const isSingle = secondarySelection.length === 1;
432
+ const
433
+ secondarySelection = secondaryGetSelection(),
434
+ isSingle = secondarySelection.length === 1;
407
435
  let useStaged = false;
408
436
  if (isSingle) {
409
437
  // just update this one entity
@@ -450,7 +478,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
450
478
  if (secondaryOnChange) {
451
479
  secondaryOnChange(secondarySelection);
452
480
  }
453
- if (secondaryEditorMode === EDITOR_MODE__ADD) {
481
+ if (secondaryGetEditorMode() === EDITOR_MODE__ADD) {
454
482
  if (secondaryOnAdd) {
455
483
  await secondaryOnAdd(secondarySelection);
456
484
  }
@@ -463,7 +491,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
463
491
  } else {
464
492
  secondarySetEditorMode(EDITOR_MODE__VIEW);
465
493
  }
466
- } else if (secondaryEditorMode === EDITOR_MODE__EDIT) {
494
+ } else if (secondaryGetEditorMode() === EDITOR_MODE__EDIT) {
467
495
  if (getListeners().onAfterEdit) {
468
496
  await getListeners().onAfterEdit(secondarySelection);
469
497
  }
@@ -479,6 +507,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
479
507
  secondaryDoEditorCancel = () => {
480
508
  async function doIt() {
481
509
  const
510
+ secondarySelection = secondaryGetSelection(),
482
511
  isSingle = secondarySelection.length === 1,
483
512
  isPhantom = secondarySelection[0] && !secondarySelection[0]?.isDestroyed && secondarySelection[0].isPhantom;
484
513
  if (isSingle && isPhantom) {
@@ -517,9 +546,10 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
517
546
  secondarySetIsEditorShown(false);
518
547
  });
519
548
  },
520
- calculateEditorMode = (secondaryIsIgnoreNextSelectionChange = false) => {
549
+ calculateEditorMode = () => {
521
550
 
522
- let doStayInEditModeOnSelectionChange = secondaryStayInEditModeOnSelectionChange;
551
+ let secondaryIsIgnoreNextSelectionChange = secondaryGetIsIgnoreNextSelectionChange(),
552
+ doStayInEditModeOnSelectionChange = secondaryStayInEditModeOnSelectionChange;
523
553
  if (!_.isNil(UiGlobals.stayInEditModeOnSelectionChange)) {
524
554
  // allow global override to for this property
525
555
  doStayInEditModeOnSelectionChange = UiGlobals.stayInEditModeOnSelectionChange;
@@ -529,13 +559,14 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
529
559
  }
530
560
 
531
561
  // calculateEditorMode gets called only on selection changes
562
+ const secondarySelection = secondaryGetSelection();
532
563
  let mode;
533
564
  if (secondaryEditorType === EDITOR_TYPE__SIDE && !_.isNil(UiGlobals.isSideEditorAlwaysEditMode) && UiGlobals.isSideEditorAlwaysEditMode) {
534
565
  // special case: side editor is always edit mode
535
566
  mode = EDITOR_MODE__EDIT;
536
567
  } else {
537
568
  if (secondaryIsIgnoreNextSelectionChange) {
538
- mode = secondaryEditorMode;
569
+ mode = secondaryGetEditorMode();
539
570
  if (!secondaryCanEditorViewOnly && secondaryUserCanEdit) {
540
571
  if (secondarySelection.length > 1) {
541
572
  if (!secondaryDisableEdit) {
@@ -581,9 +612,9 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
581
612
  };
582
613
 
583
614
  useEffect(() => {
584
- secondarySetEditorMode(calculateEditorMode(secondaryIsIgnoreNextSelectionChange));
615
+ secondarySetEditorMode(calculateEditorMode());
585
616
 
586
- setSecondaryIsIgnoreNextSelectionChange(false);
617
+ secondarySetIsIgnoreNextSelectionChange(false);
587
618
  setLastSelection(secondarySelection);
588
619
  }, [secondarySelection]);
589
620
 
@@ -602,7 +633,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
602
633
  // NOTE: If I don't calculate this on the fly for selection changes,
603
634
  // we see a flash of the previous state, since useEffect hasn't yet run.
604
635
  // (basically redo what's in the useEffect, above)
605
- secondaryEditorMode = calculateEditorMode(secondaryIsIgnoreNextSelectionChange);
636
+ secondarySetEditorMode(calculateEditorMode());
606
637
  }
607
638
 
608
639
  return <WrappedComponent
@@ -615,11 +646,12 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
615
646
  secondaryIsEditorViewOnly={secondaryIsEditorViewOnly}
616
647
  secondaryIsAdding={secondaryIsAdding}
617
648
  secondaryIsSaving={secondaryIsSaving}
618
- secondaryEditorMode={secondaryEditorMode}
649
+ secondaryEditorMode={secondaryGetEditorMode()}
619
650
  secondaryOnEditMode={secondarySetEditMode}
620
651
  secondaryOnViewMode={secondarySetViewMode}
621
652
  secondaryEditorStateRef={secondaryEditorStateRef}
622
653
  secondarySetIsEditorShown={secondarySetIsEditorShown}
654
+ secondarySetIsIgnoreNextSelectionChange={secondarySetIsIgnoreNextSelectionChange}
623
655
  secondaryOnAdd={(!secondaryUserCanEdit || secondaryDisableAdd) ? null : secondaryDoAdd}
624
656
  secondaryOnEdit={(!secondaryUserCanEdit || secondaryDisableEdit) ? null : secondaryDoEdit}
625
657
  secondaryOnDelete={(!secondaryUserCanEdit || secondaryDisableDelete) ? null : secondaryDoDelete}
@@ -1,18 +1,18 @@
1
- import { useState, useEffect, } from 'react';
1
+ import { forwardRef, useState, useEffect, useRef, } from 'react';
2
2
  import {
3
3
  SELECTION_MODE_SINGLE,
4
4
  SELECTION_MODE_MULTI,
5
5
  SELECT_UP,
6
6
  SELECT_DOWN,
7
7
  } from '../../../Constants/Selection.js';
8
+ import useForceUpdate from '../../../Hooks/useForceUpdate.js';
8
9
  import inArray from '../../../Functions/inArray.js';
9
10
  import _ from 'lodash';
10
11
 
11
12
  // NOTE: This is a modified version of @onehat/ui/src/Hoc/withSelection
12
- // This HOC will eventually get out of sync with that one, and may need to be updated.
13
13
 
14
14
  export default function withSelection(WrappedComponent) {
15
- return (props) => {
15
+ return forwardRef((props, ref) => {
16
16
 
17
17
  if (props.secondaryDisableWithSelection) {
18
18
  return <WrappedComponent {...props} />;
@@ -25,8 +25,7 @@ export default function withSelection(WrappedComponent) {
25
25
  return <WrappedComponent {...props} />;
26
26
  }
27
27
 
28
- const
29
- {
28
+ const {
30
29
  secondarySelection,
31
30
  secondaryDefaultSelection,
32
31
  secondaryOnChangeSelection,
@@ -49,20 +48,25 @@ export default function withSelection(WrappedComponent) {
49
48
  } = props,
50
49
  usesWithValue = !!secondarySetValue,
51
50
  initialSelection = secondarySelection || secondaryDefaultSelection || [],
52
- [secondaryLocalSelection, setLocalSelection] = useState(initialSelection),
51
+ forceUpdate = useForceUpdate(),
52
+ secondarySelectionRef = useRef(initialSelection),
53
53
  [isReady, setIsReady] = useState(secondarySelection || false), // if secondarySelection is already defined, or secondaryValue is not null and we don't need to load repository, it's ready
54
54
  secondarySetSelection = (secondarySelection) => {
55
- if (_.isEqual(secondarySelection, secondaryLocalSelection)) {
55
+ if (_.isEqual(secondarySelection, secondaryGetSelection())) {
56
56
  return;
57
57
  }
58
58
 
59
- setLocalSelection(secondarySelection);
59
+ secondarySelectionRef.current = secondarySelection;
60
60
  if (secondaryOnChangeSelection) {
61
61
  secondaryOnChangeSelection(secondarySelection);
62
62
  }
63
63
  if (fireEvent) {
64
64
  fireEvent('secondaryChangeSelection', secondarySelection);
65
65
  }
66
+ forceUpdate();
67
+ },
68
+ secondaryGetSelection = () => {
69
+ return secondarySelectionRef.current;
66
70
  },
67
71
  secondarySelectPrev = () => {
68
72
  secondarySelectDirection(SELECT_UP);
@@ -103,21 +107,21 @@ export default function withSelection(WrappedComponent) {
103
107
  }
104
108
  },
105
109
  secondaryAddToSelection = (item) => {
106
- const newSelection = _.clone(secondaryLocalSelection); // so we get a new object, so descendants rerender
110
+ const newSelection = _.clone(secondaryGetSelection()); // so we get a new object, so descendants rerender
107
111
  newSelection.push(item);
108
112
  secondarySetSelection(newSelection);
109
113
  },
110
114
  secondaryRemoveFromSelection = (item) => {
111
115
  let newSelection = [];
112
116
  if (SecondaryRepository) {
113
- newSelection = _.remove(secondaryLocalSelection, (sel) => sel !== item);
117
+ newSelection = _.remove(secondaryGetSelection(), (sel) => sel !== item);
114
118
  } else {
115
- newSelection = _.remove(secondaryLocalSelection, (sel) => sel[secondaryIdIx] !== item[secondaryIdIx]);
119
+ newSelection = _.remove(secondaryGetSelection(), (sel) => sel[secondaryIdIx] !== item[secondaryIdIx]);
116
120
  }
117
121
  secondarySetSelection(newSelection);
118
122
  },
119
123
  secondaryDeselectAll = () => {
120
- if (!_.isEmpty(secondaryLocalSelection)) {
124
+ if (!_.isEmpty(secondaryGetSelection())) {
121
125
  secondarySetSelection([]);
122
126
  }
123
127
  },
@@ -128,7 +132,7 @@ export default function withSelection(WrappedComponent) {
128
132
  // That way, after a load event, we'll keep the same selection, if possible.
129
133
  const
130
134
  newSelection = [],
131
- ids = _.map(secondaryLocalSelection, (item) => item.id);
135
+ ids = _.map(secondaryGetSelection(), (item) => item.id);
132
136
  _.each(ids, (id) => {
133
137
  const found = SecondaryRepository.getById(id);
134
138
  if (found) {
@@ -162,9 +166,9 @@ export default function withSelection(WrappedComponent) {
162
166
  secondarySelectRangeTo = (item) => {
163
167
  // Select above max or below min to this one
164
168
  const
165
- currentSelectionLength = secondaryLocalSelection.length,
169
+ currentSelectionLength = secondaryGetSelection().length,
166
170
  index = getIndexOfSelectedItem(item);
167
- let newSelection = _.clone(secondaryLocalSelection); // so we get a new object, so descendants rerender
171
+ let newSelection = _.clone(secondaryGetSelection()); // so we get a new object, so descendants rerender
168
172
 
169
173
  if (currentSelectionLength) {
170
174
  const { items, max, min, } = getMaxMinSelectionIndices();
@@ -191,10 +195,10 @@ export default function withSelection(WrappedComponent) {
191
195
  },
192
196
  secondaryIsInSelection = (item) => {
193
197
  if (SecondaryRepository) {
194
- return inArray(item, secondaryLocalSelection);
198
+ return inArray(item, secondaryGetSelection());
195
199
  }
196
200
 
197
- const found = _.find(secondaryLocalSelection, (selectedItem) => {
201
+ const found = _.find(secondaryGetSelection(), (selectedItem) => {
198
202
  return selectedItem[secondaryIdIx] === item[secondaryIdIx];
199
203
  });
200
204
  return !!found;
@@ -216,10 +220,10 @@ export default function withSelection(WrappedComponent) {
216
220
  return found;
217
221
  },
218
222
  secondaryGetIdsFromLocalSelection = () => {
219
- if (!secondaryLocalSelection[0]) {
223
+ if (!secondaryGetSelection()[0]) {
220
224
  return null;
221
225
  }
222
- const secondaryValues = _.map(secondaryLocalSelection, (item) => {
226
+ const secondaryValues = _.map(secondaryGetSelection(), (item) => {
223
227
  if (SecondaryRepository) {
224
228
  return item.id;
225
229
  }
@@ -303,7 +307,7 @@ export default function withSelection(WrappedComponent) {
303
307
  }
304
308
  }
305
309
 
306
- if (!_.isEqual(newSelection, secondaryLocalSelection)) {
310
+ if (!_.isEqual(newSelection, secondaryGetSelection())) {
307
311
  secondarySetSelection(newSelection);
308
312
  }
309
313
  };
@@ -354,7 +358,7 @@ export default function withSelection(WrappedComponent) {
354
358
  }, [secondaryValue]);
355
359
 
356
360
  if (self) {
357
- self.secondarySelection = secondaryLocalSelection;
361
+ self.secondarySelection = secondaryGetSelection();
358
362
  self.secondarySetSelection = secondarySetSelection;
359
363
  self.secondarySelectPrev = secondarySelectPrev;
360
364
  self.secondarySelectNext = secondarySelectNext;
@@ -395,8 +399,10 @@ export default function withSelection(WrappedComponent) {
395
399
 
396
400
  return <WrappedComponent
397
401
  {...props}
402
+ ref={ref}
398
403
  secondaryDisableWithSelection={false}
399
- secondarySelection={secondaryLocalSelection}
404
+ secondarySelection={secondaryGetSelection()}
405
+ secondaryGetSelection={secondaryGetSelection}
400
406
  secondarySetSelection={secondarySetSelection}
401
407
  secondarySelectionMode={secondarySelectionMode}
402
408
  secondarySelectPrev={secondarySelectPrev}
@@ -411,5 +417,5 @@ export default function withSelection(WrappedComponent) {
411
417
  secondaryGetIdsFromSelection={secondaryGetIdsFromLocalSelection}
412
418
  secondaryGetDisplayValuesFromSelection={secondaryGetDisplayValuesFromLocalSelection}
413
419
  />;
414
- };
420
+ });
415
421
  }
@@ -13,6 +13,7 @@ import {
13
13
  EDITOR_TYPE__SIDE,
14
14
  EDITOR_TYPE__INLINE,
15
15
  } from '../../Constants/Editor.js';
16
+ import useForceUpdate from '../../Hooks/useForceUpdate.js'
16
17
  import Button from '../Buttons/Button.js';
17
18
  import UiGlobals from '../../UiGlobals.js';
18
19
  import _ from 'lodash';
@@ -76,6 +77,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
76
77
  confirm,
77
78
  hideAlert,
78
79
  } = props,
80
+ forceUpdate = useForceUpdate(),
79
81
  listeners = useRef({}),
80
82
  editorStateRef = useRef(),
81
83
  newEntityDisplayValueRef = useRef(),
@@ -128,7 +130,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
128
130
  return editorModeRef.current;
129
131
  },
130
132
  setEditorMode = (mode) => {
131
- editorModeRef.current = mode;
133
+ if (editorModeRef.current !== mode) {
134
+ editorModeRef.current = mode;
135
+ forceUpdate();
136
+ }
132
137
  },
133
138
  getNewEntityDisplayValue = () => {
134
139
  return newEntityDisplayValueRef.current;
@@ -11,7 +11,6 @@ import Form from '../Form/Form.js';
11
11
  import Pdf from '../Icons/Pdf.js';
12
12
  import UiGlobals from '../../UiGlobals.js';
13
13
  import inArray from '../../Functions/inArray.js';
14
- import testProps from '../../Functions/testProps.js';
15
14
  import _ from 'lodash';
16
15
 
17
16
  export default function withPdfButtons(WrappedComponent) {
@@ -23,8 +23,7 @@ export default function withSelection(WrappedComponent) {
23
23
  return <WrappedComponent {...props} ref={ref} />;
24
24
  }
25
25
 
26
- const
27
- {
26
+ const {
28
27
  selection,
29
28
  defaultSelection,
30
29
  onChangeSelection,
@@ -120,7 +119,9 @@ export default function withSelection(WrappedComponent) {
120
119
  setSelection(newSelection);
121
120
  },
122
121
  deselectAll = () => {
123
- setSelection([]);
122
+ if (!_.isEmpty(getSelection())) {
123
+ setSelection([]);
124
+ }
124
125
  },
125
126
  refreshSelection = () => {
126
127
  // When Repository reloads, the entities get destroyed.
@@ -39,11 +39,21 @@ export function verifyElementDoesNotExist(selectors) {
39
39
  cy.get('body').then(($body) => {
40
40
  const selectorString = getTestIdSelectors(selectors);
41
41
  if ($body.find(selectorString).length > 0) {
42
- throw new Error(`Element with selectors ${selectorString} exists in the DOM`);
42
+ throw new Error(`Element with selectors ${selectorString} exists in the DOM, when it should not`);
43
43
  }
44
44
  });
45
45
  }
46
46
 
47
+ /**
48
+ * Verifies that an element with the given selectors exists in the DOM.
49
+ * @argument {string | string[]} selectors - data-testid attribute values
50
+ * If an array is given, these will be considered nested selectors.
51
+ * e.g. ['parent', 'child'] will be converted to '[data-testid="parent"] [data-testid="child"]'
52
+ */
53
+ export function verifyElementExists(selectors) {
54
+ cy.get(getTestIdSelectors(selectors)).should('exist');
55
+ }
56
+
47
57
  /**
48
58
  * Builds selector string for data-testid attributes.
49
59
  * It leaves classname, id, and attribute selectors unchanged.
@@ -103,13 +113,21 @@ export function drag(draggableSelectors, droppableSelectors, options = {}) {
103
113
  });
104
114
  }
105
115
 
116
+
117
+ // conditions functions that make use of cypress-if
106
118
  export function ifExists(parentSelectors, name, cb) {
107
119
  if (_.isString(parentSelectors)) {
108
120
  parentSelectors = [parentSelectors];
109
121
  }
110
- return getDomNode([...parentSelectors, name]).if().then((node) => { // NOTE if() is a cypress-if function
122
+ return getDomNode([...parentSelectors, name]).if().then((node) => {
111
123
  if (node) {
112
124
  cb(node);
113
125
  }
114
126
  });
115
127
  }
128
+ export function ifNotExists(parentSelectors, name, cb) {
129
+ if (_.isString(parentSelectors)) {
130
+ parentSelectors = [parentSelectors];
131
+ }
132
+ return getDomNode([...parentSelectors, name]).if('not.exist').then(cb);
133
+ }
@@ -274,100 +274,100 @@ export function setInputValue(selectors, value) {
274
274
  setTextValue(selectors, value);
275
275
  }
276
276
 
277
+ /**
278
+ * Given a form,
279
+ * return a url-encoded string representing all keys and values
280
+ *
281
+ * @param {jQuery} form
282
+ * @return {String}
283
+ * /
284
+ export function formSerialize(form) {
285
+ 'use strict';
286
+ var i, j, len, jLen, formElement,
287
+ q = [],
288
+ theForm = form[0],
289
+ varCounters = {};
290
+
291
+ function addNameValue(name, value) { // create this function so I can use varCounters for
292
+ var matches = name.match(/([\w\d]+)\[\]/i),
293
+ varName,
294
+ ix = 0;
295
+ if (matches && matches[1]) {
296
+ varName = matches[1];
297
+ if (typeof varCounters[varName] === 'undefined') {
298
+ varCounters[varName] = ix;
299
+ } else {
300
+ ix = ++varCounters[varName];
301
+ }
302
+ name = varName + '[' + ix + ']';
303
+ }
304
+ q.push(urlencode(name) + '=' + urlencode(value));
305
+ }
277
306
 
278
- // /**
279
- // * Given a form,
280
- // * return a url-encoded string representing all keys and values
281
- // *
282
- // * @param {jQuery} form
283
- // * @return {String}
284
- // */
285
- // export function formSerialize(form) {
286
- // 'use strict';
287
- // var i, j, len, jLen, formElement,
288
- // q = [],
289
- // theForm = form[0],
290
- // varCounters = {};
291
-
292
- // function addNameValue(name, value) { // create this function so I can use varCounters for
293
- // var matches = name.match(/([\w\d]+)\[\]/i),
294
- // varName,
295
- // ix = 0;
296
- // if (matches && matches[1]) {
297
- // varName = matches[1];
298
- // if (typeof varCounters[varName] === 'undefined') {
299
- // varCounters[varName] = ix;
300
- // } else {
301
- // ix = ++varCounters[varName];
302
- // }
303
- // name = varName + '[' + ix + ']';
304
- // }
305
- // q.push(urlencode(name) + '=' + urlencode(value));
306
- // }
307
-
308
- // if (!theForm || !theForm.nodeName || theForm.nodeName.toLowerCase() !== 'form') {
309
- // throw 'You must supply a form element';
310
- // }
311
- // for (i = 0, len = theForm.elements.length; i < len; i++) {
312
- // formElement = theForm.elements[i];
313
- // if (formElement.name === '' || formElement.disabled) {
314
- // continue;
315
- // }
316
- // switch (formElement.nodeName.toLowerCase()) {
317
- // case 'input':
318
- // switch (formElement.type) {
319
- // case 'text':
320
- // case 'hidden':
321
- // case 'password':
322
- // case 'button': // Not submitted when submitting form manually, though jQuery does serialize this and it can be an HTML4 successful control
323
- // case 'submit':
324
- // addNameValue(formElement.name, formElement.value);
325
- // break;
326
- // case 'checkbox':
327
- // case 'radio':
328
- // if (formElement.checked) {
329
- // addNameValue(formElement.name, formElement.value);
330
- // } else if (formElement.value === '1') {
331
- // addNameValue(formElement.name, '0'); // Submit actual value of zero for booleans, instead of no value at all
332
- // }
333
- // break;
334
- // case 'file':
335
- // // addNameValue(formElement.name, formElement.value); // Will work and part of HTML4 "successful controls", but not used in jQuery
336
- // break;
337
- // case 'reset':
338
- // break;
339
- // }
340
- // break;
341
- // case 'textarea':
342
- // addNameValue(formElement.name, formElement.value);
343
- // break;
344
- // case 'select':
345
- // switch (formElement.type) {
346
- // case 'select-one':
347
- // addNameValue(formElement.name, formElement.value);
348
- // break;
349
- // case 'select-multiple':
350
- // for (j = 0, jLen = formElement.options.length; j < jLen; j++) {
351
- // if (formElement.options[j].selected) {
352
- // addNameValue(formElement.name, formElement.options[j].value);
353
- // }
354
- // }
355
- // break;
356
- // }
357
- // break;
358
- // case 'button': // jQuery does not submit these, though it is an HTML4 successful control
359
- // switch (formElement.type) {
360
- // case 'reset':
361
- // case 'submit':
362
- // case 'button':
363
- // addNameValue(formElement.name, formElement.value);
364
- // break;
365
- // }
366
- // break;
367
- // }
368
- // }
369
- // return q.join('&');
370
- // }
307
+ if (!theForm || !theForm.nodeName || theForm.nodeName.toLowerCase() !== 'form') {
308
+ throw 'You must supply a form element';
309
+ }
310
+ for (i = 0, len = theForm.elements.length; i < len; i++) {
311
+ formElement = theForm.elements[i];
312
+ if (formElement.name === '' || formElement.disabled) {
313
+ continue;
314
+ }
315
+ switch (formElement.nodeName.toLowerCase()) {
316
+ case 'input':
317
+ switch (formElement.type) {
318
+ case 'text':
319
+ case 'hidden':
320
+ case 'password':
321
+ case 'button': // Not submitted when submitting form manually, though jQuery does serialize this and it can be an HTML4 successful control
322
+ case 'submit':
323
+ addNameValue(formElement.name, formElement.value);
324
+ break;
325
+ case 'checkbox':
326
+ case 'radio':
327
+ if (formElement.checked) {
328
+ addNameValue(formElement.name, formElement.value);
329
+ } else if (formElement.value === '1') {
330
+ addNameValue(formElement.name, '0'); // Submit actual value of zero for booleans, instead of no value at all
331
+ }
332
+ break;
333
+ case 'file':
334
+ // addNameValue(formElement.name, formElement.value); // Will work and part of HTML4 "successful controls", but not used in jQuery
335
+ break;
336
+ case 'reset':
337
+ break;
338
+ }
339
+ break;
340
+ case 'textarea':
341
+ addNameValue(formElement.name, formElement.value);
342
+ break;
343
+ case 'select':
344
+ switch (formElement.type) {
345
+ case 'select-one':
346
+ addNameValue(formElement.name, formElement.value);
347
+ break;
348
+ case 'select-multiple':
349
+ for (j = 0, jLen = formElement.options.length; j < jLen; j++) {
350
+ if (formElement.options[j].selected) {
351
+ addNameValue(formElement.name, formElement.options[j].value);
352
+ }
353
+ }
354
+ break;
355
+ }
356
+ break;
357
+ case 'button': // jQuery does not submit these, though it is an HTML4 successful control
358
+ switch (formElement.type) {
359
+ case 'reset':
360
+ case 'submit':
361
+ case 'button':
362
+ addNameValue(formElement.name, formElement.value);
363
+ break;
364
+ }
365
+ break;
366
+ }
367
+ }
368
+ return q.join('&');
369
+ }
370
+ */
371
371
 
372
372
 
373
373
 
@@ -377,94 +377,100 @@ export function setInputValue(selectors, value) {
377
377
  // / /_/ / __/ /_/ /_/ __/ / (__ )
378
378
  // \____/\___/\__/\__/\___/_/ /____/
379
379
 
380
- // /**
381
- // * Get data from a form
382
- // * @param {object} schema - fieldName/fieldType pairs
383
- // * @returns {object} formValues - object of fieldName/value pairs
384
- // */
385
- // export function getFormValues(editor, schema) {
386
- // const fields = editor.find('.x-form-field'),
387
- // formValues = {};
388
-
389
- // _.each(fields, (field) => {
390
- // const fieldType = schema[field.name];
391
- // switch(fieldType) {
392
- // case 'checkbox':
393
- // formValues[fieldName] = getCheckboxValue(fieldName);
394
- // break;
395
- // case 'combo':
396
- // formValues[fieldName] = getComboValue(fieldName);
397
- // break;
398
- // case 'date':
399
- // formValues[fieldName] = getDateValue(fieldName);
400
- // break;
401
- // case 'datetime':
402
- // formValues[fieldName] = getDatetimeValue(fieldName);
403
- // break;
404
- // case 'file':
405
- // formValues[fieldName] = getFileValue(fieldName);
406
- // break;
407
- // case 'number':
408
- // formValues[fieldName] = getNumberValue(fieldName);
409
- // break;
410
- // case 'radio':
411
- // formValues[fieldName] = getRadioValue(fieldName);
412
- // break;
413
- // case 'tag':
414
- // formValues[fieldName] = getTagValue(fieldName);
415
- // break;
416
- // case 'text':
417
- // case 'textarea':
418
- // formValues[fieldName] = getTextValue(fieldName);
419
- // break;
420
- // case 'time':
421
- // formValues[fieldName] = getTimeValue(fieldName);
422
- // break;
423
- // }
424
- // });
425
- // return formValues;
426
- // }
380
+ /**
381
+ * Get data from a form
382
+ * @param {object} schema - fieldName/fieldType pairs
383
+ * @returns {object} formValues - object of fieldName/value pairs
384
+ * /
385
+ export function getFormValues(editor, schema) {
386
+ const fields = editor.find('.x-form-field'),
387
+ formValues = {};
388
+
389
+ _.each(fields, (field) => {
390
+ const fieldType = schema[field.name];
391
+ switch(fieldType) {
392
+ case 'checkbox':
393
+ formValues[fieldName] = getCheckboxValue(fieldName);
394
+ break;
395
+ case 'combo':
396
+ formValues[fieldName] = getComboValue(fieldName);
397
+ break;
398
+ case 'date':
399
+ formValues[fieldName] = getDateValue(fieldName);
400
+ break;
401
+ case 'datetime':
402
+ formValues[fieldName] = getDatetimeValue(fieldName);
403
+ break;
404
+ case 'file':
405
+ formValues[fieldName] = getFileValue(fieldName);
406
+ break;
407
+ case 'number':
408
+ formValues[fieldName] = getNumberValue(fieldName);
409
+ break;
410
+ case 'radio':
411
+ formValues[fieldName] = getRadioValue(fieldName);
412
+ break;
413
+ case 'tag':
414
+ formValues[fieldName] = getTagValue(fieldName);
415
+ break;
416
+ case 'text':
417
+ case 'textarea':
418
+ formValues[fieldName] = getTextValue(fieldName);
419
+ break;
420
+ case 'time':
421
+ formValues[fieldName] = getTimeValue(fieldName);
422
+ break;
423
+ }
424
+ });
425
+ return formValues;
426
+ }
427
+ */
427
428
 
428
429
 
429
- // /**
430
- // * Validate that form values match what they're supposed to
431
- // */
432
- // export function validateFormValues(data, schema) {
433
- // cy.wrap().then(() => { // Wrap this in a Cypress promise, so it executes in correct order, relative to other Cypress promises
434
-
435
- // const formValues = getFormValues(schema);
436
- // let diff = deepDiffObj(formValues, data);
437
-
438
- // // SPECIAL CASE: Omit password fields from diff
439
- // const omitFields = [];
440
- // _.each(diff, (value, key) => {
441
- // if (key.match(/^password/i)) {
442
- // omitFields.push(key);
443
- // }
444
- // });
445
- // if (omitFields.length) {
446
- // diff = _.omit(diff, omitFields);
447
- // }
430
+ /**
431
+ * Validate that form values match what they're supposed to
432
+ * /
433
+ export function validateFormValues(data, schema) {
434
+ cy.wrap().then(() => { // Wrap this in a Cypress promise, so it executes in correct order, relative to other Cypress promises
435
+
436
+ const formValues = getFormValues(schema);
437
+ let diff = deepDiffObj(formValues, data);
438
+
439
+ // SPECIAL CASE: Omit password fields from diff
440
+ const omitFields = [];
441
+ _.each(diff, (value, key) => {
442
+ if (key.match(/^password/i)) {
443
+ omitFields.push(key);
444
+ }
445
+ });
446
+ if (omitFields.length) {
447
+ diff = _.omit(diff, omitFields);
448
+ }
448
449
 
449
- // // If there are still any differences, log them
450
- // if (_.keys(diff).length > 0) {
451
- // console.log('data', data);
452
- // console.log('formValues', formValues);
453
- // console.log('diff', diff);
454
- // }
455
-
456
- // expect(diff).to.deep.equal({});
457
- // });
458
- // }
450
+ // If there are still any differences, log them
451
+ if (_.keys(diff).length > 0) {
452
+ console.log('data', data);
453
+ console.log('formValues', formValues);
454
+ console.log('diff', diff);
455
+ }
456
+
457
+ expect(diff).to.deep.equal({});
458
+ });
459
+ }
460
+ */
459
461
 
460
462
 
461
463
 
462
464
  // export function getCheckboxValue(fieldName) {
463
465
 
464
466
  // }
465
- // export function getComboValue(fieldName) {
466
-
467
- // }
467
+ export function getComboValue(selectors, fieldName) {
468
+ cy.log('getComboValue ' + fieldName);
469
+ return getDomNode([...selectors, 'field-' + fieldName, 'input']).then((field) => {
470
+ return cy.wrap(field)
471
+ .invoke('val');
472
+ });
473
+ }
468
474
  // export function getDateValue(fieldName) {
469
475
 
470
476
  // }
@@ -11,6 +11,15 @@ import _ from 'lodash';
11
11
  const $ = Cypress.$;
12
12
 
13
13
 
14
+ // Get cells
15
+ export function getGridCellValue(gridSelector, rowId, field) {
16
+ cy.log('getGridCellValue ' + gridSelector + ' ' + rowId + ' ' + field);
17
+ const rowSelector = getGridRowSelectorById(gridSelector, rowId);
18
+ return getDomNode([gridSelector, rowSelector, 'cell-' + field])
19
+ .invoke('text');
20
+ }
21
+
22
+
14
23
 
15
24
  // Get rows
16
25
  export function hasRowWithFieldValue(gridSelector, field, value) {
@@ -56,12 +65,16 @@ export function selectGridRowIfNotAlreadySelectedById(gridSelector, id) {
56
65
  }
57
66
  })
58
67
  }
68
+ export function selectGridRowByIx(gridSelector, ix) {
69
+ cy.log('selectGridRowByIx ' + gridSelector + ' ' + ix);
70
+
71
+ ix++; // compensate for header row
72
+ getDomNode([gridSelector, '[data-ix=' + ix + ']'])
73
+ .click();
74
+ }
59
75
  // export function selectRowWithText(grid, text) {
60
76
  // getRowWithText(grid, text).click(5, 5);
61
77
  // }
62
- // export function selectRowWithIx(grid, ix) {
63
- // getRowWithIx(grid, ix).click(5, 5);
64
- // }
65
78
  // export function cmdClickRowWithId(grid, id) {
66
79
  // getRowWithId(grid, id).click('left', { metaKey: true });
67
80
  // }
@@ -234,12 +247,8 @@ export function getModelFromGridSelector(gridSelector) {
234
247
  }
235
248
  export function getGridRowSelectorById(gridSelector, id) {
236
249
  const
237
- model = getModelFromGridSelector(gridSelector);
238
-
239
- if (!model) {
240
- debugger;
241
- }
242
- const inflected = fixInflector(Inflector.camelize(Inflector.pluralize(model)));
250
+ model = getModelFromGridSelector(gridSelector),
251
+ inflected = fixInflector(Inflector.camelize(Inflector.pluralize(model)));
243
252
  return inflected + '-' + id;
244
253
  }
245
254