@visns-studio/visns-components 6.24.4 → 6.26.0

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 (51) hide show
  1. package/package.json +4 -2
  2. package/src/components/Autocomplete.jsx +189 -119
  3. package/src/components/DataGrid.jsx +472 -31
  4. package/src/components/Navigation.jsx +475 -51
  5. package/src/components/auth/ClientAuthFrame.jsx +5 -0
  6. package/src/components/auth/ClientAuthScreen.jsx +29 -0
  7. package/src/components/callQueue/CallQueueDiagnostics.jsx +1043 -0
  8. package/src/components/callQueue/CallQueuePop.jsx +713 -90
  9. package/src/components/callQueue/CallQueueSettings.jsx +308 -146
  10. package/src/components/callQueue/callPopStatus.js +236 -0
  11. package/src/components/callQueue/callQueueHelpers.js +284 -2
  12. package/src/components/columns/ColumnRenderers.jsx +3 -46
  13. package/src/components/columns/StackedRow.jsx +186 -0
  14. package/src/components/controls/DataGridSearch.jsx +110 -2
  15. package/src/components/controls/DataGridSortSheet.jsx +155 -0
  16. package/src/components/generic/GenericAuth.jsx +50 -18
  17. package/src/components/generic/GenericDashboard.jsx +20 -1
  18. package/src/components/generic/GenericDetail.jsx +446 -259
  19. package/src/components/mapboxSearchBox.js +640 -0
  20. package/src/components/navBadges.js +63 -1
  21. package/src/components/navDrawer.js +147 -0
  22. package/src/components/sms/SmsThreadPanel.jsx +34 -6
  23. package/src/components/sms/smsHelpers.js +15 -0
  24. package/src/components/styles/CallQueueDiagnostics.module.scss +398 -0
  25. package/src/components/styles/CallQueuePop.module.scss +29 -0
  26. package/src/components/styles/CallQueueSettings.module.scss +93 -0
  27. package/src/components/styles/ClientAuth.module.scss +39 -0
  28. package/src/components/styles/DataGrid.module.scss +158 -5
  29. package/src/components/styles/Field.module.scss +52 -1
  30. package/src/components/styles/Form.module.scss +82 -0
  31. package/src/components/styles/GenericClientPortal.module.scss +72 -20
  32. package/src/components/styles/GenericDashboard.module.scss +50 -0
  33. package/src/components/styles/GenericDetail.module.scss +63 -1
  34. package/src/components/styles/GenericDynamic.module.scss +23 -0
  35. package/src/components/styles/GenericFormBuilder.module.scss +11 -0
  36. package/src/components/styles/GenericIndex.module.scss +6 -1
  37. package/src/components/styles/Navigation.module.scss +460 -7
  38. package/src/components/styles/Sms.module.scss +92 -0
  39. package/src/components/styles/StackedRow.module.scss +182 -0
  40. package/src/components/styles/TicketConversation.module.scss +76 -0
  41. package/src/components/styles/Vault.module.scss +192 -0
  42. package/src/components/styles/density.css +10 -0
  43. package/src/components/styles/global-datagrid.css +163 -0
  44. package/src/components/styles/global.css +20 -0
  45. package/src/components/tickets/TicketConversation.jsx +13 -8
  46. package/src/components/utils/ConfirmDialog.js +22 -3
  47. package/src/components/utils/cardLayout.js +666 -0
  48. package/src/components/utils/contactChannels.js +130 -0
  49. package/src/components/utils/editPlacement.js +95 -0
  50. package/src/components/utils/useDensity.js +303 -7
  51. package/src/index.js +42 -0
@@ -47,9 +47,20 @@ import { resolveFormVariant } from '../utils/formLayout';
47
47
  import { confirmDialog } from './utils/ConfirmDialog';
48
48
  import {
49
49
  useDensity,
50
+ useNarrowViewport,
50
51
  useAvailableHeight,
52
+ useCardRowsEnabled,
51
53
  rowsForHeight,
54
+ gridHeightFloor,
55
+ NARROW_COLUMN_MIN_WIDTH,
52
56
  } from './utils/useDensity';
57
+ import {
58
+ CARD_COLUMN_NAME,
59
+ MAX_BODY_LINES,
60
+ cardRowHeight,
61
+ deriveCardLayout,
62
+ } from './utils/cardLayout';
63
+ import StackedRow from './columns/StackedRow';
53
64
  import { openRecordWindow } from './utils/popupWindow';
