@onehat/ui 0.4.105 → 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.105",
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
@@ -969,6 +970,14 @@ function Form(props) {
969
970
  buildAncillary = () => {
970
971
  const
971
972
  validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
973
+ parentEditorModeRaw = getEditorMode?.() || props.editorMode || null,
974
+ parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
975
+ ? EDITOR_MODE__EDIT
976
+ : parentEditorModeRaw,
977
+ normalizedParentEditorMode =
978
+ parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
979
+ ? parentEditorMode
980
+ : null,
972
981
  components = [];
973
982
  setAncillaryButtons([]);
974
983
  if (validAncillaryItems.length) {
@@ -1011,6 +1020,8 @@ function Form(props) {
1011
1020
  }
1012
1021
 
1013
1022
  const
1023
+ ancillaryEditorMode = itemPropsToPass.editorMode ?? normalizedParentEditorMode,
1024
+ ancillaryInitialEditorMode = itemPropsToPass.initialEditorMode ?? ancillaryEditorMode ?? undefined,
1014
1025
  Element = getComponentFromType(type),
1015
1026
  element = <Element
1016
1027
  {...testProps('ancillary-' + type)}
@@ -1020,6 +1031,8 @@ function Form(props) {
1020
1031
  uniqueRepository={true}
1021
1032
  parent={self}
1022
1033
  {...itemPropsToPass}
1034
+ editorMode={ancillaryEditorMode}
1035
+ initialEditorMode={ancillaryInitialEditorMode}
1023
1036
  />;
1024
1037
  if (title) {
1025
1038
  if (record?.displayValue) {
@@ -1033,7 +1046,7 @@ function Form(props) {
1033
1046
  )}
1034
1047
  >{title}</Text>;
1035
1048
  if (icon) {
1036
- 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>
1037
1050
  }
1038
1051
  }
1039
1052
  if (description) {
@@ -1137,6 +1150,29 @@ function Form(props) {
1137
1150
  }
1138
1151
  }, [record]);
1139
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
+
1140
1176
  useEffect(() => {
1141
1177
  if (skipAll) {
1142
1178
  return;
@@ -1237,7 +1273,8 @@ function Form(props) {
1237
1273
  showCloseBtn = false,
1238
1274
  showCancelBtn = false,
1239
1275
  showSaveBtn = false,
1240
- showSubmitBtn = false;
1276
+ showSubmitBtn = false,
1277
+ isAddMode = getEditorMode() === EDITOR_MODE__ADD;
1241
1278
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
1242
1279
 
1243
1280
  // create editor
@@ -1320,7 +1357,7 @@ function Form(props) {
1320
1357
  isSaveDisabled = true;
1321
1358
  isSubmitDisabled = true;
1322
1359
  }
1323
- if (_.isEmpty(formState.dirtyFields) && !isPhantom) {
1360
+ if (_.isEmpty(formState.dirtyFields) && !isPhantom && !isAddMode) {
1324
1361
  isSaveDisabled = true;
1325
1362
  }
1326
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
@@ -57,6 +57,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
57
57
  newEntityDisplayValue,
58
58
  newEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
59
59
  defaultValues,
60
+ editorMode: parentEditorModeProp,
60
61
  initialEditorMode = EDITOR_MODE__VIEW,
61
62
  stayInEditModeOnSelectionChange = false,
62
63
  inheritParentEditorMode = true,
@@ -148,7 +149,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
148
149
  selection = getSelection();
149
150
  if (!_.isEmpty(formState?.dirtyFields) && newSelection !== selection && getEditorMode() === EDITOR_MODE__EDIT) {
150
151
  confirm('This record has unsaved changes. Are you sure you want to cancel editing? Changes will be lost.', doIt);
151
- } 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) {
152
153
  confirm('This new record is unsaved. Are you sure you want to cancel editing? Changes will be lost.', async () => {
153
154
  await selection[0].delete();
154
155
  doIt();
@@ -174,7 +175,21 @@ export default function withEditor(WrappedComponent, isTree = false) {
174
175
  }
175
176
  },
176
177
  getParentEditorMode = () => {
177
- return parentEditorModeContext?.effectiveEditorMode || null;
178
+ const contextMode = parentEditorModeContext?.effectiveEditorMode || null;
179
+ if (contextMode) {
180
+ return contextMode;
181
+ }
182
+
183
+ // Some modal implementations break React context boundaries. Fall back to
184
+ // an explicitly-passed parent mode so nested ancillary editors still inherit.
185
+ if (parentEditorModeProp === EDITOR_MODE__ADD) {
186
+ return EDITOR_MODE__EDIT;
187
+ }
188
+ if (parentEditorModeProp === EDITOR_MODE__EDIT || parentEditorModeProp === EDITOR_MODE__VIEW) {
189
+ return parentEditorModeProp;
190
+ }
191
+
192
+ return null;
178
193
  },
179
194
  getInheritedEditorMode = () => {
180
195
  if (!inheritParentEditorMode) {
@@ -201,7 +216,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
201
216
  if (!record || record.isDestroyed) {
202
217
  return false;
203
218
  }
204
- return !!(record.isPhantom || record.isRemotePhantom);
219
+ return !!record.isPhantom;
205
220
  },
206
221
  getIsEditorDisabledByParent = () => {
207
222
  return getIsParentSaveLocked();
@@ -665,8 +680,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
665
680
  // just update this one entity
666
681
  selection[0].setValues(data);
667
682
 
668
- // If this is a remote phantom, and nothing is dirty, stage it so it actually gets saved to server and solidified
669
- 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) {
670
685
  selection[0].markStaged();
671
686
  useStaged = true;
672
687
  }
@@ -819,7 +834,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
819
834
  if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
820
835
  return EDITOR_MODE__VIEW;
821
836
  }
822
- 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) {
823
838
  return EDITOR_MODE__ADD;
824
839
  }
825
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
  };
@@ -126,6 +126,11 @@ export default function MetersEditor(props) {
126
126
  isEditable: false,
127
127
  isEditingEnabledInPlainEditor: true,
128
128
  },
129
+ {
130
+ name: 'meters__latest_meter_reading_date',
131
+ isEditable: false,
132
+ isEditingEnabledInPlainEditor: true,
133
+ },
129
134
  ...(includeExtendedCalculatedFields ? [
130
135
  {
131
136
  name: 'meters__latest_inspection_date',
@@ -18,6 +18,9 @@ import {
18
18
  EDIT,
19
19
  } from '../../Constants/Commands.js';
20
20
  import {
21
+ EDITOR_MODE__ADD,
22
+ EDITOR_MODE__EDIT,
23
+ EDITOR_MODE__VIEW,
21
24
  EDITOR_TYPE__SIDE,
22
25
  EDITOR_TYPE__SMART,
23
26
  } from '../../Constants/Editor.js';
@@ -48,6 +51,7 @@ function Viewer(props) {
48
51
  const {
49
52
  viewerCanDelete = false,
50
53
  items = [], // Columns, FieldSets, Fields, etc to define the form
54
+ isItemsCustomLayout = false,
51
55
  ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
52
56
  showAncillaryButtons = false,
53
57
  columnDefaults = {}, // defaults for each Column defined in items (above)
@@ -69,6 +73,8 @@ function Viewer(props) {
69
73
 
70
74
  // withEditor
71
75
  editorType,
76
+ getEditorMode,
77
+ editorMode,
72
78
  onEditMode,
73
79
  onClose,
74
80
  onDelete,
@@ -178,7 +184,7 @@ function Viewer(props) {
178
184
  let children;
179
185
  const style = {};
180
186
  if (type === 'Column') {
181
- const isEverythingInOneColumn = containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD;
187
+ const isEverythingInOneColumn = isItemsCustomLayout || containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD;
182
188
  if (itemPropsToPass.hasOwnProperty('flex')) {
183
189
  if (!isEverythingInOneColumn) {
184
190
  style.flex = itemPropsToPass.flex;
@@ -353,6 +359,14 @@ function Viewer(props) {
353
359
  buildAncillary = () => {
354
360
  const
355
361
  validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
362
+ parentEditorModeRaw = (getEditorMode && getEditorMode()) || editorMode || null,
363
+ parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
364
+ ? EDITOR_MODE__EDIT
365
+ : parentEditorModeRaw,
366
+ normalizedParentEditorMode =
367
+ parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
368
+ ? parentEditorMode
369
+ : null,
356
370
  components = [];
357
371
  setAncillaryButtons([]);
358
372
  if (validAncillaryItems.length) {
@@ -396,6 +410,8 @@ function Viewer(props) {
396
410
  }
397
411
 
398
412
  const
413
+ ancillaryEditorMode = itemPropsToPass.editorMode ?? normalizedParentEditorMode,
414
+ ancillaryInitialEditorMode = itemPropsToPass.initialEditorMode ?? ancillaryEditorMode ?? undefined,
399
415
  Element = getComponentFromType(type),
400
416
  element = <Element
401
417
  {...testProps('ancillary-' + type)}
@@ -407,6 +423,8 @@ function Viewer(props) {
407
423
  uniqueRepository={true}
408
424
  parent={self}
409
425
  {...itemPropsToPass}
426
+ editorMode={ancillaryEditorMode}
427
+ initialEditorMode={ancillaryInitialEditorMode}
410
428
  className={className}
411
429
  canRowsReorder={false}
412
430
  />;
@@ -472,7 +490,9 @@ function Viewer(props) {
472
490
  const
473
491
  showDeleteBtn = onDelete && viewerCanDelete,
474
492
  showCloseBtn = !isSideEditor && !isSmartEditor && onClose,
475
- 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;
476
496
  let additionalButtons = null,
477
497
  viewerComponents = null,
478
498
  ancillaryComponents = null,
@@ -594,8 +614,8 @@ function Viewer(props) {
594
614
  {buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
595
615
  </Toolbar>}
596
616
 
597
- {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Viewer-formComponents-HStack p-4 gap-4 justify-center">{viewerComponents}</HStack> : null}
598
- {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}
599
619
  <VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
600
620
  </ScrollView>
601
621