@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 +123 -0
- package/package.json +1 -1
- package/src/components/callQueue/CallQueueDiagnostics.jsx +13 -0
- package/src/components/callQueue/CallQueuePop.jsx +308 -43
- package/src/components/callQueue/callPopStatus.js +23 -0
- package/src/components/callQueue/callQueueHelpers.js +66 -0
- package/src/components/echo/sharedPrivateChannel.js +217 -0
- package/src/components/phone/ZoomPhoneBadge.jsx +5 -0
- package/src/components/phone/phonePresenceHelpers.js +31 -0
- package/src/components/phone/useZoomPhoneLive.js +147 -7
- package/src/components/styles/TicketConversation.module.scss +22 -1
- package/src/components/tickets/TicketConversation.jsx +96 -5
- package/src/index.js +15 -0
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.
|
|
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
|