@onehat/ui 0.4.106 → 0.4.107

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.106",
3
+ "version": "0.4.107",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -46,10 +46,11 @@ function Editor(props) {
46
46
  if (canRecordBeEdited && !canRecordBeEdited(selection)) {
47
47
  canEdit = false;
48
48
  }
49
+ const record = selection[0];
50
+ self.record = record; // make it so we can target the record from within a Viewer or Form
49
51
 
50
52
  // Repository?.isRemotePhantomMode && selection.length === 1 &&
51
53
  if (getEditorMode() === EDITOR_MODE__VIEW || isEditorViewOnly || !canEdit) {
52
- const record = selection[0];
53
54
  if (record.isDestroyed) {
54
55
  return null;
55
56
  }
@@ -1223,6 +1223,25 @@ export const ComboComponent = forwardRef((props, ref) => {
1223
1223
  }
1224
1224
 
1225
1225
  if (isViewerShown && Editor) {
1226
+ let modalBackdrop = <ModalBackdrop className="Combo-viewer-ModalBackdrop" />;
1227
+ if (CURRENT_MODE === UI_MODE_NATIVE) {
1228
+ // Gluestack's ModalBackdrop was not working on Native,
1229
+ // so workaround is to do it manually for now
1230
+ modalBackdrop = <Pressable
1231
+ onPress={onViewerClose}
1232
+ className={clsx(
1233
+ 'Combo-viewer-ModalBackdrop-replacment',
1234
+ 'h-full',
1235
+ 'w-full',
1236
+ 'absolute',
1237
+ 'top-0',
1238
+ 'left-0',
1239
+ 'bg-[#000]',
1240
+ 'opacity-50',
1241
+ )}
1242
+ />;
1243
+ }
1244
+
1226
1245
  const propsForViewer = _.pick(props, [
1227
1246
  'disableCopy',
1228
1247
  'disableDuplicate',
@@ -1246,6 +1265,7 @@ export const ComboComponent = forwardRef((props, ref) => {
1246
1265
  isOpen={true}
1247
1266
  onClose={onViewerClose}
1248
1267
  >
1268
+ {modalBackdrop}
1249
1269
  <Editor
1250
1270
  editorType={EDITOR_TYPE__WINDOWED}
1251
1271
  isEditorViewOnly={true}
@@ -240,6 +240,7 @@ function Form(props) {
240
240
  resolver: yupResolver(validatorToUse),
241
241
  context: { isPhantom },
242
242
  }),
243
+ currentEditorMode = getEditorMode(),
243
244
  buildFromColumnsConfig = () => {
244
245
  // Only used in InlineEditor
245
246
  // Build the fields that match the current columnsConfig in the grid
@@ -1045,7 +1046,7 @@ function Form(props) {
1045
1046
  )}
1046
1047
  >{title}</Text>;
1047
1048
  if (icon) {
1048
- titleElement = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{titleElement}</HStack>
1049
+ titleElement = <HStack className="items-center mb-1"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{titleElement}</HStack>
1049
1050
  }
1050
1051
  }
1051
1052
  if (description) {
@@ -1149,6 +1150,29 @@ function Form(props) {
1149
1150
  }
1150
1151
  }, [record]);
1151
1152
 
1153
+ useEffect(() => {
1154
+ if (skipAll) {
1155
+ return;
1156
+ }
1157
+ if (currentEditorMode !== EDITOR_MODE__ADD) {
1158
+ return;
1159
+ }
1160
+ if (!containerWidth) {
1161
+ // Wait until fields are mounted; before this, isValid can be false with empty errors.
1162
+ return;
1163
+ }
1164
+
1165
+ // In some flows the editor mode flips to ADD after the record effect runs.
1166
+ // Validate again so the Add button is enabled immediately when form is valid.
1167
+ const timeoutId = setTimeout(() => {
1168
+ trigger();
1169
+ }, 0);
1170
+
1171
+ return () => {
1172
+ clearTimeout(timeoutId);
1173
+ };
1174
+ }, [record, currentEditorMode, containerWidth, trigger]);
1175
+
1152
1176
  useEffect(() => {
1153
1177
  if (skipAll) {
1154
1178
  return;
@@ -1249,7 +1273,8 @@ function Form(props) {
1249
1273
  showCloseBtn = false,
1250
1274
  showCancelBtn = false,
1251
1275
  showSaveBtn = false,
1252
- showSubmitBtn = false;
1276
+ showSubmitBtn = false,
1277
+ isAddMode = getEditorMode() === EDITOR_MODE__ADD;
1253
1278
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
1254
1279
 
1255
1280
  // create editor
@@ -1332,7 +1357,7 @@ function Form(props) {
1332
1357
  isSaveDisabled = true;
1333
1358
  isSubmitDisabled = true;
1334
1359
  }
1335
- if (_.isEmpty(formState.dirtyFields) && !isPhantom) {
1360
+ if (_.isEmpty(formState.dirtyFields) && !isPhantom && !isAddMode) {
1336
1361
  isSaveDisabled = true;
1337
1362
  }
1338
1363
  if (onDelete && getEditorMode() === EDITOR_MODE__EDIT && isSingle) {
@@ -430,7 +430,7 @@ function GridComponent(props) {
430
430
  return processedConfig;
431
431
  });
432
432
  const items = _.map(processedButtons, (config, ix) => getIconButtonFromConfig(config, ix, self));
433
- if (canRowsReorder && CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
433
+ if (canRowsReorder && CURRENT_MODE === UI_MODE_WEB && onEdit && (!canUser || canUser(EDIT)) && !isEditorViewOnly) { // DND is currently web-only TODO: implement for RN
434
434
  items.unshift(<IconButton
435
435
  {...testProps('reorderBtn')}
436
436
  key="reorderBtn"
@@ -450,6 +450,8 @@ const GridRow = forwardRef((props, ref) => {
450
450
  )}
451
451
  />}
452
452
  </>;
453
+ const hasCustomBgClass = rowProps?.className && /\bbg-/.test(rowProps.className);
454
+
453
455
  if (dropTargetRef) {
454
456
  rowContents = <HStack
455
457
  ref={dropTargetRef}
@@ -459,9 +461,7 @@ const GridRow = forwardRef((props, ref) => {
459
461
  'flex-1',
460
462
  'grow-1',
461
463
  )}
462
- style={{
463
- backgroundColor: bg,
464
- }}
464
+ style={hasCustomBgClass ? undefined : { backgroundColor: bg }}
465
465
  >{rowContents}</HStack>;
466
466
  }
467
467
 
@@ -485,9 +485,7 @@ const GridRow = forwardRef((props, ref) => {
485
485
  {...rowProps}
486
486
  key={hash}
487
487
  className={rowClassName}
488
- style={{
489
- backgroundColor: bg,
490
- }}
488
+ style={hasCustomBgClass ? undefined : { backgroundColor: bg }}
491
489
  >{rowContents}</HStackNative>;
492
490
  if (rowProps.tooltip) {
493
491
  row = <Tooltip
@@ -149,7 +149,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
149
149
  selection = getSelection();
150
150
  if (!_.isEmpty(formState?.dirtyFields) && newSelection !== selection && getEditorMode() === EDITOR_MODE__EDIT) {
151
151
  confirm('This record has unsaved changes. Are you sure you want to cancel editing? Changes will be lost.', doIt);
152
- } else if (selection && selection[0] && !selection[0].isDestroyed && (selection[0]?.isPhantom || selection[0]?.isRemotePhantom)) {
152
+ } else if (selection && selection[0] && !selection[0].isDestroyed && selection[0].isPhantom) {
153
153
  confirm('This new record is unsaved. Are you sure you want to cancel editing? Changes will be lost.', async () => {
154
154
  await selection[0].delete();
155
155
  doIt();
@@ -216,7 +216,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
216
216
  if (!record || record.isDestroyed) {
217
217
  return false;
218
218
  }
219
- return !!(record.isPhantom || record.isRemotePhantom);
219
+ return !!record.isPhantom;
220
220
  },
221
221
  getIsEditorDisabledByParent = () => {
222
222
  return getIsParentSaveLocked();
@@ -680,8 +680,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
680
680
  // just update this one entity
681
681
  selection[0].setValues(data);
682
682
 
683
- // If this is a remote phantom, and nothing is dirty, stage it so it actually gets saved to server and solidified
684
- if (selection[0].isRemotePhantom && !selection[0].isDirty) {
683
+ // In ADD mode, if record is phantom and nothing is dirty, stage it so save() still submits and solidifies.
684
+ if (getEditorMode() === EDITOR_MODE__ADD && selection[0].isPhantom && !selection[0].isDirty) {
685
685
  selection[0].markStaged();
686
686
  useStaged = true;
687
687
  }
@@ -834,7 +834,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
834
834
  if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
835
835
  return EDITOR_MODE__VIEW;
836
836
  }
837
- if (selection.length === 1 && !selection[0].isDestroyed && (selection[0].isPhantom || selection[0].isRemotePhantom) && !disableAdd) {
837
+ if (selection.length === 1 && !selection[0].isDestroyed && selection[0].isPhantom && !disableAdd) {
838
838
  return EDITOR_MODE__ADD;
839
839
  }
840
840
  return selection.length ? EDITOR_MODE__EDIT : EDITOR_MODE__VIEW;
@@ -33,7 +33,9 @@ export default function withPdfButtons(WrappedComponent) {
33
33
  additionalEditButtons = [],
34
34
  additionalViewButtons = [],
35
35
  items = [],
36
+ pdfItems,
36
37
  ancillaryItems = [],
38
+ pdfAncillaryItems,
37
39
  columnDefaults = {},
38
40
 
39
41
  // withComponent
@@ -58,32 +60,15 @@ export default function withPdfButtons(WrappedComponent) {
58
60
  styles = UiGlobals.styles,
59
61
  propertyNames = [],
60
62
  buildModalItems = () => {
61
- const modalItems = _.map(_.cloneDeep(items), (item, ix) => buildNextLayer(item, ix, columnDefaults)); // clone, as we don't want to alter the item by reference
62
-
63
- // remove additionalEditButtons from the modal
64
- function walkTreeToDeleteAdditionalEditButtons(item) {
65
- if (!item) {
66
- return;
67
- }
68
-
69
- let {
70
- additionalEditButtons,
71
- items,
72
- } = item;
73
- if (!_.isEmpty(items)) {
74
- _.each(items, (item) => {
75
- walkTreeToDeleteAdditionalEditButtons(item);
76
- });
77
- }
78
- if (additionalEditButtons) {
79
- delete item.additionalEditButtons;
80
- }
81
- }
82
- _.each(modalItems, walkTreeToDeleteAdditionalEditButtons);
63
+ // Build a cloned PDF item tree so we never mutate source items by reference.
64
+ const
65
+ itemsTouse = pdfItems || items,
66
+ ancillaryItemsToUse = pdfAncillaryItems || ancillaryItems,
67
+ modalItems = _.compact(_.map(itemsTouse, (item, ix) => buildNextLayer(item, ix, columnDefaults)));
83
68
 
84
- if (!_.isEmpty(ancillaryItems)) {
69
+ if (!_.isEmpty(ancillaryItemsToUse)) {
85
70
  const
86
- ancillaryItemsClone = _.cloneDeepWith(ancillaryItems, (value) => {
71
+ ancillaryItemsClone = _.cloneDeepWith(ancillaryItemsToUse, (value) => {
87
72
  // Exclude the 'parent' property from being cloned, as it would introduce an infinitely recursive loop
88
73
  if (value && value.parent) {
89
74
  const { parent, ...rest } = value;
@@ -127,47 +112,75 @@ export default function withPdfButtons(WrappedComponent) {
127
112
  return modalItems;
128
113
  },
129
114
  buildNextLayer = (item, ix, defaults) => {
130
- let {
131
- type,
132
- name,
133
- items,
134
- } = item;
115
+ if (!item) {
116
+ return null;
117
+ }
118
+
119
+ const {
120
+ type,
121
+ name,
122
+ title,
123
+ items: childItems,
124
+ isHiddenInViewMode,
125
+ } = item;
126
+
135
127
  if (inArray(type, ['Column', 'FieldSet'])) {
136
- if (!item.defaults) {
137
- item.defaults = {};
128
+ const nextDefaults = {
129
+ ...(defaults || {}),
130
+ ...(item.defaults || {}),
131
+ labelWidth: '90%',
132
+ };
133
+
134
+ const nextItem = {
135
+ type,
136
+ defaults: nextDefaults,
137
+ };
138
+
139
+ if (title) {
140
+ nextItem.title = title;
141
+ }
142
+ if (item.reference) {
143
+ nextItem.reference = item.reference;
144
+ }
145
+ if (item.flex) {
146
+ nextItem.flex = item.flex;
138
147
  }
139
148
  if (type === 'FieldSet') {
140
- item.showToggleAllCheckbox = true;
141
- item.isCollapsible = false;
149
+ nextItem.showToggleAllCheckbox = true;
150
+ nextItem.isCollapsible = false;
142
151
  }
143
- item.defaults.labelWidth = '90%';
144
- if (!_.isEmpty(items)) {
145
- const defaults = item.defaults;
146
- item.items = _.map(items, (item, ix) => {
147
- if (!item){
148
- return null;
149
- }
150
- return buildNextLayer(item, ix, defaults);
151
- });
152
+
153
+ if (!_.isEmpty(childItems)) {
154
+ nextItem.items = _.compact(_.map(childItems, (childItem, childIx) => buildNextLayer(childItem, childIx, nextDefaults)));
152
155
  }
153
- return item;
156
+
157
+ return nextItem;
154
158
  }
155
159
 
156
- if (item.isHiddenInViewMode || type === 'Button') {
160
+ if (isHiddenInViewMode || type === 'Button') {
157
161
  return null;
158
162
  }
159
163
 
160
- if (!item.title) {
164
+ let resolvedTitle = title;
165
+ if (!resolvedTitle) {
161
166
  const propertyDef = name && Repository?.getSchema().getPropertyDefinition(name);
162
167
  if (propertyDef?.title) {
163
- item.title = propertyDef.title;
168
+ resolvedTitle = propertyDef.title;
164
169
  }
165
170
  }
166
171
  if (name) {
167
172
  propertyNames.push(name); // for validator
168
173
  }
169
- item.type = 'Checkbox';
170
- return item;
174
+ if (!name) {
175
+ return null;
176
+ }
177
+
178
+ return {
179
+ type: 'Checkbox',
180
+ name,
181
+ title: resolvedTitle,
182
+ isEditable: false, // hack to force all checkboxes to use same render branch in Form
183
+ };
171
184
  },
172
185
  buildValidator = () => {
173
186
  const propertyValidatorDefs = {};
@@ -3,6 +3,7 @@ import {
3
3
  selectIsSetupMode,
4
4
  toggleSetupMode,
5
5
  } from '@src/Models/Slices/AppSlice';
6
+ import clsx from 'clsx';
6
7
  import Button from '../Buttons/Button';
7
8
  import IconButton from '../Buttons/IconButton';
8
9
  import Gear from '../Icons/Gear';
@@ -13,19 +14,44 @@ export default function SetupButton(props) {
13
14
  } = props,
14
15
  dispatch = useDispatch(),
15
16
  isSetupMode = useSelector(selectIsSetupMode),
16
- onPress = () => dispatch(toggleSetupMode());
17
+ onPress = () => dispatch(toggleSetupMode()),
18
+ buttonClassName = clsx(
19
+ 'SetupButton',
20
+ isSetupMode
21
+ ? 'bg-red-500 data-[hover=true]:bg-red-600 data-[active=true]:bg-red-700'
22
+ : 'bg-grey-100 data-[hover=true]:bg-grey-900/20 data-[active=true]:bg-grey-900/50',
23
+ ),
24
+ textClassName = clsx(
25
+ isSetupMode
26
+ ? 'text-white data-[hover=true]:text-white data-[active=true]:text-white'
27
+ : 'text-black data-[hover=true]:text-black data-[active=true]:text-black',
28
+ ),
29
+ iconClassName = clsx(
30
+ isSetupMode ? 'fill-white' : 'fill-black',
31
+ isSetupMode ? 'text-white' : 'text-black',
32
+ );
33
+
17
34
  return isMinimized ?
18
35
  <IconButton
19
36
  icon={Gear}
37
+ _icon={{
38
+ className: iconClassName,
39
+ }}
20
40
  onPress={onPress}
21
41
  tooltip="Toggle Setup Mode"
22
- className="SetupButton-IconButton"
42
+ className={buttonClassName}
23
43
  /> :
24
44
  <Button
25
45
  text={isSetupMode ? 'Exit Setup' : 'Setup'}
26
46
  icon={Gear}
47
+ _text={{
48
+ className: textClassName,
49
+ }}
50
+ _icon={{
51
+ className: iconClassName,
52
+ }}
27
53
  onPress={onPress}
28
54
  tooltip="Toggle Setup Mode"
29
- className="SetupButton-Button"
55
+ className={buttonClassName}
30
56
  />;
31
57
  };
@@ -51,6 +51,7 @@ function Viewer(props) {
51
51
  const {
52
52
  viewerCanDelete = false,
53
53
  items = [], // Columns, FieldSets, Fields, etc to define the form
54
+ isItemsCustomLayout = false,
54
55
  ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
55
56
  showAncillaryButtons = false,
56
57
  columnDefaults = {}, // defaults for each Column defined in items (above)
@@ -183,7 +184,7 @@ function Viewer(props) {
183
184
  let children;
184
185
  const style = {};
185
186
  if (type === 'Column') {
186
- const isEverythingInOneColumn = containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD;
187
+ const isEverythingInOneColumn = isItemsCustomLayout || containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD;
187
188
  if (itemPropsToPass.hasOwnProperty('flex')) {
188
189
  if (!isEverythingInOneColumn) {
189
190
  style.flex = itemPropsToPass.flex;
@@ -489,7 +490,9 @@ function Viewer(props) {
489
490
  const
490
491
  showDeleteBtn = onDelete && viewerCanDelete,
491
492
  showCloseBtn = !isSideEditor && !isSmartEditor && onClose,
492
- showFooter = (showDeleteBtn || showCloseBtn);
493
+ showFooter = (showDeleteBtn || showCloseBtn),
494
+ hasTopLevelColumns = _.some(items, (item) => item?.type === 'Column'),
495
+ shouldUseHorizontalViewerLayout = !isItemsCustomLayout && hasTopLevelColumns && containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD;
493
496
  let additionalButtons = null,
494
497
  viewerComponents = null,
495
498
  ancillaryComponents = null,
@@ -611,8 +614,8 @@ function Viewer(props) {
611
614
  {buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
612
615
  </Toolbar>}
613
616
 
614
- {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Viewer-formComponents-HStack p-4 gap-4 justify-center">{viewerComponents}</HStack> : null}
615
- {containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD ? <VStack className="Viewer-formComponents-VStack p-4">{viewerComponents}</VStack> : null}
617
+ {shouldUseHorizontalViewerLayout ? <HStack className="Viewer-formComponents-HStack p-4 gap-4 justify-center">{viewerComponents}</HStack> : null}
618
+ {!shouldUseHorizontalViewerLayout ? <VStack className="Viewer-formComponents-VStack p-4">{viewerComponents}</VStack> : null}
616
619
  <VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
617
620
  </ScrollView>
618
621