54
65
  import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../utils/relationshipSortingUtils';
55
66
  import {
@@ -1072,7 +1083,145 @@ const DataGrid = forwardRef(
1072
1083
  // unreliable after auto-refresh on an idle tab (background-throttled
1073
1084
  // rAF/timers). A fixed height bypasses measurement entirely and
1074
1085
  // prevents action buttons from being clipped.
1075
- const { grid } = useDensity();
1086
+ const { grid, density } = useDensity();
1087
+
1088
+ // Width, which is a different question from density — see
1089
+ // NARROW_VIEWPORT_QUERY. A phone is both; a browser window dragged to
1090
+ // a third of a monitor is only this one.
1091
+ const isNarrow = useNarrowViewport();
1092
+
1093
+ // ---------------------------------------------------------------
1094
+ // CARD MODE — the same grid, projected onto one synthetic column
1095
+ // ---------------------------------------------------------------
1096
+ //
1097
+ // Below 640px the columns are built exactly as they always are — every
1098
+ // per-type renderer, the trailing Action column, the lot — and then
1099
+ // ONE column is handed to the grid whose `render` draws them stacked.
1100
+ //
1101
+ // This is the whole design decision, and it is what is NOT rewritten
1102
+ // that makes it worth having: `dataSource`, the search debounce, the
1103
+ // `where` clauses, `suppressRepeated`, `setTotal`/`setTableData`,
1104
+ // auto-refresh, selection, the pager and its finished ≤640px CSS all
1105
+ // carry on knowing nothing about any of it. Above all `onRowClick`
1106
+ // resolves a link by CELL INDEX, and the card IS a cell — so a tap on
1107
+ // a card follows the same link the table's first column does, through
1108
+ // the same code.
1109
+ //
1110
+ // A `renderRow` override was the obvious alternative and needs
1111
+ // `rowHeight={null}` plus natural measurement, which is precisely the
1112
+ // configuration `utils/useDensity.js` documents as unreliable on a
1113
+ // coarse pointer. A sibling card list re-owns skip/limit/sortInfo/
1114
+ // filterValue and becomes the second fetch path.
1115
+ const cardRowsEnabled = useCardRowsEnabled();
1116
+ const cardOverrides = tableSetting?.cardLayout;
1117
+
1118
+ const cardMapping = useMemo(() => {
1119
+ // Precedence, highest first: a view that says no; a view that
1120
+ // supplies its own mapping (which opts itself in, whatever the
1121
+ // app-wide attribute says); the app-wide attribute.
1122
+ if (cardOverrides === false) {
1123
+ return null;
1124
+ }
1125
+
1126
+ const viewOptedIn =
1127
+ cardOverrides !== null &&
1128
+ cardOverrides !== undefined &&
1129
+ typeof cardOverrides === 'object';
1130
+
1131
+ if (!cardRowsEnabled && !viewOptedIn) {
1132
+ return null;
1133
+ }
1134
+
1135
+ // A grouped grid draws bucket headers between its rows and the
1136
+ // card knows nothing about them. Hard off rather than half-right.
1137
+ if (ajaxSetting?.groupBy) {
1138
+ return null;
1139
+ }
1140
+
1141
+ return deriveCardLayout(columns, {
1142
+ overrides: cardOverrides,
1143
+ maxLines:
1144
+ density === 'large'
1145
+ ? MAX_BODY_LINES.large
1146
+ : MAX_BODY_LINES.default,
1147
+ log: debugLog,
1148
+ });
1149
+ }, [
1150
+ columns,
1151
+ cardOverrides,
1152
+ cardRowsEnabled,
1153
+ ajaxSetting?.groupBy,
1154
+ density,
1155
+ ]);
1156
+
1157
+ // The reader's own escape hatch, per grid and for this session only.
1158
+ // Nothing about card mode is a preference worth persisting to a
1159
+ // profile: it answers "I need the filter row for the next minute", and
1160
+ // a choice that outlives the tab is a choice somebody has to undo on
1161
+ // a device they are no longer holding.
1162
+ const cardPreferenceKey = `visns:cardRows:${ajaxSetting?.url ?? ''}`;
1163
+ const [cardPreference, setCardPreference] = useState('');
1164
+
1165
+ useEffect(() => {
1166
+ let stored = '';
1167
+
1168
+ try {
1169
+ stored =
1170
+ window.sessionStorage.getItem(cardPreferenceKey) || '';
1171
+ } catch (unavailable) {
1172
+ // Private browsing, or storage disabled. The default stands.
1173
+ }
1174
+
1175
+ setCardPreference(stored);
1176
+ }, [cardPreferenceKey]);
1177
+
1178
+ const chooseCardPreference = useCallback(
1179
+ (next) => {
1180
+ setCardPreference(next);
1181
+
1182
+ try {
1183
+ window.sessionStorage.setItem(cardPreferenceKey, next);
1184
+ } catch (unavailable) {
1185
+ // The choice still holds for as long as the grid is mounted.
1186
+ }
1187
+ },
1188
+ [cardPreferenceKey]
1189
+ );
1190
+
1191
+ const cardCapable = isNarrow && Boolean(cardMapping);
1192
+ const cardMode = cardCapable && cardPreference !== 'table';
1193
+
1194
+ // `tableSetting` is SPREAD onto the datagrid, so every key a view puts
1195
+ // in it becomes a prop. `cardLayout` is ours and means nothing to the
1196
+ // toolkit; leaving it in the spread hands ReactDataGrid an unknown
1197
+ // prop, and unknown props on this component end up on the DOM node as
1198
+ // React attribute warnings.
1199
+ const gridTableSetting = useMemo(() => {
1200
+ if (!tableSetting || tableSetting.cardLayout === undefined) {
1201
+ return tableSetting;
1202
+ }
1203
+
1204
+ const { cardLayout, ...rest } = tableSetting;
1205
+
1206
+ return rest;
1207
+ }, [tableSetting]);
1208
+
1209
+ // Every row is this tall, computed from the mapping rather than
1210
+ // measured — see `cardRowHeight`. It reaches the grid in FOUR places
1211
+ // and all four have to agree: `rowHeight`, `minRowHeight`, the page
1212
+ // size (`rowsForHeight`, or the pager reads "Page 1 of 236") and the
1213
+ // height floor's own pitch.
1214
+ const cardHeight = useMemo(() => {
1215
+ if (!cardMode) {
1216
+ return null;
1217
+ }
1218
+
1219
+ const hasActions =
1220
+ (memoizedSettings?.length ?? 0) > 0 ||
1221
+ (cardMapping?.actionColumns?.length ?? 0) > 0;
1222
+
1223
+ return cardRowHeight(cardMapping, { density, hasActions });
1224
+ }, [cardMode, cardMapping, memoizedSettings, density]);
1076
1225
 
1077
1226
  // ---------------------------------------------------------------
1078
1227
  // Per-column autoGrow
@@ -3123,9 +3272,27 @@ const DataGrid = forwardRef(
3123
3272
  })),
