@onehat/ui 0.4.106 → 0.4.108

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.108",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -424,6 +424,8 @@ function Container(props) {
424
424
 
425
425
  componentProps._panel.isCollapsible = false;
426
426
  componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
427
+ componentProps.isCollapsible = false;
428
+ componentProps.isDisabled = isDisabled || isComponentsDisabled;
427
429
  componentProps.onLayout = debouncedOnLayout;
428
430
  centerComponent = cloneElement(center, componentProps);
429
431
  if (north) {
@@ -432,9 +434,11 @@ function Container(props) {
432
434
 
433
435
  componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
434
436
  componentProps._panel.className = 'h-full w-full ' + (north.props.className || '');
437
+ componentProps.isDisabled = !!north.props?.isDisabled || isDisabled || isComponentsDisabled;
438
+ componentProps.className = 'h-full w-full ' + (north.props.className || '');
435
439
  wrapperProps.onLayout = (e) => {
436
440
  const height = parseFloat(e.nativeEvent.layout.height);
437
- if (height && height !== northHeight) {
441
+ if (height && height !== getNorthHeight()) {
438
442
  setNorthHeight(height);
439
443
  }
440
444
  };
@@ -453,6 +457,9 @@ function Container(props) {
453
457
  componentProps._panel.collapseDirection = VERTICAL;
454
458
  componentProps._panel.isCollapsed = getNorthIsCollapsed();
455
459
  componentProps._panel.setIsCollapsed = setNorthIsCollapsed;
460
+ componentProps.collapseDirection = VERTICAL;
461
+ componentProps.isCollapsed = getNorthIsCollapsed();
462
+ componentProps.setIsCollapsed = setNorthIsCollapsed;
456
463
  if (isWeb && northIsResizable) {
457
464
  northSplitter = <Splitter
458
465
  mode={VERTICAL}
@@ -470,6 +477,8 @@ function Container(props) {
470
477
 
471
478
  componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
472
479
  componentProps._panel.className = 'h-full w-full ' + (south.props.className || '');
480
+ componentProps.isDisabled = !!south.props?.isDisabled || isDisabled || isComponentsDisabled;
481
+ componentProps.className = 'h-full w-full ' + (south.props.className || '');
473
482
  wrapperProps.onLayout = (e) => {
474
483
  const height = parseFloat(e.nativeEvent.layout.height);
475
484
  if (height && height !== getSouthHeight()) {
@@ -491,6 +500,9 @@ function Container(props) {
491
500
  componentProps._panel.collapseDirection = VERTICAL;
492
501
  componentProps._panel.isCollapsed = getSouthIsCollapsed();
493
502
  componentProps._panel.setIsCollapsed = setSouthIsCollapsed;
503
+ componentProps.collapseDirection = VERTICAL;
504
+ componentProps.isCollapsed = getSouthIsCollapsed();
505
+ componentProps.setIsCollapsed = setSouthIsCollapsed;
494
506
  if (isWeb && southIsResizable) {
495
507
  southSplitter = <Splitter
496
508
  mode={VERTICAL}
@@ -508,6 +520,8 @@ function Container(props) {
508
520
 
509
521
  componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
510
522
  componentProps._panel.className = 'h-full w-full ' + (east.props.className || '');
523
+ componentProps.isDisabled = !!east.props?.isDisabled || isDisabled || isComponentsDisabled;
524
+ componentProps.className = 'h-full w-full ' + (east.props.className || '');
511
525
  wrapperProps.onLayout = (e) => {
512
526
  const width = parseFloat(e.nativeEvent.layout.width);
513
527
  if (width && width !== getEastWidth()) {
@@ -529,6 +543,9 @@ function Container(props) {
529
543
  componentProps._panel.collapseDirection = HORIZONTAL;
530
544
  componentProps._panel.isCollapsed = getEastIsCollapsed();
531
545
  componentProps._panel.setIsCollapsed = setEastIsCollapsed;
546
+ componentProps.collapseDirection = HORIZONTAL;
547
+ componentProps.isCollapsed = getEastIsCollapsed();
548
+ componentProps.setIsCollapsed = setEastIsCollapsed;
532
549
  if (isWeb && eastIsResizable) {
533
550
  eastSplitter = <Splitter
534
551
  mode={HORIZONTAL}
@@ -546,6 +563,8 @@ function Container(props) {
546
563
 
547
564
  componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
548
565
  componentProps._panel.className = 'h-full w-full ' + (west.props.className || '');
566
+ componentProps.isDisabled = !!west.props?.isDisabled || isDisabled || isComponentsDisabled;
567
+ componentProps.className = 'h-full w-full ' + (west.props.className || '');
549
568
  wrapperProps.onLayout = (e) => {
550
569
  const width = parseFloat(e.nativeEvent.layout.width);
551
570
  if (width && width !== getWestWidth()) {
@@ -567,6 +586,9 @@ function Container(props) {
567
586
  componentProps._panel.collapseDirection = HORIZONTAL;
568
587
  componentProps._panel.isCollapsed = getWestIsCollapsed();
569
588
  componentProps._panel.setIsCollapsed = setWestIsCollapsed;
589
+ componentProps.collapseDirection = HORIZONTAL;
590
+ componentProps.isCollapsed = getWestIsCollapsed();
591
+ componentProps.setIsCollapsed = setWestIsCollapsed;
570
592
  if (isWeb && westIsResizable) {
571
593
  westSplitter = <Splitter
572
594
  mode={HORIZONTAL}
@@ -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"
@@ -375,9 +375,11 @@ const GridRow = forwardRef((props, ref) => {
375
375
  // TODO: incorporate better scrollbar formatting with
376
376
  // tailwind plugin 'tailwind-scrollbar' (already installed, just not yet used here)
377
377
 
378
+ const isEmptyCellValue = _.isNil(value) || value === '';
379
+
378
380
  let textClassName = clsx(
379
381
  'GridRow-TextNative',
380
- 'self-center',
382
+ isEmptyCellValue ? 'self-stretch' : 'self-center', // if the cell value is empty, stretch the cell to be full height
381
383
  areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
382
384
  '[&::-webkit-scrollbar]:h-2',
383
385
  '[&::-webkit-scrollbar-thumb]:bg-gray-300',
@@ -386,6 +388,14 @@ const GridRow = forwardRef((props, ref) => {
386
388
  styles.GRID_CELL_CLASSNAME,
387
389
  styles.GRID_ROW_MAX_HEIGHT_EXTRA,
388
390
  );
391
+ const textStyle = {
392
+ // userSelect: 'none',
393
+ ...colStyle,
394
+ ...(isEmptyCellValue ? { // if the cell value is empty, stretch the cell to be full height
395
+ height: '100%',
396
+ display: 'block',
397
+ } : {}),
398
+ };
389
399
  if (rowProps?._cell?.className) {
390
400
  textClassName += ' ' + rowProps._cell.className;
391
401
  }
@@ -395,16 +405,13 @@ const GridRow = forwardRef((props, ref) => {
395
405
  return <TextNative
396
406
  {...testProps('cell-' + config.fieldName)}
397
407
  key={key}
398
- style={{
399
- // userSelect: 'none',
400
- ...colStyle,
401
- }}
408
+ style={textStyle}
402
409
  numberOfLines={1}
403
410
  ellipsizeMode="head"
404
411
  className={textClassName}
405
412
  {...elementProps}
406
413
  {...propsToPass}
407
- >{value}</TextNative>;
414
+ >{isEmptyCellValue ? ' ' : value}</TextNative>;
408
415
  });
409
416
  } else {
410
417
  // TODO: if 'columnsConfig' is an object, parse its contents
@@ -450,6 +457,8 @@ const GridRow = forwardRef((props, ref) => {
450
457
  )}
451
458
  />}
452
459
  </>;
460
+ const hasCustomBgClass = rowProps?.className && /\bbg-/.test(rowProps.className);
461
+
453
462
  if (dropTargetRef) {
454
463
  rowContents = <HStack
455
464
  ref={dropTargetRef}
@@ -459,9 +468,7 @@ const GridRow = forwardRef((props, ref) => {
459
468
  'flex-1',
460
469
  'grow-1',
461
470
  )}
462
- style={{
463
- backgroundColor: bg,
464
- }}
471
+ style={hasCustomBgClass ? undefined : { backgroundColor: bg }}
465
472
  >{rowContents}</HStack>;
466
473
  }
467
474
 
@@ -485,9 +492,7 @@ const GridRow = forwardRef((props, ref) => {
485
492
  {...rowProps}
486
493
  key={hash}
487
494
  className={rowClassName}
488
- style={{
489
- backgroundColor: bg,
490
- }}
495
+ style={hasCustomBgClass ? undefined : { backgroundColor: bg }}
491
496
  >{rowContents}</HStackNative>;
492
497
  if (rowProps.tooltip) {
493
498
  row = <Tooltip
@@ -36,6 +36,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
36
36
  disableAdd = false,
37
37
  disableEdit = false,
38
38
  disableDelete = false,
39
+ enableMultiDelete = false, // deleting multiple records at once is opt-in only
39
40
  disableDuplicate = false,
40
41
  disableView = false,
41
42
  useRemoteDuplicate = false, // call specific copyToNew function on server, rather than simple duplicate on client
@@ -149,7 +150,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
149
150
  selection = getSelection();
150
151
  if (!_.isEmpty(formState?.dirtyFields) && newSelection !== selection && getEditorMode() === EDITOR_MODE__EDIT) {
151
152
  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)) {
153
+ } else if (selection && selection[0] && !selection[0].isDestroyed && selection[0].isPhantom) {
153
154
  confirm('This new record is unsaved. Are you sure you want to cancel editing? Changes will be lost.', async () => {
154
155
  await selection[0].delete();
155
156
  doIt();
@@ -216,7 +217,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
216
217
  if (!record || record.isDestroyed) {
217
218
  return false;
218
219
  }
219
- return !!(record.isPhantom || record.isRemotePhantom);
220
+ return !!record.isPhantom;
220
221
  },
221
222
  getIsEditorDisabledByParent = () => {
222
223
  return getIsParentSaveLocked();
@@ -441,7 +442,14 @@ export default function withEditor(WrappedComponent, isTree = false) {
441
442
  cb = args;
442
443
  }
443
444
  const selection = getSelection();
444
- if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
445
+ const hasTreeSelection = isTree || _.some(selection, (selected) => !!selected?.isTree);
446
+ if (
447
+ _.isEmpty(selection) ||
448
+ (_.isArray(selection) && (
449
+ selection[0]?.isDestroyed ||
450
+ (selection.length > 1 && (!enableMultiDelete || hasTreeSelection))
451
+ ))
452
+ ) {
445
453
  return;
446
454
  }
447
455
  if (onBeforeDelete) {
@@ -461,11 +469,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
461
469
  const
462
470
  isSingle = selection.length === 1,
463
471
  firstSelection = selection[0],
464
- isTree = firstSelection?.isTree,
465
- hasChildren = isTree ? firstSelection?.hasChildren : false,
472
+ isTreeNode = firstSelection?.isTree,
473
+ hasChildren = isTreeNode ? firstSelection?.hasChildren : false,
466
474
  isPhantom = firstSelection?.isPhantom;
467
475
 
468
- if (isSingle && isTree && hasChildren) {
476
+ if (isSingle && isTreeNode && hasChildren) {
469
477
  alert({
470
478
  title: 'Move up children?',
471
479
  message: 'The node you have selected for deletion has children. ' +
@@ -680,8 +688,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
680
688
  // just update this one entity
681
689
  selection[0].setValues(data);
682
690
 
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) {
691
+ // In ADD mode, if record is phantom and nothing is dirty, stage it so save() still submits and solidifies.
692
+ if (getEditorMode() === EDITOR_MODE__ADD && selection[0].isPhantom && !selection[0].isDirty) {
685
693
  selection[0].markStaged();
686
694
  useStaged = true;
687
695
  }
@@ -834,7 +842,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
834
842
  if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
835
843
  return EDITOR_MODE__VIEW;
836
844
  }
837
- if (selection.length === 1 && !selection[0].isDestroyed && (selection[0].isPhantom || selection[0].isRemotePhantom) && !disableAdd) {
845
+ if (selection.length === 1 && !selection[0].isDestroyed && selection[0].isPhantom && !disableAdd) {
838
846
  return EDITOR_MODE__ADD;
839
847
  }
840
848
  return selection.length ? EDITOR_MODE__EDIT : EDITOR_MODE__VIEW;
@@ -1022,6 +1030,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
1022
1030
  isEditor={true}
1023
1031
  userCanEdit={userCanEdit}
1024
1032
  userCanView={userCanView}
1033
+ enableMultiDelete={enableMultiDelete}
1025
1034
  disableAdd={disableAdd || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1026
1035
  disableEdit={disableEdit || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1027
1036
  disableDelete={disableDelete || isEditorDisabledByParent || isCrudBlockedByInheritedView}
@@ -97,7 +97,7 @@ export default function withFilters(WrappedComponent) {
97
97
 
98
98
  let title, type;
99
99
  if (propertyDef) {
100
- title = propertyDef.title;
100
+ title = propertyDef.filterTitle || propertyDef.title;
101
101
  type = propertyDef.filterType;
102
102
  } else if (modelAncillaryFilters[field]) {
103
103
  const ancillaryFilter = modelFilterTypes[field];
@@ -262,7 +262,7 @@ export default function withFilters(WrappedComponent) {
262
262
 
263
263
  if (!title) {
264
264
  const propertyDef = Repository.getSchema().getPropertyDefinition(field);
265
- title = propertyDef?.title;
265
+ title = propertyDef?.filterTitle || propertyDef?.title;
266
266
  }
267
267
 
268
268
  if (_.isString(filterType)) {
@@ -398,7 +398,10 @@ export default function withFilters(WrappedComponent) {
398
398
 
399
399
  // basic property filter
400
400
  const propertyDef = Repository.getSchema().getPropertyDefinition(filterField);
401
- data.push([ filterField, propertyDef?.title ]);
401
+ if (propertyDef?.isFilteringDisabled) {
402
+ return;
403
+ }
404
+ data.push([ filterField, propertyDef?.filterTitle || propertyDef?.title ]);
402
405
  });
403
406
 
404
407
  // sort by title
@@ -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
+ label: resolvedTitle,
183
+ };
171
184
  },
172
185
  buildValidator = () => {
173
186
  const propertyValidatorDefs = {};
@@ -215,6 +228,7 @@ export default function withPdfButtons(WrappedComponent) {
215
228
  parent={self}
216
229
  reference="chooseFieldsForm"
217
230
  editorType={EDITOR_TYPE__PLAIN}
231
+ checkIsEditingDisabled={false /* hack so layout looks right */}
218
232
  alert={alert}
219
233
  columnDefaults={{
220
234
  labelWidth: '100px',
@@ -75,6 +75,7 @@ export default function withPresetButtons(WrappedComponent) {
75
75
  disableAdd = !isEditor,
76
76
  disableEdit = !isEditor,
77
77
  disableDelete = !isEditor,
78
+ enableMultiDelete = false,
78
79
  disableView = isTree,
79
80
  disableCopy = isTree,
80
81
  disableDuplicate = !isEditor,
@@ -300,7 +301,7 @@ export default function withPresetButtons(WrappedComponent) {
300
301
  icon = Trash;
301
302
  if (isNoSelectorSelected() ||
302
303
  isEmptySelection() ||
303
- isMultiSelection() ||
304
+ (isMultiSelection() && (!enableMultiDelete || isTree)) ||
304
305
  isProtectedValue() ||
305
306
  (canRecordBeDeleted && !canRecordBeDeleted(selection))
306
307
  ) {
@@ -22,6 +22,7 @@ function ScreenHeader(props) {
22
22
  const {
23
23
  title,
24
24
  icon,
25
+ additionalButtons,
25
26
  info,
26
27
  _info = {},
27
28
  useModeIcons = false,
@@ -88,6 +89,7 @@ function ScreenHeader(props) {
88
89
  tooltip="To side editor"
89
90
  />
90
91
  </>}
92
+ {additionalButtons}
91
93
  {info &&
92
94
  <IconButton
93
95
  {...testProps('infoBtn')}
@@ -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