@onehat/ui 0.4.102 → 0.4.104

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.
Files changed (56) hide show
  1. package/.github/copilot-instructions.md.bak.20260307094051 +65 -0
  2. package/package.json +1 -1
  3. package/src/Components/Accordion/Accordion.js +65 -6
  4. package/src/Components/Container/Container.js +10 -4
  5. package/src/Components/Form/Field/Combo/Combo.js +8 -2
  6. package/src/Components/Form/Form.js +17 -9
  7. package/src/Components/Grid/Grid.js +234 -154
  8. package/src/Components/Grid/GridRow.js +5 -1
  9. package/src/Components/Hoc/Secondary/withSecondaryEditor.js +18 -1
  10. package/src/Components/Hoc/withEditor.js +18 -1
  11. package/src/Components/Hoc/withPdfButtons.js +3 -0
  12. package/src/Components/Hoc/withPresetButtons.js +20 -6
  13. package/src/Components/Icons/ArrowsLeftRight.js +10 -0
  14. package/src/Components/Icons/Bar.js +10 -0
  15. package/src/Components/Icons/Box.js +11 -0
  16. package/src/Components/Icons/BoxOpen.js +11 -0
  17. package/src/Components/Icons/Bucket.js +10 -0
  18. package/src/Components/Icons/Bump.js +21 -0
  19. package/src/Components/Icons/Calculator.js +12 -0
  20. package/src/Components/Icons/Dots.js +20 -0
  21. package/src/Components/Icons/Fleets.js +26 -0
  22. package/src/Components/Icons/Microchip.js +12 -0
  23. package/src/Components/Icons/Num1.js +10 -0
  24. package/src/Components/Icons/Num2.js +10 -0
  25. package/src/Components/Icons/Num3.js +10 -0
  26. package/src/Components/Icons/Num4.js +10 -0
  27. package/src/Components/Icons/OilCan.js +11 -0
  28. package/src/Components/Icons/Operations.js +10 -0
  29. package/src/Components/Icons/OverduePms.js +10 -0
  30. package/src/Components/Icons/SackDollar.js +11 -0
  31. package/src/Components/Icons/ShortBar.js +15 -0
  32. package/src/Components/Icons/Tower.js +10 -0
  33. package/src/Components/Icons/UpcomingPms.js +10 -0
  34. package/src/Components/Layout/ScreenHeader.js +35 -3
  35. package/src/Components/Layout/SetupButton.js +31 -0
  36. package/src/Components/Layout/UserIndicator.js +35 -0
  37. package/src/Components/Panel/Panel.js +37 -9
  38. package/src/Components/Pms/Editor/BumpPmsEditor.js +9 -0
  39. package/src/Components/Pms/Editor/MetersEditor.js +173 -0
  40. package/src/Components/Pms/Editor/PmEventsEditor.js +291 -0
  41. package/src/Components/Pms/Grid/UpcomingPmsGrid.js +569 -0
  42. package/src/Components/Pms/Layout/TreeSpecific/MakeTreeSelection.js +11 -0
  43. package/src/Components/Pms/Layout/TreeSpecific/TreeSpecific.js +30 -0
  44. package/src/Components/Pms/Modals/BulkAssignTechnician.js +104 -0
  45. package/src/Components/Pms/Screens/PmsManager.js +136 -0
  46. package/src/Components/Pms/Window/BumpPmsEditorWindow.js +25 -0
  47. package/src/Components/Screens/Manager.js +5 -1
  48. package/src/Components/Toolbar/PaginationToolbar.js +5 -3
  49. package/src/Components/Tree/Tree.js +15 -6
  50. package/src/Components/Viewer/PmCalcDebugViewer.js +164 -146
  51. package/src/Components/Viewer/TextWithLinks.js +9 -1
  52. package/src/Components/Viewer/Viewer.js +43 -33
  53. package/src/Constants/PmSchedules.js +1 -0
  54. package/src/Functions/buildAdditionalButtons.js +5 -0
  55. package/src/Functions/flatten.js +39 -0
  56. package/src/Functions/verifyCanCrudPmEvents.js +33 -0
