@visns-studio/visns-components 6.3.6 → 6.3.8

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
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.3.6",
94
+ "version": "6.3.8",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -78,6 +78,16 @@ import {
78
78
  } from 'lucide-react';
79
79
  import styles from './styles/DataGrid.module.scss';
80
80
 
81
+ /**
82
+ * `ajaxSetting.rowStyles[].style` tokens → the module class that draws them.
83
+ * Kept as a map rather than `styles['row' + capitalise(token)]` so an unknown
84
+ * token in a config is simply ignored instead of resolving to `undefined` and
85
+ * putting the literal string "undefined" in the row's class list.
86
+ */
87
+ const ROW_STYLE_CLASSES = {
88
+ complete: styles.rowComplete,
89
+ };
90
+
81
91
  import '@visns-studio/visns-datagrid-enterprise/index.css';
82
92
 
83
93
  import CustomFetch from './Fetch';
@@ -649,11 +659,11 @@ const DataGrid = forwardRef(
649
659
  }
650
660
 
651
661
  // Opt-in grouped display (ajaxSetting.treeGroupBy {key,
652
- // column, parentFields, countLabel, collapsible,
653
- // defaultCollapsed, icons}): consecutive rows sharing a
654
- // non-empty `key` value get a synthetic summary parent
655
- // row inserted above them. `parentFields` are lifted from
656
- // the first row onto the parent and blanked on the
662
+ // column, parentFields, parentFieldAliases, countLabel,
663
+ // collapsible, defaultCollapsed, icons}): consecutive rows
664
+ // sharing a non-empty `key` value get a synthetic summary
665
+ // parent row inserted above them. `parentFields` are lifted
666
+ // from the first row onto the parent and blanked on the
657
667
  // children, so shared details display once; children get
658
668
  // an "↳" prefix in the display column. Single rows stay
659
669
  // plain — unlike the grid's own groupBy, which forces a
@@ -668,6 +678,7 @@ const DataGrid = forwardRef(
668
678
  key: treeKey,
669
679
  column: treeDisplayColumn,
670
680
  parentFields = [],
681
+ parentFieldAliases = {},
671
682
  countLabel,
672
683
  collapsible,
673
684
  defaultCollapsed,
@@ -712,6 +723,35 @@ const DataGrid = forwardRef(
712
723
  });
713
724
  });
714
725
 
726
+ // `parentFieldAliases` ({columnField:
727
+ // sourceField}): the parent shows a DIFFERENT
728
+ // field in a column than its children do, so
729
+ // one column can carry the per-member value on
730
+ // members and the group's summary on the
731
+ // parent instead of spending a second column
732
+ // on the summary alone.
733
+ //
734
+ // Unlike `parentFields` this deliberately does
735
+ // NOT blank the column on the children — the
736
+ // members' own values are the reason the
737
+ // column exists. (Listing the same field in
738
+ // both would blank it on the children and
739
+ // defeat that, so pick one.)
740
+ //
741
+ // Distinct from `countLabel` too, which
742
+ // templates a summary from the child COUNT and
743
+ // so can only produce one fixed wording. An
744
+ // alias reads a field the backend computed,
745
+ // which is what it takes for the summary to
746
+ // vary with the data ("2 Headers" for one kind
747
+ // of group, "2 Stations" for another).
748
+ Object.entries(parentFieldAliases).forEach(
749
+ ([columnField, sourceField]) => {
750
+ parent[columnField] =
751
+ children[0]?.[sourceField];
752
+ }
753
+ );
754
+
715
755
  // Mark children for styling (getRowStyle) and
716
756
  // prefix their display value so nesting reads
