@onehat/ui 0.3.57 → 0.3.59

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.
@@ -1,9 +1,8 @@
1
- import { useState, } from 'react';
1
+ import { useState, useRef, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  Modal,
5
5
  Row,
6
- Text,
7
6
  } from 'native-base';
8
7
  import {
9
8
  EDITOR_TYPE__WINDOWED,
@@ -12,50 +11,11 @@ import withAlert from '../../../Hoc/withAlert.js';
12
11
  import withComponent from '../../../Hoc/withComponent.js';
13
12
  import withData from '../../../Hoc/withData.js';
14
13
  import withValue from '../../../Hoc/withValue.js';
15
- import IconButton from '../../../Buttons/IconButton.js';
16
- import Eye from '../../../Icons/Eye.js';
17
- import Xmark from '../../../Icons/Xmark.js';
14
+ import ValueBox from './ValueBox.js';
18
15
  import Combo, { ComboEditor } from '../Combo/Combo.js';
19
16
  import _ from 'lodash';
20
17
 
21
18
 
22
- function ValueBox(props) {
23
- const {
24
- text,
25
- onView,
26
- onDelete,
27
- } = props;
28
-
29
- return <Row
30
- borderWidth={1}
31
- borderColor="trueGray.400"
32
- borderRadius="md"
33
- mr={1}
34
- bg="trueGray.200"
35
- alignItems="center"
36
- >
37
- <IconButton
38
- _icon={{
39
- as: Eye,
40
- color: 'trueGray.600',
41
- size: 'sm',
42
- }}
43
- onPress={onView}
44
- h="100%"
45
- />
46
- <Text color="trueGray.600">{text}</Text>
47
- {onDelete &&
48
- <IconButton
49
- _icon={{
50
- as: Xmark,
51
- color: 'trueGray.600',
52
- size: 'sm',
53
- }}
54
- onPress={onDelete}
55
- h="100%"
56
- />}
57
- </Row>;
58
- }
59
19
 
60
20
  function TagComponent(props) {
61
21
 
@@ -68,6 +28,9 @@ function TagComponent(props) {
68
28
  // parent Form
69
29
  onChangeValue,
70
30
 
31
+ // withAlert
32
+ alert,
33
+
71
34
  // withComponent
72
35
  self,
73
36
 
@@ -76,16 +39,29 @@ function TagComponent(props) {
76
39
  setValue,
77
40
  ...propsToPass // break connection between Tag and Combo props
78
41
  } = props,
42
+ ignoreNextComboValueChangeRef = useRef(false),
79
43
  [isViewerShown, setIsViewerShown] = useState(false),
80
44
  [viewerSelection, setViewerSelection] = useState(false),
45
+ getIgnoreNextComboValueChange = () => {
46
+ return ignoreNextComboValueChangeRef.current;
47
+ },
48
+ setIgnoreNextComboValueChange = (bool) => {
49
+ ignoreNextComboValueChangeRef.current = bool;
50
+ },
81
51
  onViewerClose = () => setIsViewerShown(false),
82
52
  onView = async (item, e) => {
83
53
  const
84
54
  id = item.id,
85
55
  repository = propsToPass.Repository;
56
+ if (!repository.isLoaded) {
57
+ await repository.load();
58
+ }
59
+ if (repository.isLoading) {
60
+ await repository.waitUntilDoneLoading();
61
+ }
86
62
  let record = repository.getById(id); // first try to get from entities in memory
87
63
  if (!record && repository.getSingleEntityFromServer) {
88
- record = await repository.getSingleEntityFromServer(id); // TODO: Build this
64
+ record = await repository.getSingleEntityFromServer(id);
89
65
  }
90
66
 
91
67
  if (!record) {
@@ -96,27 +72,63 @@ function TagComponent(props) {
96
72
  setViewerSelection([record]);
97
73
  setIsViewerShown(true);
98
74
  },
99
- onAdd = (item, e) => {
75
+ clearComboValue = () => {
76
+ setIgnoreNextComboValueChange(true); // we're clearing out the value of the underlying Combo, so ignore it when this combo submits the new value change
77
+ self.children.combo.clear();
78
+ },
79
+ onChangeComboValue = (comboValue) => {
80
+ if (getIgnoreNextComboValueChange()) {
81
+ setIgnoreNextComboValueChange(false);
82
+ return;
83
+ }
84
+
100
85
  // make sure value doesn't already exist
101
86
  let exists = false;
102
87
  _.each(value, (val) => {
103
- if (val.id === item.getId()) {
88
+ if (val.id === comboValue) {
104
89
  exists = true;
105
90
  return false; // break
106
91
  }
107
92
  });
108
93
  if (exists) {
94
+ clearComboValue();
109
95
  alert('Value already exists!');
110
96
  return;
111
97
  }
112
98
 
99
+ // The value we get from combo is a simple int
100
+ // Convert this to id and displayValue from either Repository or data array.
101
+ const
102
+ Repository = props.Repository,
103
+ data = props.data,
104
+ idIx = props.idIx,
105
+ displayIx = props.displayIx,
106
+ id = comboValue;
107
+ let item,
108
+ displayValue;
109
+ if (Repository) {
110
+ item = Repository.getById(id);
111
+ if (!item) {
112
+ throw Error('item not found');
113
+ }
114
+ displayValue = item.displayValue;
115
+ } else {
116
+ item = _.find(data, (datum) => datum[idIx] === id);
117
+ if (!item) {
118
+ throw Error('item not found');
119
+ }
120
+ displayValue = item[displayIx];
121
+ }
122
+
123
+
113
124
  // add new value
114
125
  const newValue = _.clone(value); // so we trigger a re-render
115
126
  newValue.push({
116
- id: item.getId(),
117
- text: item.getDisplayValue(),
127
+ id,
128
+ text: displayValue,
118
129
  })
119
130
  setValue(newValue);
131
+ clearComboValue();
120
132
  },
121
133
  onDelete = (val) => {
122
134
  // Remove from value array
@@ -135,24 +147,44 @@ function TagComponent(props) {
135
147
  }),
136
148
  WhichCombo = isEditor ? ComboEditor : Combo;
137
149
 
150
+ const sizeProps = {};
151
+ if (!props.flex && !props.w) {
152
+ sizeProps.flex = 1;
153
+ } else {
154
+ if (props.w) {
155
+ sizeProps.w = props.w;
156
+ }
157
+ if (props.flex) {
158
+ sizeProps.flex = props.flex;
159
+ }
160
+ }
161
+
138
162
  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
- />
163
+ <Column
164
+ {...props}
165
+ {...sizeProps}
166
+ px={0}
167
+ py={0}
168
+ >
169
+ <Row
170
+ w="100%"
171
+ borderWidth={1}
172
+ borderColor="trueGray.300"
173
+ borderRadius="md"
174
+ bg="trueGray.100"
175
+ p={1}
176
+ mb={1}
177
+ minHeight={10}
178
+ flexWrap="wrap"
179
+ >{valueBoxes}</Row>
180
+ {isEditor &&
181
+ <WhichCombo
182
+ Repository={props.Repository}
183
+ Editor={props.Editor}
184
+ onChangeValue={onChangeComboValue}
185
+ parent={self}
186
+ reference="combo"
187
+ />}
156
188
  </Column>
157
189
  {isViewerShown &&
158
190
  <Modal
@@ -162,6 +194,9 @@ function TagComponent(props) {
162
194
  <Editor
163
195
  editorType={EDITOR_TYPE__WINDOWED}
164
196
  {...propsToPass}
197
+ px={0}
198
+ py={0}
199
+ w="100%"
165
200
  parent={self}
166
201
  reference="viewer"
167
202
 
@@ -0,0 +1,45 @@
1
+ import {
2
+ Row,
3
+ Text,
4
+ } from 'native-base';
5
+ import IconButton from '../../../Buttons/IconButton.js';
6
+ import Eye from '../../../Icons/Eye.js';
7
+ import Xmark from '../../../Icons/Xmark.js';
8
+ import _ from 'lodash';
9
+
10
+ export default function ValueBox(props) {
11
+ const {
12
+ text,
13
+ onView,
14
+ onDelete,
15
+ } = props;
16
+ return <Row
17
+ borderWidth={1}
18
+ borderColor="trueGray.400"
19
+ borderRadius="md"
20
+ mr={1}
21
+ bg="trueGray.200"
22
+ alignItems="center"
23
+ >
24
+ <IconButton
25
+ _icon={{
26
+ as: Eye,
27
+ color: 'trueGray.600',
28
+ size: 'sm',
29
+ }}
30
+ onPress={onView}
31
+ h="100%"
32
+ />
33
+ <Text color="trueGray.600" mr={onDelete ? 0 : 2}>{text}</Text>
34
+ {onDelete &&
35
+ <IconButton
36
+ _icon={{
37
+ as: Xmark,
38
+ color: 'trueGray.600',
39
+ size: 'sm',
40
+ }}
41
+ onPress={onDelete}
42
+ h="100%"
43
+ />}
44
+ </Row>;
45
+ }
@@ -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>;
@@ -322,13 +322,17 @@ function Form(props) {
322
322
  type = 'Text';
323
323
  }
324
324
  }
325
+ const isCombo = type?.match && type.match(/Combo/);
325
326
  if (item.hasOwnProperty('autoLoad')) {
326
327
  editorTypeProps.autoLoad = item.autoLoad;
327
328
  } else {
328
- if (type?.match && type.match(/Combo$/) && Repository?.isRemote && !Repository?.isLoaded) {
329
+ if (isCombo && Repository?.isRemote && !Repository?.isLoaded) {
329
330
  editorTypeProps.autoLoad = true;
330
331
  }
331
332
  }
333
+ if (isCombo) {
334
+ editorTypeProps.showXButton = true;
335
+ }
332
336
  const Element = getComponentFromType(type);
333
337
  let children;
334
338
 
@@ -439,6 +443,9 @@ function Form(props) {
439
443
  name={name}
440
444
  value={value}
441
445
  onChangeValue={(newValue) => {
446
+ if (newValue === undefined) {
447
+ newValue = null; // React Hook Form doesn't respond well when setting value to undefined
448
+ }
442
449
  onChange(newValue);
443
450
  if (onEditorChange) {
444
451
  onEditorChange(newValue, formSetValue, formGetValues, formState);
@@ -452,21 +459,21 @@ function Form(props) {
452
459
  {...propsToPass}
453
460
  {...editorTypeProps}
454
461
  />;
455
- if (error) {
456
- if (editorType !== EDITOR_TYPE__INLINE) {
457
- let message = error.message;
462
+ if (editorType !== EDITOR_TYPE__INLINE) {
463
+ let message = null;
464
+ if (error) {
465
+ message = error.message;
458
466
  if (label) {
459
467
  message = message.replace(error.ref.name, label);
460
468
  }
461
- element = <Column pt={1} flex={1}>
462
- {element}
463
- <Text color="#f00">{message}</Text>
464
- </Column>;
465
- } else {
466
- debugger;
467
-
468
-
469
469
  }
470
+ if (message) {
471
+ message = <Text color="#f00">{message}</Text>;
472
+ }
473
+ element = <Column pt={1} flex={1}>
474
+ {element}
475
+ {message}
476
+ </Column>;
470
477
  }
471
478
 
472
479
  if (item.additionalEditButtons) {
@@ -573,17 +580,24 @@ function Form(props) {
573
580
 
574
581
  useEffect(() => {
575
582
  if (!Repository) {
576
- return () => {};
583
+ return () => {
584
+ if (!_.isNil(editorStateRef)) {
585
+ editorStateRef.current = null; // clean up the editorStateRef on unmount
586
+ }
587
+ };
577
588
  }
578
589
 
579
590
  Repository.ons(['changeData', 'change'], forceUpdate);
580
591
 
581
592
  return () => {
582
593
  Repository.offs(['changeData', 'change'], forceUpdate);
594
+ if (!_.isNil(editorStateRef)) {
595
+ editorStateRef.current = null; // clean up the editorStateRef on unmount
596
+ }
583
597
  };
584
598
  }, [Repository]);
585
599
 
586
- // if (Repository && (!record || _.isEmpty(record))) {
600
+ // if (Repository && (!record || _.isEmpty(record) || record.isDestroyed)) {
587
601
  // return null;
588
602
  // }
589
603
 
@@ -622,7 +636,14 @@ function Form(props) {
622
636
  additionalButtons,
623
637
  isSaveDisabled = false,
624
638
  isSubmitDisabled = false,
625
- savingProps = {};
639
+ savingProps = {},
640
+
641
+ showDeleteBtn = false,
642
+ showResetBtn = false,
643
+ showCloseBtn = false,
644
+ showCancelBtn = false,
645
+ showSaveBtn = false,
646
+ showSubmitBtn = false;
626
647
 
627
648
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
628
649
 
@@ -678,7 +699,7 @@ function Form(props) {
678
699
  isSaveDisabled = true;
679
700
  isSubmitDisabled = true;
680
701
  }
681
- if (_.isEmpty(formState.dirtyFields) && !record?.isRemotePhantom) {
702
+ if (_.isEmpty(formState.dirtyFields) && !record?.isPhantom) {
682
703
  isSaveDisabled = true;
683
704
  }
684
705
 
@@ -688,6 +709,34 @@ function Form(props) {
688
709
  footerProps.alignItems = 'flex-start';
689
710
  }
690
711
 
712
+ if (onDelete && editorMode === EDITOR_MODE__EDIT && isSingle) {
713
+ showDeleteBtn = true;
714
+ }
715
+ if (!isEditorViewOnly) {
716
+ showResetBtn = true;
717
+ }
718
+ if (editorType !== EDITOR_TYPE__SIDE) { // side editor won't show either close or cancel buttons!
719
+ // determine whether we should show the close or cancel button
720
+ if (isEditorViewOnly) {
721
+ showCloseBtn = true;
722
+ } else {
723
+ if (formState.isDirty || record?.isPhantom) {
724
+ if (isSingle && onCancel) {
725
+ showCancelBtn = true;
726
+ }
727
+ } else {
728
+ if (onClose) {
729
+ showCloseBtn = true;
730
+ }
731
+ }
732
+ }
733
+ }
734
+ if (!isEditorViewOnly && onSave) {
735
+ showSaveBtn = true;
736
+ }
737
+ if (!!onSubmit) {
738
+ showSubmitBtn = true;
739
+ }
691
740
  }
692
741
 
693
742
  return <Column {...sizeProps} onLayout={onLayoutDecorated} ref={formRef}>
@@ -712,6 +761,7 @@ function Form(props) {
712
761
 
713
762
  <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
714
763
  {onDelete && editorMode === EDITOR_MODE__EDIT && isSingle &&
764
+
715
765
  <Row flex={1} justifyContent="flex-start">
716
766
  <Button
717
767
  key="deleteBtn"
@@ -724,7 +774,7 @@ function Form(props) {
724
774
  >Delete</Button>
725
775
  </Row>}
726
776
 
727
- {!isEditorViewOnly &&
777
+ {showResetBtn &&
728
778
  <IconButton
729
779
  key="resetBtn"
730
780
  onPress={() => {
@@ -733,36 +783,45 @@ function Form(props) {
733
783
  }
734
784
  reset();
735
785
  }}
736
- icon={<Rotate color="#fff" />}
786
+ icon={Rotate}
787
+ _icon={{
788
+ color: !formState.isDirty ? 'trueGray.400' : '#000',
789
+ }}
790
+ isDisabled={!formState.isDirty}
791
+ mr={2}
737
792
  />}
738
- {!isEditorViewOnly && isSingle && onCancel &&
793
+
794
+ {showCancelBtn &&
739
795
  <Button
740
796
  key="cancelBtn"
741
797
  variant="ghost"
742
798
  onPress={onCancel}
743
799
  color="#fff"
744
800
  >Cancel</Button>}
745
- {!isEditorViewOnly && onSave &&
801
+
802
+ {showCloseBtn &&
803
+ <Button
804
+ key="closeBtn"
805
+ variant="ghost"
806
+ onPress={onClose}
807
+ color="#fff"
808
+ >Close</Button>}
809
+
810
+ {showSaveBtn &&
746
811
  <Button
747
812
  key="saveBtn"
748
813
  onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
749
814
  isDisabled={isSaveDisabled}
750
815
  color="#fff"
751
816
  >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
752
- {onSubmit &&
817
+
818
+ {showSubmitBtn &&
753
819
  <Button
754
820
  key="submitBtn"
755
821
  onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
756
822
  isDisabled={isSubmitDisabled}
757
823
  color="#fff"
758
824
  >{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
825
 
767
826
  {additionalFooterButtons && _.map(additionalFooterButtons, (props) => {
768
827
  return <Button
@@ -27,6 +27,7 @@ export default function withAlert(WrappedComponent) {
27
27
  [isAlertShown, setIsAlertShown] = useState(false),
28
28
  [title, setTitle] = useState(''),
29
29
  [message, setMessage] = useState(''),
30
+ [canClose, setCanClose] = useState(true),
30
31
  [includeCancel, setIncludeCancel] = useState(false),
31
32
  [okCallback, setOkCallback] = useState(),
32
33
  [yesCallback, setYesCallback] = useState(),
@@ -35,7 +36,7 @@ export default function withAlert(WrappedComponent) {
35
36
  [mode, setMode] = useState(ALERT_MODE_OK),
36
37
  autoFocusRef = useRef(null),
37
38
  cancelRef = useRef(null),
38
- onAlert = (arg1, okCallback, includeCancel = false) => {
39
+ onAlert = (arg1, okCallback, includeCancel = false, canClose = true) => {
39
40
  clearAll();
40
41
  if (_.isString(arg1)) {
41
42
  setMode(ALERT_MODE_OK);
@@ -43,6 +44,7 @@ export default function withAlert(WrappedComponent) {
43
44
  setMessage(arg1);
44
45
  setOkCallback(() => okCallback);
45
46
  setIncludeCancel(includeCancel);
47
+ setCanClose(canClose);
46
48
  } else if (_.isPlainObject(arg1)) {
47
49
  // custom
48
50
  const {
@@ -50,12 +52,14 @@ export default function withAlert(WrappedComponent) {
50
52
  message,
51
53
  buttons,
52
54
  includeCancel,
55
+ canClose,
53
56
  } = arg1;
54
57
  setMode(ALERT_MODE_CUSTOM);
55
58
  setTitle(title);
56
59
  setMessage(message);
57
60
  setCustomButtons(buttons);
58
61
  setIncludeCancel(includeCancel);
62
+ setCanClose(canClose);
59
63
  }
60
64
  showAlert();
61
65
  },
@@ -161,7 +165,7 @@ export default function withAlert(WrappedComponent) {
161
165
  onClose={() => setIsAlertShown(false)}
162
166
  >
163
167
  <AlertDialog.Content>
164
- <AlertDialog.CloseButton />
168
+ {canClose && <AlertDialog.CloseButton />}
165
169
  <AlertDialog.Header>{title}</AlertDialog.Header>
166
170
  <AlertDialog.Body>
167
171
  <Row>