@onehat/ui 0.3.29 → 0.3.30

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.29",
3
+ "version": "0.3.30",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,28 +1,79 @@
1
- import { useState, } from 'react';
1
+ import { useState, useRef, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  Column,
5
5
  Row,
6
6
  Text,
7
7
  } from 'native-base';
8
+ import FieldSetContext from '../../Contexts/FieldSetContext.js';
9
+ import useForceUpdate from '../../Hooks/useForceUpdate.js';
8
10
  import UiGlobals from '../../UiGlobals.js';
9
11
  import IconButton from '../Buttons/IconButton.js';
12
+ import CheckboxButton from '../Buttons/CheckboxButton.js';
10
13
  import CaretUp from '../Icons/CaretUp.js';
11
14
  import CaretDown from '../Icons/CaretDown.js';
15
+ import _ from 'lodash';
12
16
 
13
17
  export default function FieldSet(props) {
14
18
  const {
15
19
  title,
16
20
  helpText,
17
21
  children,
22
+ isCollapsible = true,
18
23
  isCollapsed,
19
24
  hasErrors,
25
+ showToggleAllCheckbox = false,
20
26
  ...propsToPass
21
27
  } = props,
22
28
  styles = UiGlobals.styles,
29
+ forceUpdate = useForceUpdate(),
30
+ childRefs = useRef([]),
31
+ isAllCheckedRef = useRef(false),
23
32
  [localIsCollapsed, setLocalIsCollapsed] = useState(isCollapsed),
33
+ getIsAllChecked = () => {
34
+ return isAllCheckedRef.current;
35
+ },
36
+ setIsAllChecked = (bool) => {
37
+ isAllCheckedRef.current = bool;
38
+ forceUpdate();
39
+ },
24
40
  onToggleCollapse = () => {
25
41
  setLocalIsCollapsed(!localIsCollapsed);
42
+ },
43
+ onToggleAllChecked = () => {
44
+ const bool = !getIsAllChecked();
45
+ setIsAllChecked(bool);
46
+
47
+ _.each(childRefs.current, (child) => {
48
+ if (child.value !== bool) {
49
+ child.value = bool;
50
+ child.setValue(bool);
51
+ }
52
+ });
53
+ },
54
+ registerChild = (child) => {
55
+ childRefs.current.push(child);
56
+ checkChildren();
57
+ },
58
+ onChangeValue = (value, childRef) => {
59
+ const child = _.find(childRefs.current, child => child.childRef === childRef);
60
+ if (child.value !== value) {
61
+ child.value = value;
62
+ checkChildren();
63
+ }
64
+ },
65
+ checkChildren = () => {
66
+ let isAllChecked = true;
67
+ _.each(childRefs.current, (child) => {
68
+ if (!child.value) {
69
+ isAllChecked = false;
70
+ return false; // break
71
+ }
72
+ });
73
+
74
+ if (isAllChecked !== getIsAllChecked()) {
75
+ setIsAllChecked(isAllChecked);
76
+ }
26
77
  };
27
78
 
28
79
  return <Box
@@ -50,16 +101,34 @@ export default function FieldSet(props) {
50
101
  numberOfLines={1}
51
102
  ellipsizeMode="head"
52
103
  >{title}</Text>
53
- <IconButton
54
- _icon={{
55
- as: localIsCollapsed ? <CaretDown /> : <CaretUp />,
56
- size: 'sm',
57
- color: 'trueGray.300',
58
- }}
59
- onPress={onToggleCollapse}
60
- />
104
+ {showToggleAllCheckbox && <Row alignSelf="right">
105
+ <Text
106
+ fontSize={styles.FORM_FIELDSET_FONTSIZE}
107
+ py={1}
108
+ px={3}
109
+ flex={1}
110
+ numberOfLines={1}
111
+ >Toggle All?</Text>
112
+ <CheckboxButton
113
+ isChecked={getIsAllChecked()}
114
+ onPress={onToggleAllChecked}
115
+ _icon={{
116
+ size: 'lg',
117
+ }}
118
+ />
119
+ </Row>}
120
+ {isCollapsible && <IconButton
121
+ _icon={{
122
+ as: localIsCollapsed ? <CaretDown /> : <CaretUp />,
123
+ size: 'sm',
124
+ color: 'trueGray.300',
125
+ }}
126
+ onPress={onToggleCollapse}
127
+ />}
61
128
  </Row>}
62
129
  {helpText && <Text>{helpText}</Text>}
63
- {!localIsCollapsed && children}
130
+ {!localIsCollapsed && <FieldSetContext.Provider value={{ registerChild, onChangeValue, }}>
131
+ {children}
132
+ </FieldSetContext.Provider>}
64
133
  </Box>;
65
134
  }