@@ -296,6 +296,11 @@ function GridComponent(props) {
296
296
  footerToolbarRef = useRef(null),
297
297
  rowRefs = useRef([]),
298
298
  previousEntitiesLength = useRef(0),
299
+ paginationSelectionGuardRef = useRef({
300
+ source: null,
301
+ pending: new Set(),
302
+ expiresAt: 0,
303
+ }),
299
304
  hasRemeasuredAfterRowsAppeared = useRef(false),
300
305
  [isInited, setIsInited] = useState(false),
301
306
  [isReady, setIsReady] = useState(false),
@@ -305,20 +310,6 @@ function GridComponent(props) {
305
310
  showRowHandle = showSelectHandle || areRowsDragSource || (canRowsReorder && isReorderMode),
306
311
  rowLongPressDelay = rowLongPressDelayMs ?? ((areRowsDragSource || canRowsReorder) ? 800 : undefined),
307
312
  [lastMeasuredContainerHeight, setLastMeasuredContainerHeight] = useState(0),
308
- getMeasurementPhase = () => {
309
- return measurementPhaseRaw.current;
310
- },
311
- setMeasurementPhase = (phase) => {
312
- measurementPhaseRaw.current = phase;
313
- forceUpdate();
314
- },
315
- getMeasuredRowHeight = () => {
316
- return measuredRowHeightRaw.current;
317
- },
318
- setMeasuredRowHeight = (height) => {
319
- measuredRowHeightRaw.current = height;
320
- forceUpdate();
321
- },
322
313
  getIsExpanded = (index) => {
323
314
  return !!expandedRowsRef.current[index];
324
315
  },
@@ -427,13 +418,15 @@ function GridComponent(props) {
427
418
  }
428
419
  },
429
420
  getFooterToolbarItems = () => {
430
- // Process additionalToolbarButtons to evaluate getIsButtonDisabled functions
421
+ // Process additionalToolbarButtons to evaluate functions
431
422
  const processedButtons = _.map(additionalToolbarButtons, (config) => {
432
423
  const processedConfig = { ...config };
433
- // If the button has an getIsButtonDisabled function, evaluate it with current selection
434
424
  if (_.isFunction(config.getIsButtonDisabled)) {
435
425
  processedConfig.isDisabled = config.getIsButtonDisabled(selection);
436
426
  }
427
+ if (_.isFunction(config.getText)) {
428
+ processedConfig.text = config.getText(selection);
429
+ }
437
430
  return processedConfig;
438
431
  });
439
432
  const items = _.map(processedButtons, (config, ix) => getIconButtonFromConfig(config, ix, self));
@@ -596,6 +589,8 @@ function GridComponent(props) {
596
589
  'flex-row',
597
590
  'grow',
598
591
  'max-h-[80px]',
592
+ 'focus:outline-none', // hide the focus outline
593
+ 'focus-visible:outline-none',
599
594
  )}
600
595
  >
601
596
  {({
@@ -1018,6 +1013,151 @@ function GridComponent(props) {
1018
1013
  marker.remove();
1019
1014
  cachedDragElements.current = null;
1020
1015
  },
1016
+ applySelectorSelected = () => {
1017
+ if (disableSelectorSelected || !selectorId) {
1018
+ return
1019
+ }
1020
+
1021
+ if (previousSelectorId.current && selectorId !== previousSelectorId.current) {
1022
+ Repository.pauseEvents();
1023
+ Repository.clearFilters(previousSelectorId.current);
1024
+ Repository.resumeEvents();
1025
+ }
1026
+ previousSelectorId.current = selectorId;
1027
+
1028
+ let value = null;
1029
+ if (selectorSelected) {
1030
+ value = selectorSelected[selectorSelectedField];
1031
+ }
1032
+ if (noSelectorMeansNoResults && _.isEmpty(selectorSelected)) {
1033
+ value = 'NO_MATCHES';
1034
+ }
1035
+
1036
+ Repository.filter(selectorId, value, false); // false so it doesn't clear existing filters
1037
+ },
1038
+ onGridKeyDown = (e) => {
1039
+ if (isInlineEditorShown) {
1040
+ return;
1041
+ }
1042
+ if (disableWithSelection) {
1043
+ return;
1044
+ }
1045
+ const {
1046
+ shiftKey = false,
1047
+ } = e;
1048
+ if (selectionMode === SELECTION_MODE_MULTI && shiftKey) {
1049
+ switch(e.key) {
1050
+ case 'ArrowDown':
1051
+ e.preventDefault();
1052
+ addNextToSelection();
1053
+ break;
1054
+ case 'ArrowUp':
1055
+ e.preventDefault();
1056
+ addPrevToSelection();
1057
+ break;
1058
+ }
1059
+ } else {
1060
+ // selectionMode is SELECTION_MODE_SINGLE
1061
+ switch(e.key) {
1062
+ case 'Enter':
1063
+ // NOTE: This is never being reached.
1064
+ // The event is getting captured somwhere else,
1065
+ // but I can't find where.
1066
+ // e.preventDefault();
1067
+
1068
+ // launch inline or windowed editor
1069
+ // const p = props;
1070
+ // debugger;
1071
+ break;
1072
+ case 'ArrowDown':
1073
+ e.preventDefault();
1074
+ selectNext();
1075
+ break;
1076
+ case 'ArrowUp':
1077
+ e.preventDefault();
1078
+ selectPrev();
1079
+ break;
1080
+ }
1081
+ }
1082
+ },
1083
+ showColumnsSelector = () => {
1084
+ const
1085
+ modalItems = _.map(localColumnsConfig, (config, ix) => {
1086
+ return {
1087
+ name: config.id,
1088
+ label: config.header,
1089
+ type: config.isHidable ? 'Checkbox' : 'Text',
1090
+ isEditable: config.isHidable ?? false,
1091
+ };
1092
+ }),
1093
+ startingValues = (() => {
1094
+ const startingValues = {};
1095
+ _.each(localColumnsConfig, (config) => {
1096
+ const value = !config.isHidden; // checkbox implies to show it, so flip the polarity
1097
+ startingValues[config.id] = config.isHidable ? value : 'Always shown';
1098
+ });
1099
+ return startingValues;
1100
+ })();
1101
+
1102
+ showModal({
1103
+ title: 'Column Selector',
1104
+ includeReset: true,
1105
+ includeCancel: true,
1106
+ h: 800,
1107
+ w: styles.FORM_STACK_ROW_THRESHOLD + 10,
1108
+ body: <Form
1109
+ editorType={EDITOR_TYPE__PLAIN}
1110
+ columnDefaults={{
1111
+ labelWidth: '250px',
1112
+ }}
1113
+ items={[
1114
+ {
1115
+ name: 'instructions',
1116
+ type: 'DisplayField',
1117
+ text: 'Please select which columns to show in the grid.',
1118
+ className: 'mb-3',
1119
+ },
1120
+ {
1121
+ type: 'FieldSet',
1122
+ title: 'Columns',
1123
+ reference: 'columns',
1124
+ showToggleAllCheckbox: true,
1125
+ items: [
1126
+ ...modalItems,
1127
+ ],
1128
+ }
1129
+ ]}
1130
+ startingValues={startingValues}
1131
+ onSave={(values)=> {
1132
+ hideModal();
1133
+
1134
+ const newColumnsConfig = _.cloneDeep(localColumnsConfig);
1135
+ _.each(newColumnsConfig, (config, ix) => {
1136
+ if (config.isHidable) {
1137
+ newColumnsConfig[ix].isHidden = !values[config.id]; // checkbox implies to show it, so flip the polarity
1138
+ }
1139
+ });
1140
+ setLocalColumnsConfig(newColumnsConfig);
1141
+ }}
1142
+ />,
1143
+ });
1144
+ },
1145
+
1146
+ // These methods relate to auto-pageSize measurement and adjustment:
1147
+ getMeasurementPhase = () => {
1148
+ return measurementPhaseRaw.current;
1149
+ },
1150
+ setMeasurementPhase = (phase) => {
1151
+ measurementPhaseRaw.current = phase;
1152
+ forceUpdate();
1153
+ },
1154
+ getMeasuredRowHeight = () => {
1155
+ return measuredRowHeightRaw.current;
1156
+ },
1157
+ setMeasuredRowHeight = (height) => {
1158
+ measuredRowHeightRaw.current = height;
1159
+ forceUpdate();
1160
+ },
1021
1161
  calculatePageSize = (containerHeight, useActualMeasurements = false) => {
1022
1162
  if (DEBUG) {
1023
1163
  console.log(`${getMeasurementPhase()}, calculatePageSize A containerHeight=${containerHeight}, useActualMeasurements=${useActualMeasurements}, measuredRowHeight=${getMeasuredRowHeight()}`);
@@ -1202,6 +1342,7 @@ function GridComponent(props) {
1202
1342
  if (DEBUG) {
1203
1343
  console.log(`${getMeasurementPhase()}, applyMeasuredRowHeight B Repository.setPageSize(${newPageSize})`);
1204
1344
  }
1345
+ startAutoPageSizeSelectionGuard();
1205
1346
  Repository.setPageSize(newPageSize);
1206
1347
  }
1207
1348
  }
@@ -1261,6 +1402,7 @@ function GridComponent(props) {
1261
1402
  if (DEBUG) {
1262
1403
  console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight D Repository.setPageSize(${pageSize})`);
1263
1404
  }
1405
+ startAutoPageSizeSelectionGuard();
1264
1406
  Repository.setPageSize(pageSize);
1265
1407
  }
1266
1408
  }
@@ -1279,136 +1421,51 @@ function GridComponent(props) {
1279
1421
  }
1280
1422
  },
1281
1423
  debouncedAdjustPageSizeToHeight = useCallback(_.debounce(adjustPageSizeToHeight, 200), []),
1282
- applySelectorSelected = () => {
1283
- if (disableSelectorSelected || !selectorId) {
1284
- return
1285
- }
1286
-
1287
- if (previousSelectorId.current && selectorId !== previousSelectorId.current) {
1288
- Repository.pauseEvents();
1289
- Repository.clearFilters(previousSelectorId.current);
1290
- Repository.resumeEvents();
1291
- }
1292
- previousSelectorId.current = selectorId;
1293
1424
 
1294
- let value = null;
1295
- if (selectorSelected) {
1296
- value = selectorSelected[selectorSelectedField];
1297
- }
1298
- if (noSelectorMeansNoResults && _.isEmpty(selectorSelected)) {
1299
- value = 'NO_MATCHES';
1300
- }
1301
-
1302
- Repository.filter(selectorId, value, false); // false so it doesn't clear existing filters
1425
+ // These methods guard for selection/pagination interaction
1426
+ // (If user makes a selection during the auto-pageSize adjustment,
1427
+ // we don't want to clear the selection if the current selection
1428
+ // would still be valid with the new page size):
1429
+ clearPaginationSelectionGuard = () => {
1430
+ paginationSelectionGuardRef.current = {
1431
+ source: null,
1432
+ pending: new Set(),
1433
+ expiresAt: 0,
1434
+ };
1303
1435
  },
1304
- onGridKeyDown = (e) => {
1305
- if (isInlineEditorShown) {
1436
+ startAutoPageSizeSelectionGuard = () => {
1437
+ if (disableWithSelection || !deselectAll) {
1306
1438
  return;
1307
1439
  }
1308
- if (disableWithSelection) {
1309
- return;
1440
+ paginationSelectionGuardRef.current = {
1441
+ source: 'autoPageSize',
1442
+ pending: new Set(['changePage', 'changePageSize',]),
1443
+ expiresAt: Date.now() + 5000,
1444
+ };
1445
+ },
1446
+ consumePaginationSelectionGuard = (eventType) => {
1447
+ const guard = paginationSelectionGuardRef.current;
1448
+ if (guard.source !== 'autoPageSize') {
1449
+ return false;
1310
1450
  }
1311
- const {
1312
- shiftKey = false,
1313
- } = e;
1314
- if (selectionMode === SELECTION_MODE_MULTI && shiftKey) {
1315
- switch(e.key) {
1316
- case 'ArrowDown':
1317
- e.preventDefault();
1318
- addNextToSelection();
1319
- break;
1320
- case 'ArrowUp':
1321
- e.preventDefault();
1322
- addPrevToSelection();
1323
- break;
1324
- }
1325
- } else {
1326
- // selectionMode is SELECTION_MODE_SINGLE
1327
- switch(e.key) {
1328
- case 'Enter':
1329
- // NOTE: This is never being reached.
1330
- // The event is getting captured somwhere else,
1331
- // but I can't find where.
1332
- // e.preventDefault();
1333
-
1334
- // launch inline or windowed editor
1335
- // const p = props;
1336
- // debugger;
1337
- break;
1338
- case 'ArrowDown':
1339
- e.preventDefault();
1340
- selectNext();
1341
- break;
1342
- case 'ArrowUp':
1343
- e.preventDefault();
1344
- selectPrev();
1345
- break;
1346
- }
1451
+ if (Date.now() > guard.expiresAt) {
1452
+ clearPaginationSelectionGuard();
1453
+ return false;
1454
+ }
1455
+ if (!guard.pending.has(eventType)) {
1456
+ return false;
1347
1457
  }
1348
- },
1349
- showColumnsSelector = () => {
1350
- const
1351
- modalItems = _.map(localColumnsConfig, (config, ix) => {
1352
- return {
1353
- name: config.id,
1354
- label: config.header,
1355
- type: config.isHidable ? 'Checkbox' : 'Text',
1356
- isEditable: config.isHidable ?? false,
1357
- };
1358
- }),
1359
- startingValues = (() => {
1360
- const startingValues = {};
1361
- _.each(localColumnsConfig, (config) => {
1362
- const value = !config.isHidden; // checkbox implies to show it, so flip the polarity
1363
- startingValues[config.id] = config.isHidable ? value : 'Always shown';
1364
- });
1365
- return startingValues;
1366
- })();
1367
-
1368
- showModal({
1369
- title: 'Column Selector',
1370
- includeReset: true,
1371
- includeCancel: true,
1372
- h: 800,
1373
- w: styles.FORM_STACK_ROW_THRESHOLD + 10,
1374
- body: <Form
1375
- editorType={EDITOR_TYPE__PLAIN}
1376
- columnDefaults={{
1377
- labelWidth: '250px',
1378
- }}
1379
- items={[
1380
- {
1381
- name: 'instructions',
1382
- type: 'DisplayField',
1383
- text: 'Please select which columns to show in the grid.',
1384
- className: 'mb-3',
1385
- },
1386
- {
1387
- type: 'FieldSet',
1388
- title: 'Columns',
1389
- reference: 'columns',
1390
- showToggleAllCheckbox: true,
1391
- items: [
1392
- ...modalItems,
1393
- ],
1394
- }
1395
- ]}
1396
- startingValues={startingValues}
1397
- onSave={(values)=> {
1398
- hideModal();
1399
1458
 
1400
- const newColumnsConfig = _.cloneDeep(localColumnsConfig);
1401
- _.each(newColumnsConfig, (config, ix) => {
1402
- if (config.isHidable) {
1403
- newColumnsConfig[ix].isHidden = !values[config.id]; // checkbox implies to show it, so flip the polarity
1404
- }
1405
- });
1406
- setLocalColumnsConfig(newColumnsConfig);
1407
- }}
1408
- />,
1409
- });
1459
+ guard.pending.delete(eventType);
1460
+
1461
+ if (!guard.pending.size) {
1462
+ clearPaginationSelectionGuard();
1463
+ }
1464
+
1465
+ return true;
1410
1466
  };
1411
1467
 
1468
+
1412
1469
  if (forceLoadOnRender && disableLoadOnRender) {
1413
1470
  throw new Error('incompatible config! forceLoadOnRender and disableLoadOnRender cannot both be true');
1414
1471
  }
@@ -1531,7 +1588,27 @@ function GridComponent(props) {
1531
1588
  // set up @onehat/data repository
1532
1589
  const
1533
1590
  setTrue = () => setIsLoading(true),
1534
- setFalse = () => setIsLoading(false),
1591
+ setFalse = () => {
1592
+ setIsLoading(false);
1593
+ clearPaginationSelectionGuard();
1594
+ },
1595
+ onPaginationEvent = (eventType) => {
1596
+ if (consumePaginationSelectionGuard(eventType)) {
1597
+ return;
1598
+ }
1599
+ if (!disableWithSelection && deselectAll) {
1600
+ deselectAll();
1601
+ }
1602
+ if (eventType === 'changePage' && showRowExpander) {
1603
+ expandedRowsRef.current = {}; // clear expanded rows
1604
+ }
1605
+ },
1606
+ onChangePageSize = () => {
1607
+ if (disableWithSelection || !deselectAll) {
1608
+ return;
1609
+ }
1610
+ onPaginationEvent('changePageSize');
1611
+ },
1535
1612
  onChangeFilters = () => {
1536
1613
  if (DEBUG) {
1537
1614
  console.log('onChangeFilters, reload and re-measure');
@@ -1546,20 +1623,16 @@ function GridComponent(props) {
1546
1623
  }
1547
1624
  },
1548
1625
  onChangePage = () => {
1549
- if (showRowExpander) {
1550
- expandedRowsRef.current = {}; // clear expanded rows
1551
- }
1626
+ onPaginationEvent('changePage');
1552
1627
  };
1553
1628
 
1554
1629
  Repository.on('beforeLoad', setTrue);
1555
1630
  Repository.on('load', setFalse);
1556
- if (!disableWithSelection) {
1557
- Repository.ons(['changePage', 'changePageSize',], deselectAll);
1558
- }
1631
+ Repository.on('changePage', onChangePage);
1632
+ Repository.on('changePageSize', onChangePageSize);
1559
1633
  Repository.ons(['changeData', 'change'], forceUpdate);
1560
1634
  Repository.on('changeFilters', onChangeFilters);
1561
1635
  Repository.on('changeSorters', onChangeSorters);
1562
- Repository.on('changePage', onChangePage);
1563
1636
 
1564
1637
  applySelectorSelected();
1565
1638
 
@@ -1578,13 +1651,11 @@ function GridComponent(props) {
1578
1651
  return () => {
1579
1652
  Repository.off('beforeLoad', setTrue);
1580
1653
  Repository.off('load', setFalse);
1581
- if (!disableWithSelection) {
1582
- Repository.offs(['changePage', 'changePageSize',], deselectAll);
1583
- }
1654
+ Repository.off('changePage', onChangePage);
1655
+ Repository.off('changePageSize', onChangePageSize);
1584
1656
  Repository.offs(['changeData', 'change'], forceUpdate);
1585
1657
  Repository.off('changeFilters', onChangeFilters);
1586
1658
  Repository.off('changeSorters', onChangeSorters);
1587
- Repository.off('changePage', onChangePage);
1588
1659
  };
1589
1660
  }, [isInited]);
1590
1661
 
@@ -1600,8 +1671,8 @@ function GridComponent(props) {
1600
1671
 
1601
1672
  }, [selectorId, selectorSelected]);
1602
1673
 
1603
- // Effect to trigger row height measurement after render
1604
1674
  useEffect(() => {
1675
+ // trigger row height measurement after render
1605
1676
  if (getMeasurementPhase() === PHASES__MEASURING) {
1606
1677
  // Small delay to ensure elements are fully rendered
1607
1678
  const timer = setTimeout(async () => {
@@ -1637,13 +1708,13 @@ function GridComponent(props) {
1637
1708
  }
1638
1709
  }, [autoAdjustPageSizeToHeight]);
1639
1710
 
1640
- // Reset measurement when rows were first empty then became populated
1641
1711
  useEffect(() => {
1712
+ // Reset measurement when rows were first empty then became populated
1642
1713
  const
1643
1714
  currentLength = entities?.length || 0,
1644
1715
  wasEmpty = previousEntitiesLength.current === 0,
1645
1716
  isNowPopulated = currentLength > 0,
1646
- hasPhantomRecord = entities?.some(entity => entity?.isPhantom);
1717
+ hasPhantomRecord = entities?.some(entity => !entity.isDestroyed && entity?.isPhantom);
1647
1718
 
1648
1719
  // NOTE: The Repository was reloading when a phantom record was added,
1649
1720
  // and this broke the Editor because selection was being reset to zero.
@@ -1674,6 +1745,15 @@ function GridComponent(props) {
1674
1745
  previousEntitiesLength.current = currentLength;
1675
1746
  }, [entities?.length, autoAdjustPageSizeToHeight]);
1676
1747
 
1748
+
1749
+ // Memoize footer toolbar items to avoid unnecessary re-renders, but only if they don't have dynamic properties
1750
+ const
1751
+ hasDynamicFooterToolbarItems = useMemo(() => _.some(additionalToolbarButtons, (config) => {
1752
+ return _.isFunction(config.getIsButtonDisabled) || _.isFunction(config.getText);
1753
+ }), [additionalToolbarButtons]),
1754
+ memoizedFooterToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, isReorderMode]),
1755
+ footerToolbarItemComponents = hasDynamicFooterToolbarItems ? getFooterToolbarItems() : memoizedFooterToolbarItemComponents;
1756
+
1677
1757
  if (canUser && !canUser('view')) {
1678
1758
  return <Unauthorized />;
1679
1759
  }
@@ -1685,8 +1765,6 @@ function GridComponent(props) {
1685
1765
 
1686
1766
  isAddingRaw.current = isAdding;
1687
1767
 
1688
- const footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [Repository?.hash, additionalToolbarButtons, isReorderMode]);
1689
-
1690
1768
  if (!isInited) {
1691
1769
  // first time through, render a placeholder so we can get container dimensions
1692
1770
  return <VStackNative
@@ -1838,6 +1916,8 @@ function GridComponent(props) {
1838
1916
  'w-full',
1839
1917
  'border',
1840
1918
  'border-grey-300',
1919
+ 'focus:outline-none', // hide the focus outline
1920
+ 'focus-visible:outline-none',
1841
1921
  );
1842
1922
  if (props.className) {
1843
1923
  className += ' ' + props.className;
@@ -490,7 +490,11 @@ const GridRow = forwardRef((props, ref) => {
490
490
  }}
491
491
  >{rowContents}</HStackNative>;
492
492
  if (rowProps.tooltip) {
493
- row = <Tooltip label={rowProps.tooltip} placement="bottom left">{row}</Tooltip>;
493
+ row = <Tooltip
494
+ label={rowProps.tooltip}
495
+ placement="bottom left"
496
+ triggerClassName={rowClassName}
497
+ >{row}</Tooltip>;
494
498
  }
495
499
  return row;
496
500
  }, [
@@ -47,6 +47,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
47
47
  },
48
48
  secondaryEditorType,
49
49
  secondaryOnAdd,
50
+ secondaryOnBeforeAdd,
50
51
  secondaryOnChange, // any kind of crud change
51
52
  secondaryOnBeforeDelete,
52
53
  secondaryOnDelete,
@@ -201,10 +202,26 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
201
202
  }
202
203
 
203
204
  if (getListeners().onBeforeAdd) {
204
- const listenerResult = await getListeners().onBeforeAdd();
205
+ // This listener is set by child components using setWithEditListeners()
206
+ const listenerResult = await getListeners().onBeforeAdd(addValues);
207
+ if (listenerResult === false) {
208
+ return;
209
+ }
210
+ if (listenerResult) {
211
+ // allow the listener to override the addValues by returning an object
212
+ addValues = listenerResult;
213
+ }
214
+ }
215
+ if (secondaryOnBeforeAdd) {
216
+ // This listener is set by parent components using a prop
217
+ const listenerResult = await secondaryOnBeforeAdd(addValues);
205
218
  if (listenerResult === false) {
206
219
  return;
207
220
  }
221
+ if (listenerResult) {
222
+ // allow the listener to override the addValues by returning an object
223
+ addValues = listenerResult;
224
+ }
208
225
  }
209
226
 
210
227
  if (isTree) {
@@ -47,6 +47,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
47
47
  },
48
48
  editorType,
49
49
  onAdd,
50
+ onBeforeAdd,
50
51
  onChange, // any kind of crud change
51
52
  onBeforeDelete,
52
53
  onDelete,
@@ -218,10 +219,26 @@ export default function withEditor(WrappedComponent, isTree = false) {
218
219
  }
219
220
 
220
221
  if (getListeners().onBeforeAdd) {
221
- const listenerResult = await getListeners().onBeforeAdd();
222
+ // This listener is set by child components using setWithEditListeners()
223
+ const listenerResult = await getListeners().onBeforeAdd(addValues);
224
+ if (listenerResult === false) {
225
+ return;
226
+ }
227
+ if (listenerResult) {
228
+ // allow the listener to override the addValues by returning an object
229
+ addValues = listenerResult;
230
+ }
231
+ }
232
+ if (onBeforeAdd) {
233
+ // This listener is set by parent components using a prop
234
+ const listenerResult = await onBeforeAdd(addValues);
222
235
  if (listenerResult === false) {
223
236
  return;
224
237
  }
238
+ if (listenerResult) {
239
+ // allow the listener to override the addValues by returning an object
240
+ addValues = listenerResult;
241
+ }
225
242
  }
226
243
 
227
244
  if (isTree) {
@@ -418,6 +418,9 @@ export default function withPdfButtons(WrappedComponent) {
418
418
  },
419
419
  ];
420
420
  _.each(buttons, (button) => {
421
+ if (!button) {
422
+ return; // guard against null/undefined
423
+ }
421
424
  if (!_.find(additionalEditButtons, btn => button.key === btn.key)) {
422
425
  additionalEditButtons.push(button);
423
426
  }
@@ -39,7 +39,7 @@ const presetButtons = [
39
39
  DOWNLOAD,
40
40
  ];
41
41
 
42
- export default function withPresetButtons(WrappedComponent, isGrid = false) {
42
+ export default function withPresetButtons(WrappedComponent) {
43
43
  return forwardRef((props, ref) => {
44
44
 
45
45
  if (props.disablePresetButtons || props.alreadyHasWithPresetButtons) {
@@ -64,20 +64,21 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
64
64
  {
65
65
  // extract and pass down
66
66
  isEditor = false,
67
- isTree = false,
67
+ isTree = false, // from withWindowedEditor or withSideEditor in Tree
68
68
  canDeleteRootNode = false,
69
69
  isSideEditor = false,
70
70
  canEditorViewOnly = false,
71
+ canRecordBeAdded, // fn(selection) returns bool on if the current record(s) can be added
71
72
  canRecordBeEdited, // fn(selection) returns bool on if the current record(s) can be edited
72
73
  canRecordBeDeleted, // fn(selection) returns bool on if the current record(s) can be deleted
73
74
  canRecordBeDuplicated, // fn(selection) returns bool on if the current record(s) can be duplicated
74
75
  disableAdd = !isEditor,
75
76
  disableEdit = !isEditor,
76
77
  disableDelete = !isEditor,
77
- disableView = !isGrid,
78
- disableCopy = !isGrid,
78
+ disableView = isTree,
79
+ disableCopy = isTree,
79
80
  disableDuplicate = !isEditor,
80
- disablePrint = !isGrid,
81
+ disablePrint = isTree,
81
82
  protectedValues, // records with these values cannot be edited or deleted
82
83
  addDisplayMsg,
83
84
  editDisplayMsg,
@@ -239,6 +240,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
239
240
  };
240
241
  icon = Plus;
241
242
  if (isNoSelectorSelected() ||
243
+ (canRecordBeAdded && !canRecordBeAdded(selection)) ||
242
244
  (isTree && isEmptySelection())
243
245
  ) {
244
246
  isDisabled = true;
@@ -434,6 +436,18 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
434
436
  }
435
437
  },
436
438
  onUploadDownload = () => {
439
+ const onUploadDecorator = async () => {
440
+ if (onUpload) {
441
+ await onUpload();
442
+ }
443
+ if (Repository && !Repository.isDestroyed) {
444
+ if (Repository.loadRootNodes) {
445
+ await Repository.loadRootNodes(1);
446
+ } else {
447
+ await Repository.reload();
448
+ }
449
+ }
450
+ };
437
451
  showModal({
438
452
  body: <UploadsDownloadsWindow
439
453
  reference="uploadsDownloads"
@@ -442,7 +456,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
442
456
  columnsConfig={props.columnsConfig}
443
457
  uploadHeaders={uploadHeaders}
444
458
  uploadParams={uploadParams}
445
- onUpload={onUpload}
459
+ onUpload={onUploadDecorator}
446
460
  downloadHeaders={downloadHeaders}
447
461
  downloadParams={downloadParams}
448
462
  />,