3124
3273
  });
3125
3274
 
3275
+ // "The last column is the row-action column; its icons handle
3276
+ // their own clicks, so a click on the column itself is not a
3277
+ // row click." THE RULE IS POSITIONAL AND THE POSITION IS NOT
3278
+ // ALWAYS THE ACTION COLUMN: in card mode the columns are
3279
+ // `[__checkbox-column, __card]`, the card IS last, and every
3280
+ // tap on a card would be swallowed here.
3281
+ //
3282
+ // The exemption names the card rather than re-testing the
3283
+ // trailing column for `name === 'setting'`, which is what it
3284
+ // ought to be. A grid whose config carries no `settings` gets
3285
+ // no trailing action column at all, so its last DATA column is
3286
+ // the one this branch currently ignores — making that column
3287
+ // clickable is a behaviour change across three applications,
3288
+ // and this pass has no reason to make it. Above 640px nothing
3289
+ // reaches here differently than it did.
3290
+ const clickedColumn = rowProps.columns[columnIndex];
3291
+
3126
3292
  if (
3127
3293
  rowProps.columns.length > 1 &&
3128
- columnIndex === rowProps.columns.length - 1
3294
+ columnIndex === rowProps.columns.length - 1 &&
3295
+ clickedColumn?.name !== CARD_COLUMN_NAME
3129
3296
  ) {
3130
3297
  debugLog('🔍 Last column clicked - no action');
3131
3298
  // Handle last column click
@@ -4286,13 +4453,27 @@ const DataGrid = forwardRef(
4286
4453
  // ("dd / mm / yyyy" in Firefox, glyph included -
4287
4454
  // Firefox will not hide it); below ~160px the
4288
4455
  // native input clips its own value.
4456
+ // On a narrow viewport the floor rises to
4457
+ // NARROW_COLUMN_MIN_WIDTH, because 80px is where
4458
+ // seven flex columns land on a phone and 80px is
4459
+ // an ellipsis, not a column — every header on
4460
+ // /contacts rendered as "FI…", "LA…", "TI…". The
4461
+ // columns then total more than the screen and the
4462
+ // grid's own body scrolls sideways, which it is
4463
+ // already built to do; the page does not move.
4464
+ //
4465
+ // A view's own `column.minWidth` still wins, so a
4466
+ // column that really is narrow stays narrow.
4289
4467
  minWidth:
4290
4468
  column.minWidth ??
4291
- (['date', 'datetime', 'datetime-local'].includes(
4292
- column.type
4293
- )
4294
- ? Math.max(grid.columnMinWidth, 160)
4295
- : grid.columnMinWidth),
4469
+ Math.max(
4470
+ isNarrow ? NARROW_COLUMN_MIN_WIDTH : 0,
4471
+ ['date', 'datetime', 'datetime-local'].includes(
4472
+ column.type
4473
+ )
4474
+ ? Math.max(grid.columnMinWidth, 160)
4475
+ : grid.columnMinWidth
4476
+ ),
4296
4477
  maxWidth: column.maxWidth ?? undefined,
4297
4478
  textAlign: column.textAlign ?? undefined,
4298
4479
  textVerticalAlign:
@@ -5424,7 +5605,131 @@ const DataGrid = forwardRef(
5424
5605
  });
5425
5606
  }
