@onehat/ui 0.3.29 → 0.3.31

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.31",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import {
3
+ Icon,
4
+ Pressable,
5
+ Text,
6
+ } from 'native-base';
7
+ import UiGlobals from '../../UiGlobals.js';
8
+
9
+ export default function SquareButton(props) {
10
+ const {
11
+ text,
12
+ isActive = false,
13
+ disableInteractions = false,
14
+ ...propsToPass
15
+ } = props,
16
+ styles = UiGlobals.styles,
17
+ color = isActive ? '#fff' : '#000';
18
+ const propsIcon = props._icon || {};
19
+ let icon = props.icon;
20
+ if (!icon) {
21
+ throw Error('icon missing');
22
+ }
23
+ if (!text) {
24
+ throw Error('text missing');
25
+ }
26
+
27
+ if (React.isValidElement(icon)) {
28
+ if (!_.isEmpty(propsIcon)) {
29
+ icon = React.cloneElement(icon, {...propsIcon});
30
+ }
31
+ } else {
32
+ icon = <Icon as={icon} {...propsIcon} />;
33
+ }
34
+
35
+ const
36
+ hoverProps = {},
37
+ pressedProps = {};
38
+ if (!disableInteractions) {
39
+ hoverProps.bg = styles.ICON_BUTTON_BG_HOVER;
40
+ pressedProps.bg = styles.ICON_BUTTON_BG_PRESSED;
41
+ }
42
+
43
+ return <Pressable
44
+ borderRadius="md"
45
+ flexDirection="column"
46
+ justifyContent="center"
47
+ alignItems="center"
48
+ p={2}
49
+ {...propsToPass}
50
+ bg={isActive ? '#56a6f8' : '#fff'}
51
+ _hover={hoverProps}
52
+ _pressed={pressedProps}
53
+ >
54
+ <Icon as={icon} color={color} size="xl" />
55
+ <Text fontSize={20} color={color}>{text}</Text>
56
+ </Pressable>;
57
+ }
58
+
@@ -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);
@@ -42,27 +46,36 @@ export default function withPdfButton(WrappedComponent) {
42
46
  const modalItems = _.map(_.cloneDeep(items), (item, ix) => buildNextLayer(item, ix, columnDefaults)); // clone, as we don't want to alter the item by reference
43
47
 
44
48
  if (!_.isEmpty(ancillaryItems)) {
49
+ const
50
+ ancillaryItemsClone = _.cloneDeep(ancillaryItems),
51
+ items = [];
52
+ _.each(ancillaryItemsClone, (ancillaryItem) => { // clone, as we don't want to alter the item by reference
53
+ let name;
54
+ if (ancillaryItem.model) {
55
+ name = Inflector.underscore(ancillaryItem.model);
56
+ } else {
57
+ name = ancillaryItem.title;
58
+ }
59
+ if (!inArray(name, ['Photos', 'Videos', 'Files'])) {
60
+ return;
61
+ }
62
+ name = 'ancillary___' + name;
63
+ items.push({
64
+ title: ancillaryItem.title,
65
+ label: ancillaryItem.title,
66
+ name,
67
+ type: 'Checkbox',
68
+ });
69
+ });
45
70
  modalItems.push({
46
71
  type: 'FieldSet',
47
72
  title: 'Ancillary Items',
48
73
  defaults: {
49
74
  labelWidth: '90%',
50
75
  },
51
- items: _.map(_.cloneDeep(ancillaryItems), (ancillaryItem) => { // clone, as we don't want to alter the item by reference
52
- let name;
53
- if (ancillaryItem.model) {
54
- name = Inflector.underscore(ancillaryItem.model);
55
- } else {
56
- name = ancillaryItem.title;
57
- }
58
- name = 'ancillary___' + name;
59
- return {
60
- title: ancillaryItem.title,
61
- label: ancillaryItem.title,
62
- name,
63
- type: 'Checkbox',
64
- };
65
- }),
76
+ items,
77
+ showToggleAllCheckbox: true,
78
+ isCollapsible: false,
66
79
  });
67
80
  }
68
81
 
@@ -78,6 +91,10 @@ export default function withPdfButton(WrappedComponent) {
78
91
  if (!item.defaults) {
79
92
  item.defaults = {};
80
93
  }
94
+ if (type === 'FieldSet') {
95
+ item.showToggleAllCheckbox = true;
96
+ item.isCollapsible = false;
97
+ }
81
98
  item.defaults.labelWidth = '90%';
82
99
  if (!_.isEmpty(items)) {
83
100
  const defaults = item.defaults;
@@ -97,6 +114,12 @@ export default function withPdfButton(WrappedComponent) {
97
114
  item.type = 'Checkbox';
98
115
  return item;
99
116
  },
117
+ buildValidator = (modalItems) => {
118
+
119
+ // TODO: Build a real validator that checks all modalItems as booleans
120
+
121
+ return yup.object();
122
+ },
100
123
  getStartingValues = (modalItems) => {
101
124
  const startingValues = {};
102
125
  function walkTree(item) {
@@ -117,9 +140,12 @@ export default function withPdfButton(WrappedComponent) {
117
140
  return startingValues;
118
141
  },
119
142
  getPdf = (data) => {
143
+ data.id = selection[0].id;
144
+
120
145
  const
121
146
  url = UiGlobals.baseURL + model + '/viewPdf?',
122
147
  queryString = qs.stringify(data);
148
+
123
149
  window.open(url + queryString, '_blank');
124
150
  };
125
151
 
@@ -142,7 +168,8 @@ export default function withPdfButton(WrappedComponent) {
142
168
  if (isModalShown) {
143
169
  const
144
170
  modalItems = buildModalItems(),
145
- startingValues = getStartingValues(modalItems);
171
+ startingValues = getStartingValues(modalItems),
172
+ validator = buildValidator(modalItems);
146
173
  modal = <Modal
147
174
  isOpen={true}
148
175
  onClose={() => setIsModalShown(false)}
@@ -156,14 +183,16 @@ export default function withPdfButton(WrappedComponent) {
156
183
  Repository={Repository}
157
184
  items={modalItems}
158
185
  startingValues={startingValues}
186
+ validator={validator}
187
+ checkIsEditingDisabled={false}
159
188
  onCancel={(e) => {
160
189
  setIsModalShown(false);
161
190
  }}
162
- onSave={(data, e) => {
191
+ onSubmit={(data, e) => {
163
192
  getPdf(data);
164
193
  setIsModalShown(false);
165
194
  }}
166
- saveBtnLabel="View PDF"
195
+ submitBtnLabel="View PDF"
167
196
  />
168
197
  </Column>
169
198
  </Modal>;
@@ -35,6 +35,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
35
35
  contextMenuItems,
36
36
  additionalToolbarButtons,
37
37
  onChangeColumnsConfig,
38
+ verifyCanEdit,
39
+ verifyCanDelete,
40
+ verifyCanDuplicate,
38
41
  ...propsToPass
39
42
  } = props,
40
43
  {
@@ -146,6 +149,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
146
149
  if (_.isEmpty(selection) || (_.isArray(selection) && selection.length > 1)) {
147
150
  isDisabled = true;
148
151
  }
152
+ if (verifyCanEdit && !verifyCanEdit(selection)) {
153
+ isDisabled = true;
154
+ }
149
155
  break;
150
156
  case 'delete':
151
157
  text = 'Delete';
@@ -157,6 +163,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
157
163
  if (_.isEmpty(selection) || (_.isArray(selection) && selection.length > 1)) {
158
164
  isDisabled = true;
159
165
  }
166
+ if (verifyCanDelete && !verifyCanDelete(selection)) {
167
+ isDisabled = true;
168
+ }
160
169
  break;
161
170
  case 'view':
162
171
  text = 'View';
@@ -193,6 +202,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
193
202
  if (_.isEmpty(selection) || selection.length > 1) {
194
203
  isDisabled = true;
195
204
  }
205
+ if (verifyCanDuplicate && !verifyCanDuplicate(selection)) {
206
+ isDisabled = true;
207
+ }
196
208
  break;
197
209
  // case 'print':
198
210
  // text = 'Print';
@@ -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
  }
@@ -109,6 +109,7 @@ function Viewer(props) {
109
109
 
110
110
  let element = <Element
111
111
  value={value}
112
+ isEditable={false}
112
113
  {...propsToPass}
113
114
  />;
114
115
  if (label) {
@@ -37,6 +37,7 @@ import Panel from './Panel/Panel.js';
37
37
  // import Picker from '../Components/Panel/Picker.js';
38
38
  import PlusMinusButton from './Buttons/PlusMinusButton.js';
39
39
  import RadioGroup from './Form/Field/RadioGroup/RadioGroup.js';
40
+ import SquareButton from './Buttons/SquareButton.js';
40
41
  import TabPanel from './Panel/TabPanel.js';
41
42
  import Tag from './Form/Field/Combo/Tag.js';
42
43
  import TagViewer from './Viewer/TagViewer.js';
@@ -88,6 +89,7 @@ const components = {
88
89
  // Picker,
89
90
  PlusMinusButton,
90
91
  RadioGroup,
92
+ SquareButton,
91
93
  TabPanel,
92
94
  Tag,
93
95
  TagViewer,
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+
3
+ const FieldSetContext = createContext();
4
+ export default FieldSetContext;