717
757
  children.forEach((child) => {
@@ -959,6 +999,80 @@ const DataGrid = forwardRef(
959
999
  [ajaxSetting]
960
1000
  );
961
1001
 
1002
+ /**
1003
+ * Opt-in non-colour row states (`ajaxSetting.rowStyles`).
1004
+ *
1005
+ * rowStyles: [
1006
+ * { id: 'jigging_status', value: 1, style: 'complete' }
1007
+ * ]
1008
+ *
1009
+ * Each rule names a FIELD STATE, not a look: `style` is a token the
1010
+ * package maps to a class in DataGrid.module.scss, so a project says
1011
+ * what a row means and the library decides how that reads. Today the
1012
+ * only token is `complete` (line-through plus a muted ink).
1013
+ *
1014
+ * Deliberately different from `rowColours` in two ways:
1015
+ *
1016
+ * - EVERY matching rule applies, where rowColours takes the first
1017
+ * match only. A row is frequently more than one thing at once (a
1018
+ * finished item that is also on a T-Bar), and with first-match the
1019
+ * completion state would be swallowed by whichever tint happened to
1020
+ * be listed above it.
1021
+ * - It carries no colour. That is the point: a pale tint is the first
1022
+ * thing to disappear on a plant-floor tablet in daylight, so a state
1023
+ * that matters has to be legible without one. Colour, where a rule
1024
+ * also sets one, stays the redundant second channel.
1025
+ *
1026
+ * `operator` follows the same small vocabulary as `readOnlyWhen`:
1027
+ * eq (default), neq, notEmpty.
1028
+ */
1029
+ const getRowClassName = useCallback(
1030
+ ({ data }) => {
1031
+ const rules = ajaxSetting?.rowStyles;
1032
+
1033
+ if (!Array.isArray(rules) || rules.length === 0 || !data) {
1034
+ return '';
1035
+ }
1036
+
1037
+ const matches = (rule) => {
1038
+ const value = data[rule.id];
1039
+
1040
+ switch (rule.operator) {
1041
+ case 'neq':
1042
+ return value != rule.value;
1043
+ case 'notEmpty':
1044
+ return Array.isArray(value)
1045
+ ? value.length > 0
1046
+ : value != null && value !== '';
1047
+ case 'eq':
1048
+ default:
1049
+ return value == rule.value;
1050
+ }
1051
+ };
1052
+
1053
+ return rules
1054
+ .filter(matches)
1055
+ .map((rule) => ROW_STYLE_CLASSES[rule.style])
1056
+ .filter(Boolean)
1057
+ .join(' ');
1058
+ },
1059
+ [ajaxSetting]
1060
+ );
1061
+
1062
+ /**
1063
+ * The grid's own row class plus whatever `rowStyles` adds. Passed as a
1064
+ * function rather than the bare string the grid used to get: the grid
1065
+ * accepts either, and `vs-datagrid--row` (the hover rule) has to stay
1066
+ * on every row regardless.
1067
+ */
1068
+ const rowClassName = useCallback(
1069
+ (rowProps) =>
1070
+ ['vs-datagrid--row', getRowClassName(rowProps)]
1071
+ .filter(Boolean)
1072
+ .join(' '),
1073
+ [getRowClassName]
1074
+ );
1075
+
962
1076
  const handleDownload = async (d) => {
963
1077
  const toastId = toast.loading('Starting download please wait...');
964
1078
 
@@ -5205,7 +5319,7 @@ const DataGrid = forwardRef(
5205
5319
  pagination
5206
5320
  renderRowContextMenu={renderRowContextMenu}
5207
5321
  renderColumnContextMenu={renderColumnContextMenu}
5208
- rowClassName="vs-datagrid--row"
5322
+ rowClassName={rowClassName}
5209
5323
  rowHeight={isTabletMode ? 52 : null}
5210
5324
  rowStyle={getRowStyle}
5211
5325
  selected={selected}
@@ -40,6 +40,55 @@ const buildToggleUrl = (urlTemplate, stageItem, rowData) => {
40
40
  return url;
41
41
  };
42
42
 
43
+ /**
44
+ * Evaluate an opt-in stageConfig rule ({key, operator, value}) against one
45
+ * entry of a stage list. Absent rule -> false, so every setting that uses this
46
+ * is inert until a config asks for it.
47
+ *
48
+ * Operators: eq (default), neq, gte, gt, lte, lt, in. The comparison operators
49
+ * coerce to Number, because the values these rules match on arrive as strings
50
+ * from some payloads and as numbers from others, and a stage ordinal compared
51
+ * as a string sorts "10" below "2".
52
+ */
53
+ const matchesStageRule = (rule, stageItem) => {
54
+ if (!rule || !rule.key || !stageItem) {
55
+ return false;
56
+ }
57
+
58
+ const value = stageItem[rule.key];
59
+
60
+ if (value === null || value === undefined) {
61
+ return false;
62
+ }
63
+
64
+ if (rule.operator === 'in') {
65
+ return (
66
+ Array.isArray(rule.value) &&
67
+ rule.value.some((candidate) => candidate == value)
68
+ );
69
+ }
70
+
71
+ const left = Number(value);
72
+ const right = Number(rule.value);
73
+ const comparable = !Number.isNaN(left) && !Number.isNaN(right);
74
+
75
+ switch (rule.operator) {
76
+ case 'neq':
77
+ return value != rule.value;
78
+ case 'gte':
79
+ return comparable && left >= right;
80
+ case 'gt':
81
+ return comparable && left > right;
82
+ case 'lte':
83
+ return comparable && left <= right;
84
+ case 'lt':
85
+ return comparable && left < right;
86
+ case 'eq':
87
+ default:
88
+ return value == rule.value;
89
+ }
90
+ };
91
+
43
92
  const getStageStatus = (stageItem, toggleConfig) => {
44
93
  if (!toggleConfig || !toggleConfig.statusField) return null;
45
94
 
@@ -2895,6 +2944,30 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate, onUpda
2895
2944
  });
2896
2945
  }
2897
2946
 
2947
+ // `completeWhen` (opt-in): the rule that says this
2948
+ // entry is DONE as far as the grid showing it is
2949
+ // concerned, drawn as a rule through the label plus a
2950
+ // step back in weight.
2951
+ //
2952
+ // It lives in the view config rather than in the data
2953
+ // because "complete" is a statement about the VIEWER,
2954
+ // not about the entry: the same stage list is served
2955
+ // unchanged to every grid, and a member sitting at
2956
+ // Despatch is finished business to the Despatch list
2957
+ // while still being work in progress to Pickling. The
2958
+ // grid is the only party that knows which it is.
2959
+ //
2960
+ // Not a colour, on purpose: the pill's fill already
2961
+ // encodes the stage, and a second hue on a tablet in
2962
+ // daylight is the signal least likely to survive the
2963
+ // reading conditions. The rule through the label is
2964
+ // legible in glare and in greyscale; the fill stays as
2965
+ // it was, so nothing is lost by adding this.
2966
+ const isStageComplete = matchesStageRule(
2967
+ column.stageConfig.completeWhen,
2968
+ stageItem
2969
+ );
2970
+
2898
2971
  // Apply visual feedback for inactive stages
2899
2972
  const stageStyle = {
2900
2973
  width: useFullLabel ? 'auto' : '20px',
@@ -2911,7 +2984,21 @@ export const renderStageCounterColumn = ({ column, commonProps, navigate, onUpda
2911
2984
  fontSize: '11px',
2912
2985
  fontWeight: 'bold',
2913
2986
  cursor: isToggleable ? 'pointer' : 'default',
2914
- opacity: isStageActive ? 1 : 0.5,
2987
+ ...(isStageComplete
2988
+ ? {
2989
+ textDecoration: 'line-through',
2990
+ textDecorationThickness:
2991
+ 'var(--grid-stage-complete-thickness, 2px)',
2992
+ }
2993
+ : {}),
2994
+ // An inactive toggle already mutes to 0.5; a
2995
+ // complete entry steps back a little less, so the
2996
+ // two states stay tellable apart when both apply
2997
+ opacity: !isStageActive
2998
+ ? 0.5
2999
+ : isStageComplete
3000
+ ? 'var(--grid-stage-complete-opacity, 0.65)'
3001
+ : 1,
2915
3002
  transition: 'opacity 0.2s ease, transform 0.1s ease',
2916
3003
  border: isToggleable ? '1px solid rgba(255,255,255,0.3)' : 'none',
2917
3004
  };
@@ -1354,3 +1354,51 @@
1354
1354
  }
1355
1355
 
1356
1356
 
1357
+
1358
+ /* =========================================================================
1359
+ Row states (`ajaxSetting.rowStyles`)
1360
+
1361
+ A row tint is the first thing to go on a plant-floor tablet: pale fills
1362
+ are chosen to sit under black text, which is exactly what makes them
1363
+ vanish under glare or a low-angle view. The rows that carry these states
1364
+ are the ones a crew scans a list FOR ("which of these headers is already
1365
+ done?"), so the state has to survive the conditions the list is read in.
1366
+
1367
+ The treatment is therefore a rule through the text plus a step back in
1368
+ ink weight — two signals, neither of them hue. Any `rowColours` tint on
1369
+ the same row is untouched and becomes the redundant third.
1370
+
1371
+ Strike and ink are applied to the CELLS, not the row: the grid lays its
1372
+ cells out absolutely, so `text-decoration` set on the row element has no
1373
+ in-flow descendants to inherit it and would draw nothing.
1374
+
1375
+ Density hooks, per the `--nav-*` / `--grid-legend-*` convention above —
1376
+ every declaration is `var(--hook, <the value it always had>)`, so an unset
1377
+ hook is a no-op and a project retunes from `:root`:
1378
+
1379
+ --grid-row-complete-ink struck-through text colour.
1380
+ default: var(--muted-color, #6b7688)
1381
+ --grid-row-complete-thickness weight of the rule through the text.
1382
+ default: 2px — heavier than the browser
1383
+ default, which is hairline at this size.
1384
+ --grid-row-complete-opacity applies to non-text cell content (status
1385
+ pills, icons), which a rule cannot cross.
1386
+ default: 0.7
1387
+ ========================================================================= */
1388
+
1389
+ .rowComplete {
1390
+ :global(.InovuaReactDataGrid__cell) {
1391
+ color: var(--grid-row-complete-ink, var(--muted-color, #6b7688));
1392
+ text-decoration: line-through;
1393
+ text-decoration-thickness: var(--grid-row-complete-thickness, 2px);
1394
+ }
1395
+
1396
+ // Cell content that is drawn rather than set — the action buttons, the
1397
+ // stage pills, the boolean ticks — takes the muting alone, since a rule
1398
+ // through the text cannot cross it.
1399
+ :global(.InovuaReactDataGrid__cell) svg,
1400
+ :global(.InovuaReactDataGrid__cell) img,
1401
+ :global(.InovuaReactDataGrid__cell) button {
1402
+ opacity: var(--grid-row-complete-opacity, 0.7);
1403
+ }
1404
+ }