@onehat/ui 0.4.95 → 0.4.96

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.95",
3
+ "version": "0.4.96",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -368,7 +368,7 @@ function Container(props) {
368
368
  wrapperProps = {};
369
369
 
370
370
  componentProps.isDisabled = isDisabled || isComponentsDisabled;
371
- componentProps.className = (north.props.className || '') + ' h-full w-full';
371
+ componentProps.className = 'h-full w-full ' + (north.props.className || '');
372
372
  wrapperProps.onLayout = (e) => {
373
373
  const height = parseFloat(e.nativeEvent.layout.height);
374
374
  if (height && height !== northHeight) {
@@ -406,7 +406,7 @@ function Container(props) {
406
406
  wrapperProps = {};
407
407
 
408
408
  componentProps.isDisabled = isDisabled || isComponentsDisabled;
409
- componentProps.className = (south.props.className || '') + ' h-full w-full';
409
+ componentProps.className = 'h-full w-full ' + (south.props.className || '');
410
410
  wrapperProps.onLayout = (e) => {
411
411
  const height = parseFloat(e.nativeEvent.layout.height);
412
412
  if (height && height !== getSouthHeight()) {
@@ -444,7 +444,7 @@ function Container(props) {
444
444
  wrapperProps = {};
445
445
 
446
446
  componentProps.isDisabled = isDisabled || isComponentsDisabled;
447
- componentProps.className = (east.props.className || '') + ' h-full w-full';
447
+ componentProps.className = 'h-full w-full ' + (east.props.className || '');
448
448
  wrapperProps.onLayout = (e) => {
449
449
  const width = parseFloat(e.nativeEvent.layout.width);
450
450
  if (width && width !== getEastWidth()) {
@@ -482,7 +482,7 @@ function Container(props) {
482
482
  wrapperProps = {};
483
483
 
484
484
  componentProps.isDisabled = isDisabled || isComponentsDisabled;
485
- componentProps.className = (west.props.className || '') + ' h-full w-full';
485
+ componentProps.className = 'h-full w-full ' + (west.props.className || '');
486
486
  wrapperProps.onLayout = (e) => {
487
487
  const width = parseFloat(e.nativeEvent.layout.width);
488
488
  if (width && width !== getWestWidth()) {
@@ -1247,7 +1247,7 @@ function Form(props) {
1247
1247
 
1248
1248
  if (inArray(editorType, [EDITOR_TYPE__SIDE, EDITOR_TYPE__SMART, EDITOR_TYPE__WINDOWED]) &&
1249
1249
  isSingle && getEditorMode() === EDITOR_MODE__EDIT &&
1250
- (onBack || (onViewMode && !disableView))) {
1250
+ (onBack || onViewMode)) {
1251
1251
  modeHeader = <Toolbar>
1252
1252
  <HStack className="flex-1 items-center">
1253
1253
  {onBack &&
@@ -5,6 +5,7 @@ import {
5
5
  HStack,
6
6
  Pressable,
7
7
  // ScrollView,
8
+ Text,
8
9
  VStack,
9
10
  VStackNative,
10
11
  } from '@project-components/Gluestack';
@@ -187,7 +188,17 @@ function GridComponent(props) {
187
188
  canRowsReorder = false,
188
189
  canRowDrag, // optional fn to customize whether each row can be dragged
189
190
  canRowAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
190
- getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
191
+ dragProxyField,
192
+ getCustomDragProxy = (item, selection) => { // optional fn to render custom drag preview: (item, selection) => ReactElement
193
+ let selectionCount = selection?.length || 1,
194
+ displayText = dragProxyField ? item[dragProxyField] : (item.displayValue || 'Selected Row');
195
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
196
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
197
+ {selectionCount > 1 &&
198
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
199
+ }
200
+ </VStack>;
201
+ },
191
202
  dragPreviewOptions, // optional object for drag preview positioning options
192
203
  areRowsDragSource = false,
193
204
  rowDragSourceType,
@@ -260,6 +271,7 @@ function GridComponent(props) {
260
271
  } = props,
261
272
  styles = UiGlobals.styles,
262
273
  id = props.id || props.self?.path,
274
+ entities = Repository ? (Repository.isRemote ? Repository.entities : Repository.getEntitiesOnPage()) : data,
263
275
  localColumnsConfigKey = id && id + '-localColumnsConfig',
264
276
  [hasUnserializableColumns] = useState(() => {
265
277
  return !isSerializable(columnsConfig); // (runs only once, when the component is first created)
@@ -280,6 +292,8 @@ function GridComponent(props) {
280
292
  measuredRowsRef = useRef([]),
281
293
  footerToolbarRef = useRef(null),
282
294
  rowRefs = useRef([]),
295
+ previousEntitiesLength = useRef(0),
296
+ hasRemeasuredAfterRowsAppeared = useRef(false),
283
297
  [isInited, setIsInited] = useState(false),
284
298
  [isReady, setIsReady] = useState(false),
285
299
  [isLoading, setIsLoading] = useState(false),
@@ -1118,12 +1132,12 @@ function GridComponent(props) {
1118
1132
  // Keep the current estimated pageSize, just hide the loading overlay
1119
1133
  }
1120
1134
  },
1121
- adjustPageSizeToHeight = (containerHeight) => {
1135
+ adjustPageSizeToHeight = (containerHeight, forceRemeasure = false) => {
1122
1136
  if (!Repository || Repository.isDestroyed) { // This method gets delayed, so it's possible for Repository to have been destroyed. Check for this
1123
1137
  return;
1124
1138
  }
1125
1139
  if (DEBUG) {
1126
- console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight A`);
1140
+ console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight A forceRemeasure=${forceRemeasure}`);
1127
1141
  }
1128
1142
 
1129
1143
  let doAdjustment = autoAdjustPageSizeToHeight;
@@ -1135,11 +1149,11 @@ function GridComponent(props) {
1135
1149
  console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight A2 doAdjustment=${doAdjustment}, autoAdjustPageSizeToHeight=${autoAdjustPageSizeToHeight}, UiGlobals.autoAdjustPageSizeToHeight=${UiGlobals.autoAdjustPageSizeToHeight}, containerHeight=${containerHeight}`);
1136
1150
  }
1137
1151
 
1138
- // Only proceed if height changed significantly
1152
+ // Only proceed if height changed significantly or forced
1139
1153
  const
1140
1154
  heightChanged = Math.abs(containerHeight - lastMeasuredContainerHeight) > 5, // 5px tolerance
1141
1155
  isFirstMeasurement = lastMeasuredContainerHeight === 0;
1142
- if (containerHeight > 0 && (isFirstMeasurement || heightChanged)) {
1156
+ if (containerHeight > 0 && (isFirstMeasurement || heightChanged || forceRemeasure)) {
1143
1157
  if (editorType === EDITOR_TYPE__SIDE && getIsEditorShown()) {
1144
1158
  // When side editor is shown, skip adjustment to avoid layout thrashing
1145
1159
  console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight A4 height changed significantly, but side editor is shown, skipping remeasurement`);
@@ -1543,6 +1557,34 @@ function GridComponent(props) {
1543
1557
  }
1544
1558
  }, [autoAdjustPageSizeToHeight]);
1545
1559
 
1560
+ // Reset measurement when rows were first empty then became populated
1561
+ useEffect(() => {
1562
+ const
1563
+ currentLength = entities?.length || 0,
1564
+ wasEmpty = previousEntitiesLength.current === 0,
1565
+ isNowPopulated = currentLength > 0;
1566
+
1567
+ // Only remeasure the FIRST time rows appear after being empty
1568
+ if (autoAdjustPageSizeToHeight && wasEmpty && isNowPopulated && !hasRemeasuredAfterRowsAppeared.current) {
1569
+ // Rows just appeared for the first time - restart measurement cycle to use actual heights
1570
+ if (DEBUG) {
1571
+ console.log(`${getMeasurementPhase()}, useEffect 5 - rows appeared for first time, restarting measurement cycle`);
1572
+ }
1573
+ hasRemeasuredAfterRowsAppeared.current = true;
1574
+
1575
+ setMeasurementPhase(PHASES__INITIAL);
1576
+ setMeasuredRowHeight(null);
1577
+ measuredRowsRef.current = [];
1578
+
1579
+ // Trigger remeasurement with force flag since actual rows are now available
1580
+ if (lastMeasuredContainerHeight > 0) {
1581
+ adjustPageSizeToHeight(lastMeasuredContainerHeight, true);
1582
+ }
1583
+ }
1584
+
1585
+ previousEntitiesLength.current = currentLength;
1586
+ }, [entities?.length, autoAdjustPageSizeToHeight]);
1587
+
1546
1588
  if (canUser && !canUser('view')) {
1547
1589
  return <Unauthorized />;
1548
1590
  }
@@ -1579,7 +1621,6 @@ function GridComponent(props) {
1579
1621
  }
1580
1622
 
1581
1623
  // Actual data to show in the grid
1582
- const entities = Repository ? (Repository.isRemote ? Repository.entities : Repository.getEntitiesOnPage()) : data;
1583
1624
  let rowData = [...entities]; // don't use the original array, make a new one so alterations to it are temporary
1584
1625
  if (showHeaders) {
1585
1626
  rowData.unshift({ id: 'headerRow' });
@@ -61,6 +61,7 @@ function withAlert(WrappedComponent) {
61
61
  'flex-1',
62
62
  'items-start',
63
63
  'justify-center',
64
+ 'p-4',
64
65
  )}>
65
66
  <Text className={clsx(
66
67
  'withAlert-Text',
@@ -164,7 +165,7 @@ function withAlert(WrappedComponent) {
164
165
  }),
165
166
  onOk: () => hideModal(),
166
167
  canClose: true,
167
- h: 200,
168
+ h: 250,
168
169
  w: 400,
169
170
  whichModal: 'info',
170
171
  });
@@ -30,6 +30,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
30
30
  userCanView = true,
31
31
  canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
32
32
  canProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
33
+ canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
34
+ canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
33
35
  disableAdd = false,
34
36
  disableEdit = false,
35
37
  disableDelete = false,
@@ -87,6 +89,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
87
89
  editorModeRef = useRef(initialEditorMode),
88
90
  isIgnoreNextSelectionChangeRef = useRef(false),
89
91
  isEditorShownRef = useRef(false),
92
+ canEditorBeInEditModeRef = useRef(true), // whether the editor is allowed to be in edit mode based on canRecordBeEdited
90
93
  [currentRecord, setCurrentRecord] = useState(null),
91
94
  [isAdding, setIsAdding] = useState(false),
92
95
  [isSaving, setIsSaving] = useState(false),
@@ -108,6 +111,13 @@ export default function withEditor(WrappedComponent, isTree = false) {
108
111
  getIsEditorShown = () => {
109
112
  return isEditorShownRef.current;
110
113
  },
114
+ setCanEditorBeInEditMode = (bool) => {
115
+ canEditorBeInEditModeRef.current = bool;
116
+ forceUpdate();
117
+ },
118
+ getCanEditorBeInEditMode = () => {
119
+ return canEditorBeInEditModeRef.current;
120
+ },
111
121
  setIsWaitModalShown = (bool) => {
112
122
  const
113
123
  dispatch = UiGlobals.redux?.dispatch,
@@ -624,6 +634,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
624
634
  });
625
635
  },
626
636
  calculateEditorMode = () => {
637
+ if (!getCanEditorBeInEditMode()) { // this is a result of canRecordBeEdited returning false
638
+ return EDITOR_MODE__VIEW;
639
+ }
627
640
 
628
641
  let isIgnoreNextSelectionChange = getIsIgnoreNextSelectionChange(),
629
642
  doStayInEditModeOnSelectionChange = stayInEditModeOnSelectionChange;
@@ -691,7 +704,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
691
704
  useEffect(() => {
692
705
 
693
706
  if (editorType === EDITOR_TYPE__SIDE) {
694
- if (selection?.length) { // || isAdding
707
+ if (selection?.length) { // || isAdding
695
708
  // there is a selection, so show the editor
696
709
  setIsEditorShown(true);
697
710
  } else {
@@ -700,6 +713,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
700
713
  }
701
714
  }
702
715
 
716
+ if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
717
+ setCanEditorBeInEditMode(false);
718
+ } else {
719
+ setCanEditorBeInEditMode(true);
720
+ }
703
721
  setEditorMode(calculateEditorMode());
704
722
  setLastSelection(selection);
705
723
 
@@ -749,8 +767,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
749
767
  setIsEditorShown={setIsEditorShown}
750
768
  setIsIgnoreNextSelectionChange={setIsIgnoreNextSelectionChange}
751
769
  onAdd={(!userCanEdit || disableAdd) ? null : doAdd}
752
- onEdit={(!userCanEdit || disableEdit) ? null : doEdit}
753
- onDelete={(!userCanEdit || disableDelete) ? null : doDelete}
770
+ onEdit={(!userCanEdit || disableEdit || (canRecordBeEdited && !canRecordBeEdited(selection))) ? null : doEdit}
771
+ onDelete={(!userCanEdit || disableDelete || (canRecordBeDeleted && !canRecordBeDeleted(selection))) ? null : doDelete}
754
772
  onView={doView}
755
773
  onDuplicate={doDuplicate}
756
774
  onEditorSave={doEditorSave}
@@ -48,7 +48,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
48
48
  }
49
49
 
50
50
  const {
51
- // extract and pass
51
+ // for local use
52
52
  contextMenuItems = [],
53
53
  additionalToolbarButtons = [],
54
54
  useUploadDownload = false,
@@ -59,18 +59,18 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
59
59
  downloadHeaders,
60
60
  downloadParams,
61
61
  onChangeColumnsConfig,
62
- canRecordBeEdited,
63
- canRecordBeDeleted,
64
- canRecordBeDuplicated,
65
62
  ...propsToPass
66
63
  } = props,
67
64
  {
68
- // for local use
65
+ // extract and pass down
69
66
  isEditor = false,
70
67
  isTree = false,
71
68
  canDeleteRootNode = false,
72
69
  isSideEditor = false,
73
70
  canEditorViewOnly = false,
71
+ canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
72
+ canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
73
+ canRecordBeDuplicated, // fn(selection) returns bool on if the current record(s) can be duplicated
74
74
  disableAdd = !isEditor,
75
75
  disableEdit = !isEditor,
76
76
  disableDelete = !isEditor,
@@ -12,6 +12,7 @@ export function GridPanel(props) {
12
12
  isEditor = false,
13
13
  editorType = EDITOR_TYPE__WINDOWED,
14
14
  _panel = {},
15
+ _grid = {},
15
16
  } = props;
16
17
 
17
18
  let WhichGrid = Grid;
@@ -30,7 +31,7 @@ export function GridPanel(props) {
30
31
  }
31
32
 
32
33
  return <Panel {...props} {..._panel}>
33
- <WhichGrid {...props} />
34
+ <WhichGrid {...props} {..._grid} />
34
35
  </Panel>;
35
36
  }
36
37
 
@@ -3,8 +3,11 @@ import Panel from './Panel.js';
3
3
 
4
4
 
5
5
  export default function TabPanel(props) {
6
- const panelProps = props._panel || {};
7
- return <Panel className="w-full flex" {...panelProps}>
8
- <TabBar {...props} {...props._tab} />
6
+ const {
7
+ _panel = {},
8
+ _tab = {},
9
+ } = props;
10
+ return <Panel className="w-full flex" {..._panel}>
11
+ <TabBar {...props} {..._tab} />
9
12
  </Panel>;
10
13
  }
@@ -11,6 +11,7 @@ export function TreePanel(props) {
11
11
  isEditor = false,
12
12
  editorType = EDITOR_TYPE__WINDOWED,
13
13
  _panel = {},
14
+ _tree = {},
14
15
  } = props;
15
16
 
16
17
  let WhichTree = Tree;
@@ -26,7 +27,7 @@ export function TreePanel(props) {
26
27
  }
27
28
 
28
29
  return <Panel {..._panel}>
29
- <WhichTree {...props} {..._panel} />
30
+ <WhichTree {...props} {..._tree} />
30
31
  </Panel>;
31
32
  }
32
33
 
@@ -3,6 +3,7 @@ import {
3
3
  HStack,
4
4
  Pressable,
5
5
  ScrollView,
6
+ Text,
6
7
  VStack,
7
8
  VStackNative,
8
9
  } from '@project-components/Gluestack';
@@ -133,7 +134,17 @@ function TreeComponent(props) {
133
134
  canNodeMoveInternally, // optional fn to customize whether each node can be dragged INternally
134
135
  canNodeMoveExternally, // optional fn to customize whether each node can be dragged EXternally
135
136
  canNodeAcceptDrop, // optional fn to customize whether each node can accept a dropped item: (targetItem, draggedItem) => boolean
136
- getCustomDragProxy, // optional fn to render custom drag preview: (item, selection) => ReactElement
137
+ dragProxyField,
138
+ getCustomDragProxy = (item, selection) => { // optional fn to render custom drag preview: (item, selection) => ReactElement
139
+ let selectionCount = selection?.length || 1,
140
+ displayText = dragProxyField ? item[dragProxyField] : (item.displayValue || 'Selected TreeNode');
141
+ return <VStack className="bg-white border border-gray-300 rounded-lg p-3 shadow-lg max-w-[200px]">
142
+ <Text className="font-semibold text-gray-800">{displayText}</Text>
143
+ {selectionCount > 1 &&
144
+ <Text className="text-sm text-gray-600">(+{selectionCount -1} more item{selectionCount > 2 ? 's' : ''})</Text>
145
+ }
146
+ </VStack>;
147
+ },
137
148
  dragPreviewOptions, // optional object for drag preview positioning options
138
149
  areNodesDragSource = false,
139
150
  nodeDragSourceType,
@@ -543,26 +543,27 @@ function Viewer(props) {
543
543
  )}
544
544
  >
545
545
  {scrollToTopAnchor}
546
- {canEdit && onEditMode &&
547
- <Toolbar className="justify-end">
548
- <HStack className="flex-1 items-center">
549
- <Text className="text-[20px] ml-1 text-grey-500">View Mode</Text>
550
- </HStack>
551
- {(!canUser || canUser(EDIT)) &&
552
- <Button
553
- {...testProps('toEditBtn')}
554
- key="editBtn"
555
- onPress={onEditMode}
556
- icon={Pencil}
557
- _icon={{
558
- size: 'sm',
559
- className: 'text-white'
560
- }}
561
- className="text-white"
562
- text="To Edit"
563
- tooltip="Switch to Edit Mode"
564
- />}
565
- </Toolbar>}
546
+
547
+ <Toolbar className="justify-end">
548
+ <HStack className="flex-1 items-center">
549
+ <Text className="text-[20px] ml-1 text-grey-500">View Mode</Text>
550
+ </HStack>
551
+ {onEditMode && (!canUser || canUser(EDIT)) &&
552
+ <Button
553
+ {...testProps('toEditBtn')}
554
+ key="editBtn"
555
+ onPress={onEditMode}
556
+ icon={Pencil}
557
+ _icon={{
558
+ size: 'sm',
559
+ className: 'text-white'
560
+ }}
561
+ className="text-white"
562
+ text="To Edit"
563
+ tooltip="Switch to Edit Mode"
564
+ isDisabled={!canEdit}
565
+ />}
566
+ </Toolbar>
566
567
 
567
568
  {!_.isEmpty(additionalButtons) &&
568
569
  <Toolbar className="justify-end flex-wrap gap-2">