5426
5607
 
5427
- setGridColumns(newColumns);
5608
+ // CARD MODE — hand the grid ONE column that draws the rest.
5609
+ //
5610
+ // Every descriptor above still exists and is still the only
5611
+ // thing that formats a value; the card calls them. That is
5612
+ // the point of building the full array first and throwing
5613
+ // away nothing but the LAYOUT.
5614
+ //
5615
+ // The filter defaults below are computed and set either
5616
+ // way, and they still reach the server: the datagrid's
5617
+ // `getFilterValueForColumns` is a `map`, not a `filter`, so
5618
+ // an entry whose column is not mounted passes through
5619
+ // untouched. A view whose Stage filter defaults to "active
5620
+ // deals only" opens on active deals here as well.
5621
+ if (cardMode && cardMapping) {
5622
+ // `newColumns` is `columns.map(...)` with the Action
5623
+ // column pushed after it, so index i of one is index i
5624
+ // of the other for every config column.
5625
+ const builtByColumn = new Map(
5626
+ columns.map((column, index) => [
5627
+ column,
5628
+ newColumns[index],
5629
+ ])
5630
+ );
5631
+
5632
+ const valueOf = (column, row) => {
5633
+ const path = Array.isArray(column.id)
5634
+ ? column.id
5635
+ : String(column.id ?? '').split('.');
5636
+
5637
+ return path.reduce(
5638
+ (value, segment) =>
5639
+ value === null || value === undefined
5640
+ ? value
5641
+ : value[segment],
5642
+ row
5643
+ );
5644
+ };
5645
+
5646
+ const renderCardCell = (column, row) => {
5647
+ const built = builtByColumn.get(column);
5648
+
5649
+ if (!built || typeof built.render !== 'function') {
5650
+ return null;
5651
+ }
5652
+
5653
+ return built.render({
5654
+ data: row,
5655
+ value: valueOf(column, row),
5656
+ column: built,
5657
+ });
5658
+ };
5659
+
5660
+ // The rail, and it is the trailing Action column's own
5661
+ // render restated — same `splitSettingEntries`, same
5662
+ // exclusion of settings an action column already draws,
5663
+ // same role and per-row gating inside `renderSetting`.
5664
+ // Both entry points already stopPropagation, which is
5665
+ // what makes an icon safe inside a clickable card.
5666
+ const renderCardActions = (row) => {
5667
+ if (row?.__treeParent) {
5668
+ return null;
5669
+ }
5670
+
5671
+ const { inlineActions, standard } =
5672
+ splitSettingEntries(memoizedSettings);
5673
+
5674
+ const controls = [
5675
+ ...inlineActions.map((config) =>
5676
+ renderRowAction(config, row)
5677
+ ),
5678
+ ...standard
5679
+ .filter(
5680
+ (setting) =>
5681
+ !actionColumnSettingIds.has(
5682
+ setting.id
5683
+ )
5684
+ )
5685
+ .map((setting) =>
5686
+ renderSetting(setting, row)
5687
+ ),
5688
+ ...cardMapping.actionColumns.map((column) =>
5689
+ renderCardCell(column, row)
5690
+ ),
5691
+ ].filter(Boolean);
5692
+
5693
+ return controls.length > 0 ? controls : null;
5694
+ };
5695
+
5696
+ setGridColumns([
5697
+ {
5698
+ name: CARD_COLUMN_NAME,
5699
+ header: '',
5700
+ sortable: false,
5701
+ defaultFlex: 1,
5702
+ // Lands on the CELL, which is what the ≤640px
5703
+ // block in global-datagrid.css needs: the
5704
+ // checkbox column is a cell too and must keep
5705
+ // the grid's padding and centring.
5706
+ className: 'datagrid-card-cell',
5707
+ // The NARROW floor is deliberately NOT applied.
5708
+ // It exists to stop seven flex columns
5709
+ // collapsing to an ellipsis each; one flex
5710
+ // column has no such problem, and a 160px floor
5711
+ // on it is how a card would find a horizontal
5712
+ // scrollbar it has no use for.
5713
+ minWidth: 0,
5714
+ // Inherited verbatim from whichever column the
5715
+ // mapping decided the row follows, so
5716
+ // `onRowClick` runs the same navigate /
5717
+ // window.open / openRecordWindow cascade it
5718
+ // runs for the table's first cell.
5719
+ link: cardMapping.link ?? {},
5720
+ render: ({ data }) => (
5721
+ <StackedRow
5722
+ row={data}
5723
+ mapping={cardMapping}
5724
+ renderCell={renderCardCell}
5725
+ renderActions={renderCardActions}
5726
+ />
5727
+ ),
5728
+ },
5729
+ ]);
5730
+ } else {
5731
+ setGridColumns(newColumns);
5732
+ }
5428
5733
 