@@ -65,10 +65,12 @@ function Form(props) {
65
65
  validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
66
66
  footerProps = {},
67
67
  buttonGroupProps = {}, // buttons in footer
68
+ checkIsEditingDisabled = true,
68
69
  onBack,
69
70
  onReset,
70
71
  onViewMode,
71
- saveBtnLabel,
72
+ submitBtnLabel,
73
+ onSubmit,
72
74
  additionalEditButtons = [],
73
75
 
74
76
  // sizing of outer container
@@ -274,7 +276,7 @@ function Form(props) {
274
276
  editorTypeProps = {};
275
277
 
276
278
  const propertyDef = name && Repository?.getSchema().getPropertyDefinition(name);
277
- if (propertyDef?.isEditingDisabled) {
279
+ if (propertyDef?.isEditingDisabled && checkIsEditingDisabled) {
278
280
  isEditable = false;
279
281
  }
280
282
  if (!type) {
@@ -479,6 +481,13 @@ function Form(props) {
479
481
  const values = record.submitValues;
480
482
  reset(values);
481
483
  }
484
+ },
485
+ onSubmitDecorated = async (data, e) => {
486
+ const result = await onSubmit(data, e);
487
+ if (result) {
488
+ const values = record.submitValues;
489
+ reset(values);
490
+ }
482
491
  };
483
492
 
484
493
  useEffect(() => {
@@ -579,9 +588,11 @@ function Form(props) {
579
588
  break;
580
589
  }
581
590
 
582
- let isSaveDisabled = false;
591
+ let isSaveDisabled = false,
592
+ isSubmitDisabled = false;
583
593
  if (!_.isEmpty(formState.errors)) {
584
594
  isSaveDisabled = true;
595
+ isSubmitDisabled = true;
585
596
  }
586
597
  if (_.isEmpty(formState.dirtyFields) && !record?.isRemotePhantom) {
587
598
  isSaveDisabled = true;
@@ -655,7 +666,14 @@ function Form(props) {
655
666
  onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
656
667
  isDisabled={isSaveDisabled}
657
668
  color="#fff"
658
- >{saveBtnLabel || (editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save')}</Button>}
669
+ >{editorMode === EDITOR_MODE__ADD ? 'Add' : 'Save'}</Button>}
670
+ {onSubmit && <Button
671
+ key="submitBtn"
672
+ onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
673
+ isDisabled={isSubmitDisabled}
674
+ color="#fff"
675
+ >{submitBtnLabel || 'Submit'}</Button>}
676
+
659
677
  {isEditorViewOnly && onClose && editorType !== EDITOR_TYPE__SIDE && <Button
660
678
  key="closeBtn"
661
679
  onPress={onClose}
@@ -3,8 +3,8 @@ import {
3
3
  Column,
4
4
  Button,
5
5
  Modal,
6
- Row,
7
6
  } from 'native-base';
7
+ import * as yup from 'yup'; // https://github.com/jquense/yup#string
8
8
  import Inflector from 'inflector-js';
9
9
  import qs from 'qs';
10
10
  import FormPanel from '../Panel/FormPanel.js';
@@ -35,6 +35,10 @@ export default function withPdfButton(WrappedComponent) {
35
35
  // withData
36
36
  Repository,
37
37
  model,
38
+
39
+ // withSelection
40
+ selection,
41
+
38
42
  } = props,
39
43
  [isModalShown, setIsModalShown] = useState(false),
40
44
  [width, height] = useAdjustedWindowSize(500, 800);
@@ -63,6 +67,8 @@ export default function withPdfButton(WrappedComponent) {
63
67
  type: 'Checkbox',
64
68
  };
65
69
  }),
70
+ showToggleAllCheckbox: true,
71
+ isCollapsible: false,
66
72
  });
67
73
  }
68
74
 
@@ -78,6 +84,10 @@ export default function withPdfButton(WrappedComponent) {
78
84
  if (!item.defaults) {
79
85
  item.defaults = {};
80
86
  }
87
+ if (type === 'FieldSet') {
88
+ item.showToggleAllCheckbox = true;
89
+ item.isCollapsible = false;
90
+ }
81
91
  item.defaults.labelWidth = '90%';
82
92
  if (!_.isEmpty(items)) {
83
93
  const defaults = item.defaults;
@@ -97,6 +107,12 @@ export default function withPdfButton(WrappedComponent) {
97
107
  item.type = 'Checkbox';
98
108
  return item;
99
109
  },
110
+ buildValidator = (modalItems) => {
111
+
112
+ // TODO: Build a real validator that checks all modalItems as booleans
113
+
114
+ return yup.object();
115
+ },
100
116
  getStartingValues = (modalItems) => {
101
117
  const startingValues = {};
102
118
  function walkTree(item) {
@@ -117,9 +133,12 @@ export default function withPdfButton(WrappedComponent) {
117
133
  return startingValues;
118
134
  },
119
135
  getPdf = (data) => {
136
+ data.id = selection[0].id;
137
+
120
138
  const
121
139
  url = UiGlobals.baseURL + model + '/viewPdf?',
122
140
  queryString = qs.stringify(data);
141
+
123
142
  window.open(url + queryString, '_blank');
124
143
  };
125
144
 
@@ -142,7 +161,8 @@ export default function withPdfButton(WrappedComponent) {
142
161
  if (isModalShown) {
143
162
  const
144
163
  modalItems = buildModalItems(),
145
- startingValues = getStartingValues(modalItems);
164
+ startingValues = getStartingValues(modalItems),
165
+ validator = buildValidator(modalItems);
146
166
  modal = <Modal
147
167
  isOpen={true}
148
168
  onClose={() => setIsModalShown(false)}
@@ -156,14 +176,16 @@ export default function withPdfButton(WrappedComponent) {
156
176
  Repository={Repository}
157
177
  items={modalItems}
158
178
  startingValues={startingValues}
179
+ validator={validator}
180
+ checkIsEditingDisabled={false}
159
181
  onCancel={(e) => {
160
182
  setIsModalShown(false);
161
183
  }}
162
- onSave={(data, e) => {
184
+ onSubmit={(data, e) => {
163
185
  getPdf(data);
164
186
  setIsModalShown(false);
165
187
  }}
166
- saveBtnLabel="View PDF"
188
+ submitBtnLabel="View PDF"
167
189
  />
168
190
  </Column>
169
191
  </Modal>;
@@ -1,5 +1,7 @@
1
- import { useState, useEffect, } from 'react';
1
+ import { useState, useEffect, useRef, useContext, useCallback, } from 'react';
2
2
  import natsort from 'natsort';
3
+ import useForceUpdate from '../../Hooks/useForceUpdate.js';
4
+ import FieldSetContext from '../../Contexts/FieldSetContext.js';
3
5
  import _ from 'lodash';
4
6
 
5
7
  // This HOC gives the component value props, primarily for a Form Field.
@@ -27,8 +29,23 @@ export default function withValue(WrappedComponent) {
27
29
  Repository,
28
30
  idIx,
29
31
  } = props,
30
- [localValue, setLocalValue] = useState(startingValue || value),
31
- setValue = (newValue) => {
32
+ forceUpdate = useForceUpdate(),
33
+ childRef = useRef({}),
34
+ onChangeValueRef = useRef(),
35
+ localValueRef = useRef(startingValue || value),
36
+ fieldSetOnChangeValueRef = useRef(),
37
+ fieldSetContext = useContext(FieldSetContext),
38
+ fieldSetRegisterChild = fieldSetContext?.registerChild,
39
+ fieldSetOnChangeValue = fieldSetContext?.onChangeValue,
40
+ getLocalValue = () => {
41
+ return localValueRef.current;
42
+ },
43
+ setLocalValue = (value) => {
44
+ localValueRef.current = value;
45
+ forceUpdate();
46
+ },
47
+ setValueRef = useRef((newValue) => {
48
+ // NOTE: We useRef so that this function stays current after renders
32
49
  if (valueIsAlwaysArray && !_.isArray(newValue)) {
33
50
  newValue = _.isNil(newValue) ? [] : [newValue];
34
51
  }
@@ -62,15 +79,21 @@ export default function withValue(WrappedComponent) {
62
79
  newValue = JSON.stringify(newValue);
63
80
  }
64
81
 
65
- if (newValue === localValue) {
82
+ if (newValue === getLocalValue()) {
66
83
  return;
67
84
  }
68
85
 
69
86
  setLocalValue(newValue);
70
87
 
71
- if (onChangeValue) {
72
- onChangeValue(newValue);
88
+ if (onChangeValueRef.current) {
89
+ onChangeValueRef.current(newValue, childRef.current);
73
90
  }
91
+ if (fieldSetOnChangeValueRef.current) {
92
+ fieldSetOnChangeValueRef.current(newValue, childRef.current);
93
+ }
94
+ }),
95
+ setValue = (args) => {
96
+ setValueRef.current(args);
74
97
  },
75
98
  onChangeSelection = (selection) => {
76
99
  let value = null,
@@ -96,15 +119,29 @@ export default function withValue(WrappedComponent) {
96
119
  setValue(value);
97
120
  };
98
121
 
122
+ // Ensure these passed functions stay current after render
123
+ onChangeValueRef.current = onChangeValue;
124
+ fieldSetOnChangeValueRef.current = fieldSetOnChangeValue;
125
+
99
126
  useEffect(() => {
100
- if (!_.isEqual(value, localValue)) {
127
+ if (!_.isEqual(value, getLocalValue())) {
101
128
  setLocalValue(value);
102
129
  }
103
130
  }, [value]);
104
131
 
132
+ if (fieldSetRegisterChild) {
133
+ useEffect(() => {
134
+ fieldSetRegisterChild({
135
+ childRef: childRef.current,
136
+ value,
137
+ setValue: setValueRef.current,
138
+ });
139
+ }, []);
140
+ }
141
+
105
142
 
106
143
  // Convert localValue to normal JS primitives for field components
107
- let convertedValue = localValue;
144
+ let convertedValue = getLocalValue();
108
145
  if (_.isString(convertedValue) && valueAsStringifiedJson && !_.isNil(convertedValue)) {
109
146
  convertedValue = JSON.parse(convertedValue);
110
147
  }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+
3
+ const FieldSetContext = createContext();
4
+ export default FieldSetContext;