@visns-studio/visns-components 6.26.0 → 6.28.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.
package/README.md CHANGED
@@ -9,6 +9,51 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
9
9
 
10
10
  VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
11
11
 
12
+ ## Recent Updates (v6.27.0)
13
+
14
+ ### A tint on one column: `cellColours`
15
+
16
+ A DataGrid could already tint a whole ROW from its data —
17
+ `ajaxSetting.rowColours`, first match wins:
18
+
19
+ ```js
20
+ ajaxSetting: {
21
+ rowColours: [{ id: 'priority', value: 'high', colour: '#FFCDD2' }],
22
+ }
23
+ ```
24
+
25
+ Which is the right instrument for "this record needs attention" and the wrong
26
+ one for "these are Sam's". A grid where every row is coloured has no signal
27
+ left in colour at all — the eye stops reading it after the second full-width
28
+ band. `cellColours` is the same rule shape declared on a COLUMN, painted onto
29
+ that one cell:
30
+
31
+ ```js
32
+ {
33
+ id: ['owner'],
34
+ label: 'Owner',
35
+ type: 'relation',
36
+ nameFrom: 'name',
37
+ cellColours: [
38
+ { id: 'owner_id', value: 19, colour: '#E1BEE7' },
39
+ { id: 'owner_id', value: 30, colour: '#BBDEFB' },
40
+ ],
41
+ }
42
+ ```
43
+
44
+ - **`id` names a field on the ROW, not on the column.** The value worth
45
+ matching is usually the id sitting behind the label the column renders
46
+ (`owner_id` under an Owner relation column), and the cell never shows it.
47
+ - **Loose equality**, as `rowStyles` uses — a JSON config writes `19` and an
48
+ API hands back `"19"`, and a config author should not have to know which.
49
+ - **First match wins**, as `rowColours` does. A cell has one background.
50
+ - **Every column type**, because the rule is applied by the shared column
51
+ `style`, not by any one renderer — relation, text, date, currency alike.
52
+ - **It composes with `rowColours`.** The row tint is a background on the row
53
+ element and the cell's own paints on top of it, so a tinted cell reads as
54
+ itself on a tinted row, and keeps its colour under the hover surface (which
55
+ is also below the cell).
56
+
12
57
  ## Recent Updates (v6.17.0)
13
58
 
14
59
  ### A client's text messages, on the client's own page
@@ -2167,6 +2212,7 @@ endpoint nor an Echo instance present it renders nothing and logs nothing.
2167
2212
  | `calendarPath` | `'/calendar'` | |
2168
2213
  | `callWorkspacePath` | `'/call/{number}'` | Template, or `(workspaceId, call) => path`. `{number}` is the `61…` form the workspace route expects. |
2169
2214
  | `syncChannelName` | `'throughlife-call-queue-pop'` | The `BroadcastChannel` that keeps every open tab's stack in step. Falsy switches cross-tab sync off. |
2215
+ | `clientDetailFields` | `CLIENT_DETAIL_FIELDS` (adviser / coding / age / city) | `[{ key, label, demo? }]` — the client-block rows, in card order; rows with an empty value are dropped; `demo` seeds the demo card. Pass your CRM's own fields so the card never names a field it does not have. Must be referentially stable (a module-scope constant, not an inline literal) — it sits in the demo effect's dependency list. |
2170
2216
  | `demoEnabled` | `true` | Registers `window.callPopDemo()` / `window.callPopClear()` for reviewing the UI without a backend. |
2171
2217
 
2172
2218
  **Payload contract.** Snake_case and camelCase are both accepted, so a Laravel
@@ -2181,6 +2227,11 @@ Named exports for testing: `toLocalDigits`, `formatAuPhone`, `formatEventDate`,
2181
2227
  `formatDueDate`, `toCallWorkspaceId`, `normaliseCall`, `normalisePickupCodes`,
2182
2228
  `formatElapsed`, `hasMonitorPermission`, `clientDetails`.
2183
2229
 
