@visns-studio/visns-components 6.28.0 → 6.31.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,110 @@ 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.31.0)
13
+
14
+ ### The header badge self-heals too, and the pop takes its timings from the server
15
+
16
+ Follow-up to 6.30.0. That release stopped the two holders of the shared
17
+ `call-queue-monitor.{env}` channel from destroying each other's subscription,
18
+ and taught the call pop to notice a missing channel and rebuild it. This one
19
+ finishes the job.
20
+
21
+ - **`useZoomPhoneLive` / `ZoomPhoneBadge` resubscribe the same way the pop
22
+ does** — on every socket reconnect and every `resubscribeCheckMs` (new
23
+ option/prop, default 60 s, `0` disables) while the tab is visible. Before,
24
+ a badge whose channel was taken away kept its green `Live` stamp and never
25
+ received another presence event.
26
+ - **`hasPrivateChannel(instance, name, held)`** takes the subscription the
27
+ caller is listening on. With two self-healing holders, whichever rebuilt
28
+ first left the registry looking healthy while the other still held the dead
29
+ object; passing what you hold makes the answer "no" until you re-acquire.
30
+ - **`CallQueuePop` adopts `missed_grace_ms` and `max_ringing_ms` from the
31
+ live snapshot** when the server sends them, so the browser and the server
32
+ agree on how long a declined leg keeps the card. The `missedGraceMs` prop's
33
+ default is now **20000** (it was 10000 — half the server's, so a card left
34
+ 10 s before the server considered the call gone).
35
+ - **Phantom cards expire.** New `maxRingingMs` prop (default 120000, or the
36
+ snapshot's `max_ringing_ms`): a card whose `startedAt` is older than that
37
+ is removed and logged — a lost ended webhook must not ring on a screen
38
+ forever. `expiredCallIds(calls, now, maxRingingMs)` is exported.
39
+
40
+ ## Recent Updates (v6.30.0)
41
+
42
+ ### `CallQueuePop` no longer loses its channel to the header's phone badge
43
+
44
+ Two components listen on the same private channel (`call-queue-monitor.{env}`):
45
+ the call pop, mounted at the app root, and `ZoomPhoneBadge` in the header. A
46
+ host that hides its header on some routes (the CRM does, on client and task
47
+ record pages) unmounted the badge, whose cleanup called `echo.leave()` — and
48
+ laravel-echo's `leave()` destroys a channel for every holder, not just the
49
+ caller. The pop kept reporting `subscribed: true` and simply stopped popping
50
+ until the tab was reloaded. Production's webhook ledger showed every ring
51
+ broadcast OK, which is exactly why this took a while to find.
52
+
53
+ - **`acquirePrivateChannel` / `releasePrivateChannel` / `hasPrivateChannel`**
54
+ (new, exported): a ref-counted front for `echo.private()` / `echo.leave()`.
55
+ The channel is opened on the first acquire and only really left when the
56
+ last holder releases it. Both `CallQueuePop` and `useZoomPhoneLive` go
57
+ through it. **A host component that listens on a channel one of ours also
58
+ listens on must use it too** — a direct `echo.leave()` will still take the
59
+ channel away from everyone.
60
+ - **The pop rechecks its channel** on every socket reconnect and every
61
+ `resubscribeCheckMs` (new prop, default 60 s, `0` disables) while the tab is
62
+ visible, and resubscribes when it finds the channel gone — then refetches
63
+ the live snapshot so anything that rang meanwhile is caught up.
64
+ - **Diagnostics gain a "Channel live" row** beside "Subscribed", fed by the
65
+ new `channelPresent` status field (`noteCallPopChannelPresent`, exported).
66
+ The two readings disagreeing is the symptom this release fixes; before,
67
+ there was no second reading.
68
+
69
+ ## Recent Updates (v6.29.0)
70
+
71
+ ### `TicketConversation` names who a reply reaches
72
+
73
+ A helpdesk reply goes to everybody on the email chain, not to the one person
74
+ whose message happens to be on screen — and on a forwarded ticket that is
75
+ regularly somebody the agent has never seen. The composer said *"This is
76
+ emailed to the customer."*, which is true and is not enough to check a send
77
+ against.
78
+
79
+ The server now says who, in two places, and the component draws both:
80
+
81
+ ```jsonc
82
+ // GET {base}/{ticketId}/conversation
83
+ {
84
+ "reply_audience": { // or null — not answerable by email, or unknown
85
+ "to": [{ "name": "Dana Reid", "address": "dana@acme.test" }],
86
+ "cc": [{ "name": null, "address": "accounts@acme.test" }]
87
+ },
88
+ "data": [
89
+ { "id": 4, "direction": "out", "recipients": { "to": [], "cc": [] } },
90
+ { "id": 3, "direction": "note", "recipients": null }
91
+ ]
92
+ }
93
+ ```
94
+
95
+ - **Before sending** — the `.consequence` sentence beside the Internal note
96
+ switch becomes `Emailed to Dana Reid · Cc accounts@acme.test` (the ` · Cc …`
97
+ half is omitted when Cc is empty). An internal note is unchanged, and a
98
+ `reply_audience` of `null` falls back to the old sentence rather than naming
99
+ nobody.
100
+ - **After sending** — each entry carrying `recipients` draws one quiet
101
+ `To … · Cc …` line between its head and its body. **Both directions**: on our
102
+ reply it says who we reached, on the customer's it says who else at their end
103
+ has already read it, which is what tells an agent whether the answer needs to
104
+ go anywhere new. Nothing on a note, or on a row imported before the server
105
+ knew any of this (`recipients: null`).
106
+ - A recipient renders as `name || address`; the **full `Name <address>` list is
107
+ in the `title`**, because two people called Dana is exactly the case a name
108
+ alone cannot settle.
109
+ - **Nothing is folded to "+3 more".** The recipient worth noticing is the
110
+ unexpected one, which is the one a fold would hide — so both lines wrap and
111
+ take the space they need.
112
+
113
+ The `POST {base}/{ticketId}/reply` response's `data` carries `recipients` too,
114
+ so a reply prepended by the composer shows its own line immediately.
115
+
12
116
  ## Recent Updates (v6.27.0)
13
117
 
14
118
  ### A tint on one column: `cellColours`
@@ -2214,6 +2318,9 @@ endpoint nor an Echo instance present it renders nothing and logs nothing.
2214
2318
  | `syncChannelName` | `'throughlife-call-queue-pop'` | The `BroadcastChannel` that keeps every open tab's stack in step. Falsy switches cross-tab sync off. |
2215
2319
  | `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. |
2216
2320
  | `demoEnabled` | `true` | Registers `window.callPopDemo()` / `window.callPopClear()` for reviewing the UI without a backend. |
2321
+ | `missedGraceMs` | `20000` | How long a card survives a `.queue.missed` before it is taken as gone — a call rings several devices, so one declined leg only starts this timer and a further `.queue.ringing` cancels it. Was `10000`, half the server's own `missed_grace_seconds`, which took the card off screen ten seconds before the server considered the call gone. The snapshot's `missed_grace_ms` wins over it. |
2322
+ | `maxRingingMs` | `120000` | The backstop for an end that never arrived: a card still ringing after this is removed client-side, because a dropped `.queue.ended` otherwise leaves a phantom card ringing all afternoon. Demo cards and cards already leaving are exempt. The snapshot's `max_ringing_ms` wins over it. |
2323
+ | `resubscribeCheckMs` | `60000` | How often the pop confirms its Echo channel is still subscribed (visible tabs only), resubscribing and refetching the snapshot when it is not. `0` turns the check off. |
2217
2324
 
2218
2325
  **Payload contract.** Snake_case and camelCase are both accepted, so a Laravel
2219
2326
  resource passes through untouched: `call_id`/`callId`, `queue_id`/`queueId`,
@@ -2223,6 +2330,22 @@ resource passes through untouched: `call_id`/`callId`, `queue_id`/`queueId`,
2223
2330
  The snapshot's `pickup_codes` map is keyed by Zoom call queue id — a queue
2224
2331
  absent from it still pops, its card simply has no Pick up button.
2225
2332
 
2333
+ The snapshot carries three optional settings alongside the calls, and each wins
2334
+ over the matching prop once it lands — the server holds the webhook ledger and
2335
+ the config, so it is the side that decides when a call is over:
2336
+
2337
+ ```jsonc
2338
+ {
2339
+ "calls": [ /* … */ ],
2340
+ "pickup_codes": { "77": "*996439" },
2341
+ "channel": "call-queue-monitor.production", // environment-scoped Echo channel
2342
+ "missed_grace_ms": 20000, // overrides `missedGraceMs`
2343
+ "max_ringing_ms": 120000 // overrides `maxRingingMs`
2344
+ }
2345
+ ```
2346
+
2347
+ An older backend sends none of them and the props stand in unchanged.
2348
+
2226
2349
  Named exports for testing: `toLocalDigits`, `formatAuPhone`, `formatEventDate`,
2227
2350
  `formatDueDate`, `toCallWorkspaceId`, `normaliseCall`, `normalisePickupCodes`,
2228
2351
  `formatElapsed`, `hasMonitorPermission`, `clientDetails`.
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.28.0",
96
+ "version": "6.31.0",
97
97
  "description": "Various packages to assist in the development of our Custom Applications.",
98
98
  "main": "src/index.js",
99
99
  "files": [
@@ -559,6 +559,19 @@ const CallQueueDiagnostics = ({
559
559
  ? 'bad'
560
560
  : 'ok'
561
561
  )}
562
+ {/* Deliberately beside "Subscribed", not folded into
563
+ it: a channel torn down by somebody else's
564
+ `echo.leave()` leaves "Subscribed" stuck on Yes, and
565
+ the two readings disagreeing is the symptom. */}
566
+ {row(
567
+ 'Channel live',
568
+ yesNo(status.channelPresent, 'not checked'),
569
+ status.channelPresent === false
570
+ ? 'bad'
571
+ : status.channelPresent === true
572
+ ? 'ok'
573
+ : undefined
574
+ )}
562
575
  {row(
563
576
  'Last event',
564
577
  status.lastEventName