5429
5734
  const newFilterValue = columns
5430
5735
  .filter((column) => column.filter && column.filter.type) // Only consider columns with a filter type
@@ -5511,7 +5816,22 @@ const DataGrid = forwardRef(
5511
5816
  .catch((error) => {
5512
5817
  console.error('Error in fetching dropdown data: ', error);
5513
5818
  });
5514
- }, [columns, filterDataSource, memoizedSettings, grid]);
5819
+ // `isNarrow` is in here because the column floor is built from it:
5820
+ // without it, rotating a phone or dragging a window across the
5821
+ // 640px line keeps the widths the grid was first built with.
5822
+ //
5823
+ // `cardMode` and `cardMapping` ride along for the same reason —
5824
+ // crossing 640px, or a reader pressing Table, has to rebuild the
5825
+ // column array, not just restyle it.
5826
+ }, [
5827
+ columns,
5828
+ filterDataSource,
5829
+ memoizedSettings,
5830
+ grid,
5831
+ isNarrow,
5832
+ cardMode,
5833
+ cardMapping,
5834
+ ]);
5515
5835
 
5516
5836
  // Force re-render when filterDataSource updates to ensure dropdown filters get their data
5517
5837
  const [renderKey, setRenderKey] = useState(0);
@@ -5692,11 +6012,38 @@ const DataGrid = forwardRef(
5692
6012
  return el ? el.getBoundingClientRect().height : 0;
5693
6013
  };
5694
6014
 
6015
+ // TWO BUGS LIVED IN THIS SUM, AND THEY CANCELLED.
6016
+ //
6017
+ // `.InovuaReactDataGrid__header-wrapper` ALREADY CONTAINS the
6018
+ // filter row, so adding the filter wrapper counted it twice. And
6019
+ // the pager is `.inovua-react-pagination-toolbar` — the toolkit's
6020
+ // own toolbar component, with no grid prefix on it — so
6021
+ // `.InovuaReactDataGrid__pagination-toolbar` matched nothing and
6022
+ // the pager was never counted at all.
6023
+ //
6024
+ // On /contacts both errors are the same 41px and the total came
6025
+ // out right by accident. They stop cancelling the moment the two
6026
+ // differ, which is any density change: the filter row steps to
6027
+ // 32px under a coarse pointer while the pager stays 41.
6028
+ //
6029
+ // So: take the header wrapper WHOLE when it is there, and only
6030
+ // fall back to summing the column-header and filter rows when it
6031
+ // is not.
6032
+ const headerWrapper = heightOf(
6033
+ '.InovuaReactDataGrid__header-wrapper'
6034
+ );
6035
+ const header =
6036
+ headerWrapper > 0
6037
+ ? headerWrapper
6038
+ : heightOf('.InovuaReactDataGrid__column-header') +
6039
+ heightOf(
6040
+ '.InovuaReactDataGrid__column-header__filter-wrapper'
6041
+ );
6042
+
5695
6043
  const chrome =
