@onehat/ui 0.3.11 → 0.3.17

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.11",
3
+ "version": "0.3.17",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -29,13 +29,17 @@ export default function Editor(props) {
29
29
 
30
30
  if (_.isEmpty(selection)) {
31
31
  return null; // hide the editor when no selection
32
- return <Box {...props} bg="#ddd" />;
33
32
  }
34
33
 
35
- if (Repository?.isRemotePhantomMode && selection.length === 1 && editorMode === EDITOR_MODE__VIEW) {
34
+ // Repository?.isRemotePhantomMode && selection.length === 1 &&
35
+ if (editorMode === EDITOR_MODE__VIEW) {
36
+ const record = selection[0];
37
+ if (record.isDestroyed) {
38
+ return null;
39
+ }
36
40
  return <Viewer
37
41
  {...props}
38
- record={selection[0]}
42
+ record={record}
39
43
  onEditMode={isEditorViewOnly ? null : onEditMode}
40
44
  onClose={onClose}
41
45
  onDelete={onDelete}
@@ -6,6 +6,9 @@ import {
6
6
  Row,
7
7
  Text,
8
8
  } from 'native-base';
9
+ import {
10
+ EDITOR_TYPE__SIDE,
11
+ } from '../../Constants/Editor.js';
9
12
  import UiGlobals from '../../UiGlobals.js';
10
13
  import getComponentFromType from '../../Functions/getComponentFromType.js';
11
14
  import Label from '../Form/Label.js';
@@ -29,10 +32,12 @@ export default function Viewer(props) {
29
32
  selectorSelected,
30
33
 
31
34
  // withEditor
35
+ editorType,
32
36
  onEditMode,
33
37
  onClose,
34
38
  onDelete,
35
39
  } = props,
40
+ isSideEditor = editorType === EDITOR_TYPE__SIDE,
36
41
  styles = UiGlobals.styles,
37
42
  flex = props.flex || 1,
38
43
  buildAncillary = () => {
@@ -45,12 +50,18 @@ export default function Viewer(props) {
45
50
  selectorId = null,
46
51
  ...propsToPass
47
52
  } = item;
53
+ if (!propsToPass.h) {
54
+ propsToPass.h = 400;
55
+ }
48
56
  const
49
57
  Element = getComponentFromType(type),
50
58
  element = <Element
51
59
  selectorId={selectorId}
52
60
  selectorSelected={selectorSelected || record}
53
61
  flex={1}
62
+ h={350}
63
+ canEditorViewOnly={true}
64
+ uniqueRepository={true}
54
65
  {...propsToPass}
55
66
  />;
56
67
  if (title) {
@@ -65,6 +76,10 @@ export default function Viewer(props) {
65
76
  return components;
66
77
  };
67
78
 
79
+ const
80
+ showDeleteBtn = onDelete && viewerCanDelete,
81
+ showCloseBtn = !isSideEditor;
82
+
68
83
  return <Column flex={flex} {...props}>
69
84
  <ScrollView width="100%" _web={{ height: 1 }}>
70
85
  <Column m={2}>
@@ -88,26 +103,28 @@ export default function Viewer(props) {
88
103
 
89
104
  </Column>
90
105
  </ScrollView>
91
- <Footer justifyContent="flex-end">
92
- {onDelete && viewerCanDelete &&
93
- <Row flex={1} justifyContent="flex-start">
94
- <Button
95
- key="deleteBtn"
96
- onPress={onDelete}
97
- bg="warning"
98
- _hover={{
99
- bg: 'warningHover',
100
- }}
101
- color="#fff"
102
- >Delete</Button>
103
- </Row>}
104
- <Button.Group space={2}>
105
- <Button
106
- key="closeBtn"
107
- onPress={onClose}
108
- color="#fff"
109
- >Close</Button>
110
- </Button.Group>
111
- </Footer>
106
+ {(showDeleteBtn || showCloseBtn) &&
107
+ <Footer justifyContent="flex-end">
108
+ {showDeleteBtn &&
109
+ <Row flex={1} justifyContent="flex-start">
110
+ <Button
111
+ key="deleteBtn"
112
+ onPress={onDelete}
113
+ bg="warning"
114
+ _hover={{
115
+ bg: 'warningHover',
116
+ }}
117
+ color="#fff"
118
+ >Delete</Button>
119
+ </Row>}
120
+ {showCloseBtn &&
121
+ <Button.Group space={2}>
122
+ <Button
123
+ key="closeBtn"
124
+ onPress={onClose}
125
+ color="#fff"
126
+ >Close</Button>
127
+ </Button.Group>}
128
+ </Footer>}
112
129
  </Column>;
113
130
  }
@@ -34,7 +34,6 @@ export function ComboComponent(props) {
34
34
  tooltip = null,
35
35
  menuMinWidth = 150,
36
36
  disableDirectEntry = false,
37
- disablePagination = true,
38
37
  hideMenuOnSelection = true,
39
38
  _input = {},
40
39
  isEditor = false,
@@ -55,7 +54,7 @@ export function ComboComponent(props) {
55
54
  selectionMode,
56
55
  selectNext,
57
56
  selectPrev,
58
- getDisplayFromSelection,
57
+ getDisplayValuesFromSelection,
59
58
 
60
59
  tooltipPlacement = 'bottom',
61
60
  } = props,
@@ -90,16 +89,12 @@ export function ComboComponent(props) {
90
89
  if (rectTop !== top) {
91
90
  setTop(rectTop);
92
91
  }
93
- setHeight(null);
94
92
  } else {
95
93
  // Menu is above the combo
96
-
97
- const rectTop = rect.top -200;
94
+ const rectTop = rect.top - styles.FORM_COMBO_MENU_HEIGHT;
98
95
  if (rectTop !== top) {
99
96
  setTop(rectTop);
100
97
  }
101
-
102
- setHeight(200);
103
98
  }
104
99
  if (rect.left !== left) {
105
100
  setLeft(rect.left);
@@ -322,7 +317,7 @@ export function ComboComponent(props) {
322
317
  if (found) {
323
318
  const
324
319
  newSelection = [found],
325
- newTextValue = getDisplayFromSelection(newSelection);
320
+ newTextValue = getDisplayValuesFromSelection(newSelection);
326
321
 
327
322
  setTextValue(newTextValue);
328
323
  setSelection(newSelection);
@@ -355,7 +350,7 @@ export function ComboComponent(props) {
355
350
  }
356
351
 
357
352
  // Adjust text input to match selection
358
- let localTextValue = getDisplayFromSelection(selection);
353
+ let localTextValue = getDisplayValuesFromSelection(selection);
359
354
  if (!_.isEqual(localTextValue, textValue)) {
360
355
  setTextValue(localTextValue);
361
356
  }
@@ -489,14 +484,12 @@ export function ComboComponent(props) {
489
484
  top={top + 'px'}
490
485
  left={left + 'px'}
491
486
  w={width + 'px'}
492
- h={height ? height + 'px' : null}
493
487
  minWidth={menuMinWidth}
494
488
  overflow="auto"
495
489
  bg="#fff"
496
490
  >
497
491
  <Popover.Body
498
492
  ref={menuRef}
499
- maxHeight={200}
500
493
  borderWidth={1}
501
494
  borderColor='trueGray.400'
502
495
  borderTopWidth={0}
@@ -505,8 +498,6 @@ export function ComboComponent(props) {
505
498
  <WhichGrid
506
499
  showHeaders={false}
507
500
  showHovers={true}
508
- pageSize={100}
509
- disableAdjustingPageSizeToHeight={true}
510
501
  shadow={1}
511
502
  getRowProps={() => {
512
503
  return {
@@ -518,9 +509,11 @@ export function ComboComponent(props) {
518
509
  w: '100%',
519
510
  };
520
511
  }}
512
+ allowToggleSelection={true}
513
+ disableAdjustingPageSizeToHeight={true}
521
514
  {...props}
515
+ h={styles.FORM_COMBO_MENU_HEIGHT + 'px'}
522
516
  disablePresetButtons={!isEditor}
523
- disablePagination={disablePagination}
524
517
  setSelection={(selection) => {
525
518
  // Decorator fn to add local functionality
526
519
  // Close the menu when row is selected on grid
@@ -1,12 +1,23 @@
1
1
  import {
2
2
  SELECTION_MODE_MULTI,
3
3
  } from '../../../../Constants/Selection.js';
4
- import Combo from './Combo.js';
4
+ import Combo, { ComboEditor } from './Combo.js';
5
5
 
6
- export default function Tag(props) {
7
- return <Combo
8
- selectionMode={SELECTION_MODE_MULTI}
9
- disableDirectEntry={true}
10
- {...props}
11
- />;
6
+ function withAdditionalProps(WrappedComponent) {
7
+ return (props) => {
8
+ return <WrappedComponent
9
+ selectionMode={SELECTION_MODE_MULTI}
10
+ valueIsAlwaysArray={true}
11
+ valueAsIdAndText={true}
12
+ valueAsStringifiedJson={true}
13
+ disableDirectEntry={true}
14
+ pageSize={500}
15
+ {...props}
16
+ />;
17
+ };
12
18
  }
19
+
20
+ const Tag = withAdditionalProps(Combo);
21
+ export const TagEditor = withAdditionalProps(ComboEditor);
22
+
23
+ export default Tag;
@@ -4,7 +4,6 @@ import {
4
4
  } from 'native-base';
5
5
  import UiGlobals from '../../../UiGlobals.js';
6
6
  import withTooltip from '../../Hoc/withTooltip.js';
7
- import withValue from '../../Hoc/withValue.js';
8
7
 
9
8
  const
10
9
  TextElement = (props) => {
@@ -21,7 +20,7 @@ const
21
20
  {...props}
22
21
  >{props.value}</Text>;
23
22
  },
24
- TextField = withValue(TextElement);
23
+ TextField = TextElement; // NOT using withValue on Text element, as this element is simply for display purposes!
25
24
 
26
25
  // Tooltip needs us to forwardRef
27
26
  export default withTooltip(React.forwardRef((props, ref) => {
@@ -82,6 +82,7 @@ function Form(props) {
82
82
 
83
83
  // withEditor
84
84
  isEditorViewOnly = false,
85
+ isSaving = false,
85
86
  editorMode,
86
87
  onCancel,
87
88
  onSave,
@@ -102,6 +103,7 @@ function Form(props) {
102
103
  isMultiple = _.isArray(record),
103
104
  isSingle = !isMultiple, // for convenience
104
105
  forceUpdate = useForceUpdate(),
106
+ [previousRecord, setPreviousRecord] = useState(record),
105
107
  initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
106
108
  defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
107
109
  {
@@ -162,6 +164,7 @@ function Form(props) {
162
164
  renderer,
163
165
  w,
164
166
  flex,
167
+ useSelectorId = false,
165
168
  } = config;
166
169
 
167
170
  if (!isEditable) {
@@ -213,6 +216,11 @@ function Form(props) {
213
216
  }
214
217
  const Element = getComponentFromType(editor);
215
218
 
219
+ if (useSelectorId) {
220
+ editorProps.selectorId = selectorId;
221
+ editorProps.selectorSelected = editorProps;
222
+ }
223
+
216
224
  let element = <Element
217
225
  name={name}
218
226
  value={value}
@@ -223,8 +231,6 @@ function Form(props) {
223
231
  }
224
232
  }}
225
233
  onBlur={onBlur}
226
- selectorId={selectorId}
227
- selectorSelected={selectorSelected}
228
234
  flex={1}
229
235
  {...editorProps}
230
236
  // {...defaults}
@@ -261,6 +267,7 @@ function Form(props) {
261
267
  label,
262
268
  items,
263
269
  onChange: onEditorChange,
270
+ useSelectorId = false,
264
271
  ...propsToPass
265
272
  } = item;
266
273
  let editorTypeProps = {};
@@ -365,6 +372,11 @@ function Form(props) {
365
372
  if (isValidElement(Element)) {
366
373
  throw new Error('Should not yet be valid React element. Did you use <Element> instead of () => <Element> when defining it?')
367
374
  }
375
+
376
+ if (useSelectorId) {
377
+ editorTypeProps.selectorId = selectorId;
378
+ editorTypeProps.selectorSelected = editorProps;
379
+ }
368
380
  let element = <Element
369
381
  name={name}
370
382
  value={value}
@@ -375,8 +387,6 @@ function Form(props) {
375
387
  }
376
388
  }}
377
389
  onBlur={onBlur}
378
- selectorId={selectorId}
379
- selectorSelected={selectorSelected}
380
390
  flex={1}
381
391
  {...defaults}
382
392
  {...propsToPass}
@@ -420,12 +430,16 @@ function Form(props) {
420
430
  selectorId,
421
431
  ...propsToPass
422
432
  } = item;
433
+ if (!propsToPass.h) {
434
+ propsToPass.h = 400;
435
+ }
423
436
  const
424
437
  Element = getComponentFromType(type),
425
438
  element = <Element
426
439
  selectorId={selectorId}
427
440
  selectorSelected={selectorSelected || record}
428
441
  flex={1}
442
+ uniqueRepository={true}
429
443
  {...propsToPass}
430
444
  />;
431
445
  if (title) {
@@ -444,8 +458,23 @@ function Form(props) {
444
458
  if (editorType === EDITOR_TYPE__INLINE) {
445
459
  alert(errors.message);
446
460
  }
461
+ },
462
+ onSaveDecorated = async (data, e) => {
463
+ // reset the form after a save
464
+ const result = await onSave(data, e);
465
+ if (result) {
466
+ const values = record.submitValues;
467
+ reset(values);
468
+ }
447
469
  };
448
470
 
471
+ useEffect(() => {
472
+ if (record !== previousRecord) {
473
+ setPreviousRecord(record);
474
+ reset(defaultValues);
475
+ }
476
+ }, [record]);
477
+
449
478
  useEffect(() => {
450
479
  if (!Repository) {
451
480
  return () => {};
@@ -487,6 +516,13 @@ function Form(props) {
487
516
  sizeProps.maxHeight = maxHeight;
488
517
  }
489
518
 
519
+ const savingProps = {};
520
+ if (isSaving) {
521
+ savingProps.borderTopWidth = 2;
522
+ savingProps.borderTopColor = '#f00';
523
+ }
524
+
525
+
490
526
  let formComponents,
491
527
  editor;
492
528
  if (editorType === EDITOR_TYPE__INLINE) {
@@ -570,7 +606,7 @@ function Form(props) {
570
606
 
571
607
  {editor}
572
608
 
573
- <Footer justifyContent="flex-end" {...footerProps}>
609
+ <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
574
610
  {onDelete && editorMode === EDITOR_MODE__EDIT &&
575
611
  <Row flex={1} justifyContent="flex-start">
576
612
  <Button
@@ -603,11 +639,11 @@ function Form(props) {
603
639
  >Cancel</Button>}
604
640
  {!isEditorViewOnly && onSave && <Button
605
641
  key="saveBtn"
606
- onPress={(e) => handleSubmit(onSave, onSubmitError)(e)}
642
+ onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
607
643
  isDisabled={isSaveDisabled}
608
644
  color="#fff"
609
645
  >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
610
- {isEditorViewOnly && onClose && <Button
646
+ {isEditorViewOnly && onClose && editorType !== EDITOR_TYPE__SIDE && <Button
611
647
  key="closeBtn"
612
648
  onPress={onClose}
613
649
  color="#fff"
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef, useMemo, } from 'react';
1
+ import React, { useState, useEffect, useRef, useMemo, useCallback, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  FlatList,
@@ -86,7 +86,7 @@ function GridComponent(props) {
86
86
  canColumnsReorder = true,
87
87
  canColumnsResize = true,
88
88
  canRowsReorder = false,
89
- allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the item clicked on
89
+ allowToggleSelection = false, // i.e. single click with no shift key toggles the selection of the item clicked on
90
90
  disableBottomToolbar = false,
91
91
  disablePagination = false,
92
92
  bottomToolbar = 'pagination',
@@ -104,6 +104,7 @@ function GridComponent(props) {
104
104
  onDuplicate,
105
105
  onReset,
106
106
  onContextMenu,
107
+ isAdding,
107
108
 
108
109
  // withData
109
110
  Repository,
@@ -141,6 +142,7 @@ function GridComponent(props) {
141
142
  styles = UiGlobals.styles,
142
143
  forceUpdate = useForceUpdate(),
143
144
  gridRef = useRef(),
145
+ isAddingRef = useRef(),
144
146
  [isReady, setIsReady] = useState(false),
145
147
  [isLoading, setIsLoading] = useState(false),
146
148
  [localColumnsConfig, setLocalColumnsConfigRaw] = useState([]),
@@ -162,6 +164,10 @@ function GridComponent(props) {
162
164
  shiftKey,
163
165
  metaKey,
164
166
  } = e;
167
+ let allowToggle = allowToggleSelection;
168
+ if (metaKey) {
169
+ allowToggle = true;
170
+ }
165
171
 
166
172
  if (selectionMode === SELECTION_MODE_MULTI) {
167
173
  if (shiftKey) {
@@ -170,28 +176,18 @@ function GridComponent(props) {
170
176
  } else {
171
177
  selectRangeTo(item);
172
178
  }
173
- } else if (metaKey) {
174
- if (isInSelection(item)) {
175
- // Already selected
176
- if (allowToggleSelection) {
177
- removeFromSelection(item);
178
- } else {
179
- // Do nothing.
180
- }
181
- } else {
182
- addToSelection(item);
183
- }
184
179
  } else {
185
- if (isInSelection(item)) {
186
- // Already selected
187
- if (allowToggleSelection) {
180
+ if (allowToggle) {
181
+ if (isInSelection(item)) {
188
182
  removeFromSelection(item);
189
183
  } else {
190
- // Do nothing.
184
+ addToSelection(item);
191
185
  }
192
186
  } else {
193
- // select just this one
194
- setSelection([item]);
187
+ if (!isInSelection(item)) {
188
+ // select just this one
189
+ setSelection([item]);
190
+ }
195
191
  }
196
192
  }
197
193
  } else {
@@ -199,7 +195,7 @@ function GridComponent(props) {
199
195
  let newSelection = selection;
200
196
  if (isInSelection(item)) {
201
197
  // Already selected
202
- if (allowToggleSelection) {
198
+ if (allowToggle) {
203
199
  // Create empty selection
204
200
  newSelection = [];
205
201
  } else {
@@ -616,41 +612,34 @@ function GridComponent(props) {
616
612
  setDragRowSlot(null);
617
613
  },
618
614
  onLayout = (e) => {
619
- if (disableAdjustingPageSizeToHeight || !Repository) {
615
+ if (disableAdjustingPageSizeToHeight || !Repository || CURRENT_MODE !== UI_MODE_WEB || !gridRef.current || isAddingRef.current) {
620
616
  return;
621
617
  }
622
- const {
623
- nativeEvent: {
624
- layout,
625
- target,
626
- },
627
- } = e;
628
- let pageSize;
629
- if (CURRENT_MODE === UI_MODE_WEB) {
630
- const
631
- targetBoundingBox = target.getBoundingClientRect(),
632
- targetHeight = targetBoundingBox.height,
633
- firstRow = target.children[0]?.children[0]?.children[0]?.children[0]?.children[0];
634
- if (firstRow) {
635
- const
636
- rowBoundingBox = firstRow.getBoundingClientRect(),
637
- rowHeight = rowBoundingBox.height,
638
- rowsPerTarget = Math.floor(targetHeight / rowHeight);
639
- pageSize = rowsPerTarget;
640
- if (showHeaders) {
641
- pageSize--;
642
- }
643
- if (bottomToolbar) {
644
- pageSize--;
645
- }
646
- }
618
+
619
+ const
620
+ gr = gridRef.current,
621
+ scrollableNode = gr.getScrollableNode(),
622
+ scrollableNodeBoundingBox = scrollableNode.getBoundingClientRect(),
623
+ scrollableNodeHeight = scrollableNodeBoundingBox.height,
624
+ firstRow = scrollableNode.children[0].children[showHeaders ? 1: 0];
625
+
626
+ if (!firstRow) {
627
+ return;
647
628
  }
648
-
649
- if (pageSize) {
629
+
630
+ const
631
+ rowHeight = firstRow.getBoundingClientRect().height,
632
+ rowsPerContainer = Math.floor(scrollableNodeHeight / rowHeight);
633
+ let pageSize = rowsPerContainer;
634
+ if (showHeaders) {
635
+ pageSize--;
636
+ }
637
+ if (pageSize !== Repository.pageSize) {
650
638
  Repository.setPageSize(pageSize);
651
639
  }
652
- };
653
-
640
+ },
641
+ debouncedOnLayout = useCallback(_.debounce(onLayout, 500), []);
642
+
654
643
  useEffect(() => {
655
644
 
656
645
  const calculateLocalColumnsConfig = () => {
@@ -778,6 +767,9 @@ function GridComponent(props) {
778
767
 
779
768
  }, [selectorId, selectorSelected]);
780
769
 
770
+
771
+ isAddingRef.current = isAdding;
772
+
781
773
  const footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isDragMode]);
782
774
 
783
775
  if (!isReady) {
@@ -811,23 +803,24 @@ function GridComponent(props) {
811
803
  } else {
812
804
  sizeProps.flex = flex ?? 1;
813
805
  }
806
+
814
807
  return <Column
815
808
  {...testProps('Grid')}
816
809
  w="100%"
817
810
  bg={bg}
818
811
  borderWidth={styles.GRID_BORDER_WIDTH}
819
812
  borderColor={styles.GRID_BORDER_COLOR}
820
- onLayout={onLayout}
813
+ onLayout={debouncedOnLayout}
821
814
  {...sizeProps}
822
815
  >
823
816
  {topToolbar}
824
817
 
825
- <Column w="100%" flex={1} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
818
+ <Column w="100%" flex={1} minHeight={40} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
826
819
  if (!isDragMode && !isInlineEditorShown) {
827
820
  deselectAll();
828
821
  }
829
822
  }}>
830
- {!entities.length ? <NoRecordsFound text={noneFoundText} onRefresh={onRefresh} /> :
823
+ {!entities?.length ? <NoRecordsFound text={noneFoundText} onRefresh={onRefresh} /> :
831
824
  <FlatList
832
825
  ref={gridRef}
833
826
  // ListHeaderComponent={listHeaderComponent}
@@ -285,7 +285,7 @@ export default function GridHeaderRow(props) {
285
285
 
286
286
  // These header Components should match the columns exactly
287
287
  // so we can drag/drop them to control the columns.
288
- const headerColumns = _.map(localColumnsConfig, (config, ix) => {
288
+ const headerColumns = _.map(localColumnsConfig, (config, ix, all) => {
289
289
  let {
290
290
  columnId,
291
291
  fieldName,
@@ -305,15 +305,21 @@ export default function GridHeaderRow(props) {
305
305
  borderRightColor: '#fff',
306
306
  }
307
307
 
308
- if (w) {
309
- propsToPass.w = w;
310
- } else if (flex) {
311
- propsToPass.flex = flex;
312
- propsToPass.minWidth = 100;
313
- } else if (localColumnsConfig.length === 1) {
314
- // Only one column and flex is not set
315
- propsToPass.flex = 1;
316
- if (!header) {
308
+ if (all.length === 1) {
309
+ propsToPass.w = '100%';
310
+ isReorderable = false;
311
+ isResizable = false;
312
+ } else {
313
+ if (w) {
314
+ propsToPass.w = w;
315
+ } else if (flex) {
316
+ propsToPass.flex = flex;
317
+ propsToPass.minWidth = 100;
318
+ } else if (localColumnsConfig.length === 1) {
319
+ // Only one column and flex is not set
320
+ propsToPass.flex = 1;
321
+ if (!header) {
322
+ }
317
323
  }
318
324
  }
319
325
 
@@ -32,15 +32,19 @@ export default function GridRow(props) {
32
32
  return useMemo(() => {
33
33
  const renderColumns = (item) => {
34
34
  if (_.isArray(columnsConfig)) {
35
- return _.map(columnsConfig, (config, key) => {
35
+ return _.map(columnsConfig, (config, key, all) => {
36
36
  const propsToPass = columnProps[key] || {};
37
- if (config.w) {
38
- propsToPass.w = config.w;
39
- } else if (config.flex) {
40
- propsToPass.flex = config.flex;
41
- propsToPass.minWidth = 100;
37
+ if (all.length === 1) {
38
+ propsToPass.w = '100%';
42
39
  } else {
43
- propsToPass.flex = 1;
40
+ if (config.w) {
41
+ propsToPass.w = config.w;
42
+ } else if (config.flex) {
43
+ propsToPass.flex = config.flex;
44
+ propsToPass.minWidth = 100;
45
+ } else {
46
+ propsToPass.flex = 1;
47
+ }
44
48
  }
45
49
  propsToPass.p = 1;
46
50
  propsToPass.justifyContent = 'center';
@@ -16,7 +16,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
16
16
  const {
17
17
  userCanEdit = true,
18
18
  userCanView = true,
19
- canEditorViewOnly = false,
19
+ canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
20
20
  disableAdd = false,
21
21
  disableEdit = false,
22
22
  disableDelete = false,
@@ -49,8 +49,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
49
49
  listeners = useRef({}),
50
50
  editorStateRef = useRef(),
51
51
  [currentRecord, setCurrentRecord] = useState(null),
52
+ [isAdding, setIsAdding] = useState(false),
53
+ [isSaving, setIsSaving] = useState(false),
52
54
  [isEditorShown, setIsEditorShown] = useState(false),
53
- [isEditorViewOnly, setIsEditorViewOnly] = useState(canEditorViewOnly),
55
+ [isEditorViewOnly, setIsEditorViewOnly] = useState(canEditorViewOnly), // current state of whether editor is in view-only mode
54
56
  [lastSelection, setLastSelection] = useState(),
55
57
  setSelectionDecorated = (newSelection) => {
56
58
  function doIt() {
@@ -71,7 +73,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
71
73
  // forceUpdate(); // we don't want to get into an infinite loop of renders. Simply directly assign the listeners in every child render
72
74
  },
73
75
  onAdd = async () => {
74
- const defaultValues = Repository.getSchema().model.defaultValues;
76
+ const defaultValues = Repository.getSchema().getDefaultValues();
75
77
  let addValues = _.clone(defaultValues);
76
78
 
77
79
  if (selectorId && !_.isEmpty(selectorSelected)) {
@@ -105,7 +107,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
105
107
  // Unmap the values, so we can input true originalData
106
108
  addValues = Repository.unmapData(addValues);
107
109
 
110
+ setIsAdding(true);
111
+ setIsSaving(true);
108
112
  const entity = await Repository.add(addValues, false, true);
113
+ setIsSaving(false);
109
114
  setSelection([entity]);
110
115
  setIsEditorViewOnly(false);
111
116
  setEditorMode(EDITOR_MODE__ADD);
@@ -183,6 +188,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
183
188
  if (getListeners().onAfterDelete) {
184
189
  await getListeners().onAfterDelete(selection);
185
190
  }
191
+ setSelection([]);
186
192
  if (cb) {
187
193
  cb();
188
194
  }
@@ -244,12 +250,19 @@ export default function withEditor(WrappedComponent, isTree = false) {
244
250
  await getListeners().onBeforeEditSave(what);
245
251
  }
246
252
 
253
+ setIsSaving(true);
247
254
  await Repository.save();
248
- setIsEditorShown(false);
255
+ setIsSaving(false);
256
+
257
+ setIsAdding(false);
258
+ setEditorMode(EDITOR_MODE__EDIT);
259
+ // setIsEditorShown(false);
249
260
 
250
261
  if (getListeners().onAfterEdit) {
251
262
  await getListeners().onAfterEdit(what);
252
263
  }
264
+
265
+ return true;
253
266
  },
254
267
  onEditorCancel = () => {
255
268
  async function doIt() {
@@ -259,6 +272,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
259
272
  if (isSingle && isPhantom) {
260
273
  await deleteRecord();
261
274
  }
275
+
276
+ setIsAdding(false);
262
277
  setEditorMode(EDITOR_MODE__VIEW);
263
278
  setIsEditorShown(false);
264
279
  }
@@ -330,6 +345,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
330
345
  setCurrentRecord={setCurrentRecord}
331
346
  isEditorShown={isEditorShown}
332
347
  isEditorViewOnly={isEditorViewOnly}
348
+ isAdding={isAdding}
349
+ isSaving={isSaving}
333
350
  editorMode={editorMode}
334
351
  onEditMode={onEditMode}
335
352
  onViewMode={onViewMode}
@@ -59,7 +59,7 @@ export default function withFilters(WrappedComponent) {
59
59
  defaultFilters: modelDefaultFilters,
60
60
  ancillaryFilters: modelAncillaryFilters,
61
61
  } = Repository.getSchema().model,
62
- id = useId(),
62
+ id = props.id || useId(),
63
63
 
64
64
  // determine the starting filters
65
65
  startingFilters = !_.isEmpty(customFilters) ? customFilters : // custom filters override component filters
@@ -235,6 +235,9 @@ export default function withFilters(WrappedComponent) {
235
235
  const
236
236
  filterProps = {
237
237
  mx: 1,
238
+ disableAdjustingPageSizeToHeight: true,
239
+ pageSize: 20,
240
+ uniqueRepository: true,
238
241
  },
239
242
  filterElements = [];
240
243
  _.each(filters, (filter, ix) => {
@@ -62,7 +62,6 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
62
62
 
63
63
  // withSelection
64
64
  selection,
65
- setSelection,
66
65
 
67
66
  // parent container
68
67
  selectorId,
@@ -87,7 +86,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
87
86
  }
88
87
  break;
89
88
  case 'edit':
90
- if (disableEdit || canEditorViewOnly) {
89
+ if (disableEdit || canEditorViewOnly || isSideEditor) {
91
90
  isDisabled = true;
92
91
  }
93
92
  break;
@@ -251,7 +250,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
251
250
  }
252
251
 
253
252
  // Send it to clipboard
254
- navigator.clipboard.writeText(text);
253
+ navigator?.clipboard.writeText(text);
255
254
  };
256
255
  // onPrint = () => {
257
256
  // debugger;
@@ -38,9 +38,14 @@ export default function withSelection(WrappedComponent) {
38
38
  displayIx,
39
39
  } = props,
40
40
  usesWithValue = !!setValue,
41
- [localSelection, setLocalSelection] = useState(selection || defaultSelection || []),
41
+ initialSelection = selection || defaultSelection || [],
42
+ [localSelection, setLocalSelection] = useState(initialSelection),
42
43
  [isReady, setIsReady] = useState(selection || false), // if selection is already defined, or value is not null and we don't need to load repository, it's ready
43
44
  setSelection = (selection) => {
45
+ if (_.isEqual(selection, localSelection)) {
46
+ return;
47
+ }
48
+
44
49
  setLocalSelection(selection);
45
50
  if (onChangeSelection) {
46
51
  onChangeSelection(selection);
@@ -175,7 +180,7 @@ export default function withSelection(WrappedComponent) {
175
180
  });
176
181
  return found;
177
182
  },
178
- getIdFromSelection = () => {
183
+ getIdsFromLocalSelection = () => {
179
184
  if (!localSelection[0]) {
180
185
  return null;
181
186
  }
@@ -190,7 +195,7 @@ export default function withSelection(WrappedComponent) {
190
195
  }
191
196
  return values;
192
197
  },
193
- getDisplayFromSelection = (selection) => {
198
+ getDisplayValuesFromLocalSelection = (selection) => {
194
199
  if (!selection[0]) {
195
200
  return '';
196
201
  }
@@ -203,18 +208,17 @@ export default function withSelection(WrappedComponent) {
203
208
  })
204
209
  .join(', ');
205
210
  },
206
- conformValueToSelection = () => {
211
+ conformValueToLocalSelection = () => {
207
212
  if (!setValue) {
208
213
  return;
209
214
  }
210
- // Adjust the value to match the selection
211
- const localValue = getIdFromSelection();
215
+
216
+ const localValue = getIdsFromLocalSelection();
212
217
  if (!_.isEqual(localValue, value)) {
213
218
  setValue(localValue);
214
219
  }
215
220
  },
216
- conformSelectionToValue = () => {
217
- // adjust the selection to match the value
221
+ conformSelectionToValue = async () => {
218
222
  let newSelection = [];
219
223
  if (Repository) {
220
224
  // Get entity or entities that match value
@@ -222,9 +226,20 @@ export default function withSelection(WrappedComponent) {
222
226
  if (_.isArray(value)) {
223
227
  newSelection = Repository.getBy((entity) => inArray(entity.id, value));
224
228
  } else {
225
- const found = Repository.getById(value);
229
+ let found = Repository.getById(value);
226
230
  if (found) {
227
231
  newSelection.push(found);
232
+ } else if (Repository?.isRemote && Repository?.entities.length) {
233
+
234
+ // Value cannot be found in Repository, but actually exists on server
235
+ // Try to get this value from the server directly
236
+ Repository.filter(Repository.schema.model.idProperty, value);
237
+ await Repository.load();
238
+ found = Repository.getById(value);
239
+ if (found) {
240
+ newSelection.push(found);
241
+ }
242
+
228
243
  }
229
244
  }
230
245
  }
@@ -262,7 +277,8 @@ export default function withSelection(WrappedComponent) {
262
277
 
263
278
  (async () => {
264
279
 
265
- if (Repository && usesWithValue && !Repository.isLoaded && Repository.isRemote && !Repository.isAutoLoad && !Repository.isLoading) {
280
+ if (usesWithValue && Repository?.isRemote
281
+ && !Repository.isAutoLoad && !Repository.isLoaded && !Repository.isLoading) {
266
282
  // on initialization, we can't conformSelectionToValue if the repository is not yet loaded,
267
283
  // so first load repo, then conform to value
268
284
  await Repository.load();
@@ -270,7 +286,11 @@ export default function withSelection(WrappedComponent) {
270
286
 
271
287
  if (usesWithValue && !_.isNil(value)) {
272
288
 
273
- conformSelectionToValue();
289
+ await conformSelectionToValue();
290
+
291
+ } else if (!_.isEmpty(selection)) {
292
+
293
+ conformValueToLocalSelection();
274
294
 
275
295
  } else if (autoSelectFirstItem) {
276
296
  let newSelection = [];
@@ -289,7 +309,6 @@ export default function withSelection(WrappedComponent) {
289
309
 
290
310
  }, []);
291
311
 
292
-
293
312
  if (usesWithValue) {
294
313
  useEffect(() => {
295
314
  if (!isReady) {
@@ -298,15 +317,16 @@ export default function withSelection(WrappedComponent) {
298
317
 
299
318
  conformSelectionToValue();
300
319
 
301
- }, [value, isReady]);
320
+ }, [value]);
302
321
 
303
322
  useEffect(() => {
304
323
  if (!isReady) {
305
324
  return () => {};
306
325
  }
307
326
 
308
- conformValueToSelection();
309
- }, [selection, isReady]);
327
+ conformValueToLocalSelection();
328
+
329
+ }, [selection]);
310
330
  }
311
331
 
312
332
  if (!isReady) {
@@ -325,8 +345,8 @@ export default function withSelection(WrappedComponent) {
325
345
  deselectAll={deselectAll}
326
346
  selectRangeTo={selectRangeTo}
327
347
  isInSelection={isInSelection}
328
- getIdFromSelection={getIdFromSelection}
329
- getDisplayFromSelection={getDisplayFromSelection}
348
+ getIdsFromSelection={getIdsFromLocalSelection}
349
+ getDisplayValuesFromSelection={getDisplayValuesFromLocalSelection}
330
350
  />;
331
351
  };
332
352
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, } from 'react';
2
- import isJson from '../../Functions/isJson.js';
2
+ import natsort from 'natsort';
3
3
  import _ from 'lodash';
4
4
 
5
5
  // This HOC gives the component value props, primarily for a Form Field.
@@ -19,6 +19,9 @@ export default function withValue(WrappedComponent) {
19
19
  onChangeValue,
20
20
  value,
21
21
  startingValue = null,
22
+ valueIsAlwaysArray = false,
23
+ valueAsIdAndText = false,
24
+ valueAsStringifiedJson = false,
22
25
 
23
26
  // withData
24
27
  Repository,
@@ -26,6 +29,39 @@ export default function withValue(WrappedComponent) {
26
29
  } = props,
27
30
  [localValue, setLocalValue] = useState(startingValue || value),
28
31
  setValue = (newValue) => {
32
+ if (valueIsAlwaysArray && !_.isArray(newValue)) {
33
+ newValue = _.isNil(newValue) ? [] : [newValue];
34
+ }
35
+ if (_.isArray(newValue)) {
36
+ // TODO: sort by the sortProperty, whatever that is, instead of just value
37
+ newValue.sort(natsort()); // Only sort if we're using id/text arrangement. Otherwise, keep sort order as specified in Repository.
38
+ }
39
+ if (valueAsIdAndText) {
40
+ if (_.isArray(newValue)) {
41
+ newValue = _.map(newValue, (id) => {
42
+ if (_.isNil(id)) {
43
+ return id;
44
+ }
45
+ const record = Repository.getById(id);
46
+ return {
47
+ id: record.getId(),
48
+ text: record.getDisplayValue(),
49
+ };
50
+ })
51
+ } else {
52
+ if (!_.isNil(id)) {
53
+ const record = Repository.getById(newValue);
54
+ newValue = {
55
+ id: record.getId(),
56
+ text: record.getDisplayValue(),
57
+ };
58
+ }
59
+ }
60
+ }
61
+ if (valueAsStringifiedJson) {
62
+ newValue = JSON.stringify(newValue);
63
+ }
64
+
29
65
  if (newValue === localValue) {
30
66
  return;
31
67
  }
@@ -33,10 +69,6 @@ export default function withValue(WrappedComponent) {
33
69
  setLocalValue(newValue);
34
70
 
35
71
  if (onChangeValue) {
36
- if (_.isArray(newValue)) {
37
- // convert from inner value to outer value
38
- newValue = JSON.stringify(newValue);
39
- }
40
72
  onChangeValue(newValue);
41
73
  }
42
74
  },
@@ -71,10 +103,26 @@ export default function withValue(WrappedComponent) {
71
103
  }, [value]);
72
104
 
73
105
 
106
+ // Convert localValue to normal JS primitives for field components
74
107
  let convertedValue = localValue;
75
- if (_.isString(localValue) && isJson(localValue) && !_.isNil(localValue)) {
76
- // convert from outer value to inner value
77
- convertedValue = JSON.parse(localValue);
108
+ if (_.isString(convertedValue) && valueAsStringifiedJson && !_.isNil(convertedValue)) {
109
+ convertedValue = JSON.parse(convertedValue);
110
+ }
111
+ if (valueIsAlwaysArray) {
112
+ if (_.isEmpty(convertedValue) || _.isNil(convertedValue)) {
113
+ convertedValue = null;
114
+ } else if (convertedValue.length === 1) {
115
+ convertedValue = convertedValue[0];
116
+ }
117
+ }
118
+ if (valueAsIdAndText && !_.isNil(convertedValue)) {
119
+ if (_.isArray(convertedValue)) {
120
+ convertedValue = _.map(convertedValue, (value) => {
121
+ return value?.id;
122
+ });
123
+ } else {
124
+ convertedValue = convertedValue?.id;
125
+ }
78
126
  }
79
127
 
80
128
  return <WrappedComponent
@@ -9,10 +9,12 @@ import {
9
9
  HORIZONTAL,
10
10
  VERTICAL,
11
11
  } from '../../Constants/Directions.js';
12
+ import Inflector from 'inflector-js';
12
13
  import Header from './Header.js';
13
14
  import Mask from './Mask.js';
14
15
  import withCollapsible from '../Hoc/withCollapsible.js';
15
16
  import emptyFn from '../../Functions/emptyFn.js';
17
+ import UiGlobals from '../../UiGlobals.js';
16
18
  import _ from 'lodash';
17
19
 
18
20
  // Note on collapseDirections:
@@ -33,7 +35,7 @@ function Panel(props) {
33
35
  onLayout = null,
34
36
 
35
37
  // Header
36
- title = props.model,
38
+ title = UiGlobals.customInflect(Inflector.camel2words(Inflector.underscore(props.model))),
37
39
  showHeader = true,
38
40
  header = null,
39
41
  isClosable = false,
@@ -60,7 +60,7 @@ export default function ManagerScreen(props) {
60
60
  whichComponent = sideModeComponent;
61
61
  }
62
62
 
63
- return <Column flex={1} w="100%">
63
+ return <Column maxHeight="100vh" overflow="hidden" flex={1} w="100%">
64
64
  <Row
65
65
  h="80px"
66
66
  py={2}
@@ -89,6 +89,8 @@ export default function ManagerScreen(props) {
89
89
  tooltip="Side Editor"
90
90
  />
91
91
  </Row>
92
+
92
93
  {whichComponent}
94
+
93
95
  </Column>;
94
96
  }
@@ -16,6 +16,7 @@ const defaults = {
16
16
  FORM_COMBO_INPUT_FONTSIZE: DEFAULT_FONTSIZE,
17
17
  FORM_COMBO_INPUT_BG: WHITE,
18
18
  FORM_COMBO_INPUT_FOCUS_BG: FOCUS,
19
+ FORM_COMBO_MENU_HEIGHT: 250,
19
20
  FORM_COMBO_TRIGGER_BG: WHITE,
20
21
  FORM_COMBO_TRIGGER_HOVER_BG: 'trueGray.300',
21
22
  FORM_DATE_ICON_BG: 'primary.200',
@@ -0,0 +1,6 @@
1
+ import UiGlobals from '../UiGlobals.js';
2
+ import _ from 'lodash';
3
+
4
+ export default function setCustomInflector(customInflector) {
5
+ UiGlobals.customInflect = customInflector;
6
+ }
@@ -49,7 +49,7 @@ function AttachmentsElement(props) {
49
49
 
50
50
  } = props,
51
51
  styles = UiGlobals.styles,
52
- model = selectorSelected?.repository.name,
52
+ model = selectorSelected?.repository?.name,
53
53
  modelid = selectorSelected?.id,
54
54
  [isReady, setIsReady] = useState(false),
55
55
  [isUploading, setIsUploading] = useState(false),
package/src/UiGlobals.js CHANGED
@@ -4,6 +4,7 @@ import _ from 'lodash';
4
4
 
5
5
  const Globals = {
6
6
  mode: CURRENT_MODE,
7
+ customInflect: (str) => str,
7
8
  };
8
9
 
9
10
  export default Globals;
@@ -1,11 +0,0 @@
1
- import {
2
- SELECTION_MODE_MULTI,
3
- } from '../../../Constants/Selection.js';
4
- import Combo from './Combo/Combo.js';
5
-
6
- export default function Tag(props) {
7
- return <Combo
8
- selectionMode={SELECTION_MODE_MULTI}
9
- {...props}
10
- />;
11
- }