2230
+ `CLIENT_DETAIL_FIELDS`, `clientDetails` and `demoClientDetails` are exported
2231
+ from the package itself (`@visns-studio/visns-components`), so a host can build
2232
+ its own field list beside the default or reuse the demo seeding; the helpers
2233
+ module is not re-exported wholesale, only those three alongside the component.
2234
+
2184
2235
  #### CallQueueSettings
2185
2236
 
2186
2237
  The admin table behind the pop: one row per Zoom call queue, carrying the
package/package.json CHANGED
@@ -93,7 +93,7 @@
93
93
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
94
94
  },
95
95
  "name": "@visns-studio/visns-components",
96
- "version": "6.26.0",
96
+ "version": "6.28.0",
97
97
  "description": "Various packages to assist in the development of our Custom Applications.",
98
98
  "main": "src/index.js",
99
99
  "files": [
@@ -128,6 +128,31 @@ const ROW_STYLE_CLASSES = {
128
128
  complete: styles.rowComplete,
129
129
  };
130
130
 
131
+ /**
132
+ * First matching per-column `cellColours` rule for a row.
133
+ *
134
+ * cellColours: [
135
+ * { id: 'owner_id', value: 19, colour: '#E1BEE7' }
136
+ * ]
137
+ *
138
+ * The column-level twin of `ajaxSetting.rowColours`: the same rule shape,
139
+ * painted onto ONE cell instead of the whole row. A grid where every row is
140
+ * tinted is a grid with no signal left, so a view that only needs to tell its
141
+ * owners apart tints the Owner column and leaves the rest of the row alone.
142
+ *
143
+ * `id` names a field on the ROW, not on the column, because the value worth
144
+ * matching is usually the id sitting behind the label the column renders
145
+ * (`owner_id` under an Owner relation column) and the cell never shows it.
146
+ *
147
+ * Loose equality, as `rowStyles` uses: a JSON config writes 19 and an API
148
+ * hands back "19", and a config author should not have to know which. First
149
+ * match wins, as `rowColours` does — a cell has exactly one background.
150
+ */
151
+ const findCellColour = (column, data) =>
152
+ Array.isArray(column?.cellColours) && data
153
+ ? column.cellColours.find((rule) => data[rule.id] == rule.value)
154
+ : undefined;
155
+
131
156
  import '@visns-studio/visns-datagrid-enterprise/index.css';
132
157
 
133
158
  import CustomFetch from './Fetch';
@@ -4438,6 +4463,26 @@ const DataGrid = forwardRef(
4438
4463
  columnsMetadata
4439
4464
  );
4440
4465
 
4466
+ // A `cellColours` tint is an inline background on the
4467
+ // cell, and global-datagrid.css paints every cell
4468
+ // `background: transparent !important` (it has to
4469
+ // outrank the vendored theme) — inline loses to
4470
+ // `!important`, so the tint would never be seen. The
4471
+ // literal class marks the cells that carry one and
4472
+ // that rule stands down for them, exactly as
4473
+ // `vs-datagrid--row-tinted` does a level up. The
4474
+ // hover and selected surfaces live on the ROW, so the
4475
+ // tint keeps painting over them and survives a hover.
4476
+ const cellColourClassName = (cellProps, extra = '') =>
4477
+ [
4478
+ findCellColour(column, cellProps?.data)
4479
+ ? 'vs-datagrid--cell-tinted'
4480
+ : '',
4481
+ extra,
4482
+ ]
4483
+ .filter(Boolean)
4484
+ .join(' ');
4485
+
4441
4486
  const commonProps = {
4442
4487
  header: column.label,
4443
4488
  defaultFlex: 1,
@@ -4481,7 +4526,7 @@ const DataGrid = forwardRef(
4481
4526
  ? style.text_vertical_align
4482
4527
  : 'center',
4483
4528
  style: (cellProps) => {
4484
- const { id, value } = cellProps;
4529
+ const { id, value, data } = cellProps;
4485
4530
  let columnStyle = {};
4486
4531
 
4487
4532
  if (
@@ -4501,8 +4546,24 @@ const DataGrid = forwardRef(
4501
4546
  };
4502
4547
  }
4503
4548
 
4549
+ // `cellColours` last, so a column's static
4550
+ // `style` cannot quietly outrank the rule the
4551
+ // view asked for. It paints over any
4552
+ // `rowColours` tint for this one cell without
4553
+ // touching it: the row tint is a background on
4554
+ // the ROW element and this one sits on the cell
4555
+ // above it.
4556
+ const cellColour = findCellColour(column, data);
4557
+
4558
+ if (cellColour) {
4559
+ columnStyle.backgroundColor =
4560
+ cellColour.colour;
4561
+ }
4562
+
4504
4563
  return columnStyle;
4505
4564
  },
4565
+ className: (cellProps) =>
4566
+ cellColourClassName(cellProps),
4506
4567
  type: column.type,
4507
4568
  };
4508
4569
 
@@ -4966,9 +5027,13 @@ const DataGrid = forwardRef(
4966
5027
  name: columnId,
4967
5028
  filterEditor: filterEditor,
4968
5029
  filterEditorProps: filterEditorProps,
4969
- className: finalWordWrap
4970
- ? 'cell-word-wrap'
4971
- : '',
5030
+ className: (cellProps) =>
5031
+ cellColourClassName(
5032
+ cellProps,
5033
+ finalWordWrap
5034
+ ? 'cell-word-wrap'
5035
+ : ''
5036
+ ),
4972
5037
  render: ({ data }) => {
4973
5038
  if (
4974
5039
  data &&
@@ -40,6 +40,7 @@ import {
40
40
  defaultClientTasksUrl,
41
41
  defaultClientUrl,
42
42
  defaultTaskUrl,
43
+ demoClientDetails,
43
44
  directRingingLine,
44
45
  formatAuPhone,
45
46
  formatDueDate,
@@ -62,6 +63,7 @@ export {
62
63
  callBadgeLabel,
63
64
  calleeLabel,
64
65
  clientDetails,
66
+ demoClientDetails,
65
67
  directRingingLine,
66
68
  formatAuPhone,
67
69
  formatDueDate,
@@ -286,6 +288,16 @@ const CallQueuePop = ({
286
288
  calendarPath = '/calendar',
287
289
  callWorkspacePath = '/call/{number}',
288
290
  syncChannelName = SYNC_CHANNEL,
291
+ // The client-block rows, in card order — the host's own fields, so the
292
+ // card never names one its CRM does not have. The default is this
293
+ // component's original financial-planning shape (adviser / coding / age /
294
+ // city) and the demo card seeds itself from the same list, so a host that
295
+ // passes its own fields also demos its own fields.
296
+ //
297
+ // MUST be referentially stable — a module-scope constant, not an inline
298
+ // literal — because it sits in the demo effect's dependency list, and a
299
+ // fresh array per render would re-register the demo helpers every time.
300
+ clientDetailFields = CLIENT_DETAIL_FIELDS,
289
301
  demoEnabled = true,
290
302
  missedGraceMs = DEFAULT_MISSED_GRACE_MS,
291
303
  }) => {
@@ -680,20 +692,22 @@ const CallQueuePop = ({
680
692
  queueName: 'Test Call Queue',
681
693
  callerNumber: '+61298765432',
682
694
  callerName: 'Margaret Chen',
683
- // Every field the server's client block can carry, so the
684
- // rich card can be reviewed without a real match. id 0
695
+ // The rich card reviewed without a real match. The detail
696
+ // rows are seeded from `clientDetailFields` itself, so the
697
+ // demo shows exactly the host's own fields with the host's
698
+ // own sample values rather than another CRM's. id 0
685
699
  // renders the name as plain text, not a link.
686
700
  client: {
687
701
  id: 0,
688
702
  name: 'Chen, Margaret (Mrs)',
689
703
  matched_on: 'home',
690
- adviser: 'Alina Bailey',
691
- coding: 'Early Retirement',
692
- age: 66,
704
+ // Before the spread, so a host listing `email` among
705
+ // its own fields demos its own sample value while a
706
+ // host that does not still gets one.
693
707
  email: 'margaret.chen@example.com',
694
- city: 'Fremantle',
708
+ ...demoClientDetails(clientDetailFields),
695
709
  next_event: {
696
- title: 'Client - Annual Review',
710
+ title: 'Client review',
697
711
  date: new Date(
698
712
  Date.now() + 2 * 24 * 60 * 60 * 1000
699
713
  ).toISOString(),
@@ -705,19 +719,19 @@ const CallQueuePop = ({
705
719
  tasks: [
706
720
  {
707
721
  id: 90001,
708
- label: 'Confirm rollover paperwork',
722
+ label: 'Confirm the site visit time',
709
723
  due_date: new Date(
710
724
  Date.now() + 3 * 24 * 60 * 60 * 1000
711
725
  ).toISOString(),
712
- task_type: 'Superannuation',
726
+ task_type: 'Ticket',
713
727
  },
714
728
  {
715
729
  id: 90002,
716
- label: 'Send updated risk profile questionnaire',
730
+ label: 'Send the updated quote',
717
731
  due_date: new Date(
718
732
  Date.now() + 9 * 24 * 60 * 60 * 1000
719
733
  ).toISOString(),
720
- task_type: 'Insurance',
734
+ task_type: 'Quote',
721
735
  },
722
736
  ],
723
737
  startedAt: new Date().toISOString(),
@@ -745,6 +759,7 @@ const CallQueuePop = ({
745
759
  addDemoPickupCode,
746
760
  broadcastSync,
747
761
  clearCalls,
762
+ clientDetailFields,
748
763
  demoEnabled,
749
764
  dropDemoPickupCode,
750
765
  ]);
@@ -1707,7 +1722,7 @@ const CallQueuePop = ({
1707
1722
  {calls.map((call) => {
1708
1723
  const pickupCode = pickupCodeFor(call);
1709
1724
  const client = call.client;
1710
- const details = clientDetails(client);
1725
+ const details = clientDetails(client, clientDetailFields);
1711
1726
  const nextEvent = client?.next_event ?? null;
1712
1727
  const nextEventDate = formatEventDate(nextEvent?.date);
1713
1728
  const openTasks = Number(client?.open_tasks ?? 0);
@@ -210,32 +210,69 @@ export const defaultTaskUrl = (task) => `/tasks/detail/${task?.id}`;
210
210
  /** Default detail route for a matched client. */
211
211
  export const defaultClientUrl = (client) => `/clients/${client?.id}`;
212
212
 
213
- /** The client fields shown as label/value rows, in card order. */
213
+ /**
214
+ * The client fields shown as label/value rows, in card order — the DEFAULT,
215
+ * inherited from the financial-planning CRM this component was ported from.
216
+ * Each entry's `demo` is the sample value the demo card shows for it.
217
+ *
218
+ * A host whose caller enrichment returns a different shape passes its own list
219
+ * as `clientDetailFields` rather than living with rows it can never fill.
220
+ */
214
221
  export const CLIENT_DETAIL_FIELDS = [
215
- { key: 'adviser', label: 'Adviser' },
216
- { key: 'coding', label: 'Coding' },
217
- { key: 'age', label: 'Age' },
218
- { key: 'city', label: 'City' },
222
+ { key: 'adviser', label: 'Adviser', demo: 'Alina Bailey' },
223
+ { key: 'coding', label: 'Coding', demo: 'Early Retirement' },
224
+ { key: 'age', label: 'Age', demo: 66 },
225
+ { key: 'city', label: 'City', demo: 'Fremantle' },
219
226
  ];
220
227
 
221
228
  /**
222
229
  * Detail rows for a client block, with the empty ones dropped — a card only
223
230
  * ever shows what the CRM actually knows, never a row with a blank value.
231
+ *
232
+ * `fields` is the host's own list, so the card only ever names fields its CRM
233
+ * actually has. An entry with no string `key` is malformed and is ignored
234
+ * rather than drawn as a row with nothing behind it.
224
235
  */
225
- export const clientDetails = (client) => {
236
+ export const clientDetails = (client, fields = CLIENT_DETAIL_FIELDS) => {
226
237
  if (!client || typeof client !== 'object') {
227
238
  return [];
228
239
  }
229
240
 
230
- return CLIENT_DETAIL_FIELDS.map(({ key, label }) => ({
231
- label,
232
- value: client[key],
233
- })).filter(
234
- (row) =>
235
- row.value !== null &&
236
- row.value !== undefined &&
237
- String(row.value).trim() !== ''
238
- );
241
+ const list = Array.isArray(fields) ? fields : CLIENT_DETAIL_FIELDS;
242
+
243
+ return list
244
+ .filter((field) => field && typeof field.key === 'string')
245
+ .map(({ key, label }) => ({
246
+ label,
247
+ value: client[key],
248
+ }))
249
+ .filter(
250
+ (row) =>
251
+ row.value !== null &&
252
+ row.value !== undefined &&
253
+ String(row.value).trim() !== ''
254
+ );
255
+ };
256
+
257
+ /**
258
+ * The demo card's client block, built from the same list the real card draws
259
+ * from — so a host passing its own fields sees its own fields in the demo
260
+ * rather than another CRM's. An entry carrying no `demo` contributes nothing.
261
+ */
262
+ export const demoClientDetails = (fields = CLIENT_DETAIL_FIELDS) => {
263
+ const list = Array.isArray(fields) ? fields : CLIENT_DETAIL_FIELDS;
264
+
265
+ return list.reduce((carry, field) => {
266
+ if (!field || typeof field.key !== 'string') {
267
+ return carry;
268
+ }
269
+
270
+ if (field.demo === undefined) {
271
+ return carry;
272
+ }
273
+
274
+ return { ...carry, [field.key]: field.demo };
275
+ }, {});
239
276
  };
240
277
 
241
278
  /** Seconds since `startedAt` rendered as m:ss (never negative). */
@@ -715,11 +715,25 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
715
715
  .InovuaReactDataGrid__cell {
716
716
  border: 0 !important;
717
717
  border-bottom: 1px solid var(--dgx-line) !important;
718
- background: transparent !important;
719
718
  color: var(--dgx-ink) !important;
720
719
  font-size: 0.8125rem !important;
721
720
  }
722
721
 
722
+ /* The same story as the row surface above, one level down. A per-column
723
+ `cellColours` tint arrives as an INLINE background on the CELL, and inline
724
+ loses to `!important` — so a blanket transparent cell would erase every
725
+ column tint a view configures. Cells carrying one are marked
726
+ `vs-datagrid--cell-tinted` (written by the column's className in
727
+ DataGrid.jsx) and this rule stands down for them; nothing else here paints
728
+ a cell, so the inline tint wins over the vendored theme's non-important
729
+ surface.
730
+ Hover and selected stay untouched because they are declared on the ROW and
731
+ the row's cell wrap, which sit BELOW the cell — the tint paints over them
732
+ and stays visible on hover, which is the point of tinting the cell. */
733
+ .InovuaReactDataGrid__cell:not(.vs-datagrid--cell-tinted) {
734
+ background: transparent !important;
735
+ }
736
+
723
737
  .InovuaReactDataGrid__cell__content {
724
738
  padding: 0 0.85rem !important;
725
739
  line-height: 1.45 !important;
package/src/index.js CHANGED
@@ -141,6 +141,13 @@ import {
141
141
  import CallQueuePop from './components/callQueue/CallQueuePop';
142
142
  import CallQueueSettings from './components/callQueue/CallQueueSettings';
143
143
  import CallQueueDiagnostics from './components/callQueue/CallQueueDiagnostics';
144
+ // The client-block field list and its two readers, so a host can build its own
145
+ // `clientDetailFields` beside the default rather than restating the shape.
146
+ import {
147
+ CLIENT_DETAIL_FIELDS,
148
+ clientDetails,
149
+ demoClientDetails,
150
+ } from './components/callQueue/callQueueHelpers';
144
151
  import {
145
152
  CALL_POP_STATUS_EVENT,
146
153
  getCallPopStatus,
@@ -365,6 +372,9 @@ export {
365
372
  CallQueueDiagnostics,
366
373
  CallQueuePop,
367
374
  CallQueueSettings,
375
+ CLIENT_DETAIL_FIELDS,
376
+ clientDetails,
377
+ demoClientDetails,
368
378
  getCallPopStatus,
369
379
  updateCallPopStatus,
370
380
  cleanBase32,