5696
- heightOf('.InovuaReactDataGrid__header-wrapper, .InovuaReactDataGrid__column-header') +
5697
- heightOf('.InovuaReactDataGrid__column-header__filter-wrapper') +
6044
+ header +
5698
6045
  heightOf('.InovuaReactDataGrid__footer-wrapper, .InovuaReactDataGrid__footer') +
5699
- heightOf('.InovuaReactDataGrid__pagination-toolbar');
6046
+ heightOf('.inovua-react-pagination-toolbar');
5700
6047
 
5701
6048
  return chrome > 0 ? chrome : null;
5702
6049
  };
@@ -5758,14 +6105,33 @@ const DataGrid = forwardRef(
5758
6105
  ? resolvedHeight
5759
6106
  : fitToRows(dataCount, window.innerHeight);
5760
6107
 
6108
+ // The chrome and the row pitch, in the one place that reads both.
6109
+ // The pitch rule is `contentHeight`'s, restated here rather than
6110
+ // duplicated in spirit: measurement first, floored by the density
6111
+ // unit and then by ROW_PITCH_FLOOR, so a stale or unavailable
6112
+ // reading can only ever over-allocate.
6113
+ //
6114
+ // In card mode the pitch is the CARD's height, not the density
6115
+ // unit — a card is 84–122px against a rowUnit of 52, and costing
6116
+ // five of them at 52 sizes the body less than half what it needs.
6117
+ // It is floored the same way for the same reason: measurement
6118
+ // reads the previous render.
6119
+ const gridMetrics = () => ({
6120
+ chrome: measureGridChrome() ?? GRID_CHROME,
6121
+ pitch: Math.max(
6122
+ measureRowPitch() ?? 0,
6123
+ cardHeight ?? 0,
6124
+ grid.rowUnit ?? 0,
6125
+ ROW_PITCH_FLOOR
6126
+ ),
6127
+ });
6128
+
5761
6129
  // A window-sized grid asks for as many rows as fit — 37 on a 1440p
5762
6130
  // screen. A table holding four of them rendered the other 33 as
5763
6131
  // empty body, which is the single most dated thing about an index
5764
6132
  // page: a thousand pixels of nothing under the last row, with the
5765
6133
  // pager stranded at the bottom of it.
5766
6134
  const contentHeight = () => {
5767
- const chrome = measureGridChrome() ?? GRID_CHROME;
5768
-
5769
6135
  // ERR HIGH, DELIBERATELY.
5770
6136
  //
5771
6137
  // The two errors here are not symmetrical. Over-allocating
@@ -5777,12 +6143,11 @@ const DataGrid = forwardRef(
5777
6143
  // use; measured, rows render at 44px against a rowUnit of 32.
5778
6144
  // Measurement is preferred when it is available, but it reads
5779
6145
  // the DOM and the DOM is not always there when this runs — so
5780
- // there is a floor under whatever it returns.
5781
- const pitch = Math.max(
5782
- measureRowPitch() ?? 0,
5783
- grid.rowUnit ?? 0,
5784
- ROW_PITCH_FLOOR
5785
- );
6146
+ // there is a floor under whatever it returns. That is
6147
+ // `gridMetrics`, shared with the floor below: the ceiling and
6148
+ // the floor costing a row differently is how a grid ends up
6149
+ // asking for rows it has no room to draw.
6150
+ const { chrome, pitch } = gridMetrics();
5786
6151
 
5787
6152
  const rows = Math.max(dataCount || 0, MIN_VISIBLE_ROWS);
5788
6153
 
@@ -5801,7 +6166,28 @@ const DataGrid = forwardRef(
5801
6166
  // being expressed, not a measurement. Honouring it literally put
5802
6167
  // ~975px of empty body under the two rows on a customer's Sites
5803
6168
  // tab.
5804
- const renderedHeight = Math.min(availableHeight, contentHeight());
6169
+ // ...and a FLOOR under the result, because a ceiling alone let the
6170
+ // grid be drawn shorter than its own chrome. See `gridHeightFloor`
6171
+ // in useDensity for the measurements; the short version is that a
6172
+ // phone leaves ~130px under the stat strip and the grid's header,
6173
+ // filter row and pager need 123 of it, so the body got seven
6174
+ // pixels and painted nothing. `contentHeight()` already carries
6175
+ // MIN_VISIBLE_ROWS, but it only ever reaches the grid through a
6176
+ // `Math.min`, so it could make the grid shorter and never taller.
6177
+ //
6178
+ // Below the floor the grid overflows its slot and the PAGE
6179
+ // scrolls, which is what a page whose content does not fit is
6180
+ // supposed to do.
6181
+ const floorFor = () => {
6182
+ const { chrome, pitch } = gridMetrics();
6183
+
6184
+ return gridHeightFloor(chrome, pitch, MIN_VISIBLE_ROWS);
6185
+ };
6186
+
6187
+ const renderedHeight = Math.max(
6188
+ Math.min(availableHeight, contentHeight()),
6189
+ floorFor()
6190
+ );
5805
6191
 
5806
6192
  // Re-measure ONCE MORE after the browser has painted.
5807
6193
  //
@@ -5816,7 +6202,10 @@ const DataGrid = forwardRef(
5816
6202
  // stops it looping: it only writes when the corrected height
5817
6203
  // actually differs.
5818
6204
  const frame = requestAnimationFrame(() => {
5819
- const corrected = Math.min(availableHeight, contentHeight());
6205
+ const corrected = Math.max(
6206
+ Math.min(availableHeight, contentHeight()),
6207
+ floorFor()
6208
+ );
5820
6209
 
5821
6210
  if (Math.abs(corrected - renderedHeight) > 1) {
5822
6211
  setGridStyle((prev) =>
@@ -5833,8 +6222,20 @@ const DataGrid = forwardRef(
5833
6222
  height: renderedHeight,
5834
6223
  }));
5835
6224
 
5836
- // Everything below sizes the REQUEST, so it reads availableHeight.
5837
- const minHeight = availableHeight;
6225
+ // Everything below sizes the REQUEST, so it reads availableHeight
6226
+ // floored to the same minimum the grid is DRAWN at.
6227
+ //
6228
+ // Without the floor the two disagree in the worst possible
6229
+ // direction: the pager on a phone read "Page 1 of 236" for 236
6230
+ // contacts, because `rowsForHeight(130, 123, 52)` is
6231
+ // `max(1, floor(7 / 52))` — one contact per page, 236 pages of
6232
+ // them. The floor is applied here rather than swapping in
6233
+ // `renderedHeight` because `renderedHeight` follows the CONTENT,
6234
+ // and sizing the request from it is the feedback loop the comment
6235
+ // above warns about: a short table shrinks the grid, a shorter
6236
+ // grid asks for fewer rows, and it can never grow back to find out
6237
+ // there was more data.
6238
+ const minHeight = Math.max(availableHeight, floorFor());
5838
6239
 
5839
6240
  // How many rows to ask for.
5840
6241
  //
@@ -5886,7 +6287,14 @@ const DataGrid = forwardRef(
5886
6287
  // first pass, before the grid exists to be measured.
5887
6288
  const chrome = measureGridChrome() ?? GRID_CHROME;
5888
6289
 
5889
- setLimit(rowsForHeight(minHeight, chrome, grid.rowUnit));
6290
+ // THE UNIT, NOT THE DENSITY UNIT, in card mode. `rowUnit` is
6291
+ // 52 under a coarse pointer and a card is 122; asking for
6292
+ // rowsForHeight(…, 52) on a phone requests twelve rows into a
6293
+ // body with room for five, which is the "Page 1 of 236" shape
6294
+ // of bug the floor above was added to fix at the other end.
6295
+ setLimit(
6296
+ rowsForHeight(minHeight, chrome, cardHeight ?? grid.rowUnit)
6297
+ );
5890
6298
  }
5891
6299
  return () => cancelAnimationFrame(frame);
5892
6300
  }, [
@@ -5895,6 +6303,7 @@ const DataGrid = forwardRef(
5895
6303
  resolvedHeight,
5896
6304
  window.innerHeight,
5897
6305
  grid,
6306
+ cardHeight,
5898
6307
  ajaxSetting?.take,
5899
6308
  userProfile?.rows_per_page,
5900
6309
  userProfile?.settings?.rows_per_page,
@@ -6001,6 +6410,17 @@ const DataGrid = forwardRef(
6001
6410
  pageData={pageData}
6002
6411
  pageSetting={pageSetting}
6003
6412
  onBulkUploadClick={handleBulkUploadClick}
6413
+ cardCapable={cardCapable}
6414
+ cardMode={cardMode}
6415
+ onCardModeChange={chooseCardPreference}
6416
+ columns={columns}
6417
+ columnsMetadata={columnsMetadata}
6418
+ sortBy={ajaxSetting?.sortBy ?? null}
6419
+ sort={ajaxSetting?.sort ?? null}
6420
+ // The SAME function a column header calls, so
6421
+ // there is one sort implementation and the
6422
+ // sheet cannot drift from it.
6423
+ onSort={handleSort}
6004
6424
  />
6005
6425
  </div>
6006
6426
  <div className={styles.dataGridTopRightContainer}>
@@ -6063,7 +6483,7 @@ const DataGrid = forwardRef(
6063
6483
  key={`datagrid-${ajaxSetting?.url || ''}-${
6064
6484
  ajaxSetting?.groupBy ? 'grouped' : 'ungrouped'
6065
6485
  }-${groupRenderKey}`}
6066
- {...tableSetting}
6486
+ {...gridTableSetting}
6067
6487
  columns={gridColumns}
6068
6488
  dataSource={dataSource}
6069
6489
  {...(ajaxSetting && ajaxSetting.groupBy
@@ -6591,7 +7011,7 @@ const DataGrid = forwardRef(
6591
7011
  }
6592
7012
  headerProps={headerProps}
6593
7013
  idProperty="id"
6594
- minRowHeight={grid.minRowHeight}
7014
+ minRowHeight={cardHeight ?? grid.minRowHeight}
6595
7015
  onFilterValueChange={(fv) => {
6596
7016
  handleFilterChange(fv, filterValue);
6597
7017
  }}
@@ -6608,7 +7028,19 @@ const DataGrid = forwardRef(
6608
7028
  // need — measurement is unreliable in a
6609
7029
  // background-throttled tab and clips action
6610
7030
  // buttons. See the density table in useDensity.
6611
- rowHeight={grid.rowHeight}
7031
+ //
7032
+ // A card is always a number, computed per view
7033
+ // from its mapping. Same argument, more force: the
7034
+ // one platform card mode runs on is the one this
7035
+ // comment says not to trust measurement on.
7036
+ rowHeight={cardHeight ?? grid.rowHeight}
7037
+ // Header AND filter row in one prop: the header
7038
+ // wrapper returns null for the whole thing, and
7039
+ // a column header over a card is a header for a
7040
+ // column that does not exist. See the Sort control
7041
+ // in DataGridSearch for what replaces the header's
7042
+ // other job.
7043
+ {...(cardMode ? { showHeader: false } : {})}
6612
7044
  rowStyle={getRowStyle}
6613
7045
  selected={selected}
6614
7046
  showZebraRows={true}
@@ -6629,11 +7061,20 @@ const DataGrid = forwardRef(
6629
7061
  border: '1px solid #d1d5db',
6630
7062
  overflow: 'hidden',
6631
7063
  }}
6632
- className={
7064
+ className={[
6633
7065
  ajaxSetting?.groupBy
6634
7066
  ? 'InovuaReactDataGrid--grouped datagrid-with-grouping datagrid-fixed-group-headers'
6635
- : 'datagrid-without-grouping'
6636
- }
7067
+ : 'datagrid-without-grouping',
7068
+ // The hook the ≤640px block in
7069
+ // global-datagrid.css needs to strip the cell's
7070
+ // padding and its right border — a card owns
7071
+ // its own gutters, and a cell divider down the
7072
+ // right of a full-width row is a line to
7073
+ // nowhere.
7074
+ cardMode ? 'datagrid-card-mode' : '',
7075
+ ]
7076
+ .filter(Boolean)
7077
+ .join(' ')}
6637
7078
  // The real prop is `checkboxColumn` (bool | column
6638
7079
  // object) — {...tableSetting} spreads the view's
6639
7080
  // `checkboxColumn: true` and this object, placed