@visns-studio/visns-components 6.32.3 → 6.34.3

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 (53) hide show
  1. package/README.md +483 -0
  2. package/package.json +1 -1
  3. package/src/components/DataGrid.jsx +44 -2
  4. package/src/components/Field.jsx +32 -0
  5. package/src/components/Form.jsx +39 -2
  6. package/src/components/Navigation.jsx +274 -15
  7. package/src/components/Notification.jsx +13 -2
  8. package/src/components/TableFilter.jsx +14 -2
  9. package/src/components/auth/Login.jsx +11 -4
  10. package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
  11. package/src/components/auth/Profile.jsx +422 -26
  12. package/src/components/auth/Reset.jsx +1 -1
  13. package/src/components/auth/TwoFactorAuth.jsx +1 -1
  14. package/src/components/auth/Verify.jsx +2 -2
  15. package/src/components/auth/authEndpoints.js +43 -0
  16. package/src/components/auth/passkeyClient.js +102 -0
  17. package/src/components/auth/passkeyPrompt.js +408 -0
  18. package/src/components/auth/profileLayout.js +45 -0
  19. package/src/components/controls/DataGridSearch.jsx +16 -2
  20. package/src/components/controls/DataGridSortSheet.jsx +2 -0
  21. package/src/components/emailCampaigns/BlockEditor.jsx +486 -0
  22. package/src/components/emailCampaigns/CampaignEditor.jsx +614 -0
  23. package/src/components/emailCampaigns/CampaignReport.jsx +98 -0
  24. package/src/components/emailCampaigns/EmailCampaigns.jsx +422 -0
  25. package/src/components/emailCampaigns/EmailLists.jsx +505 -0
  26. package/src/components/emailCampaigns/emailCampaignApi.js +135 -0
  27. package/src/components/generic/ActionButtons.jsx +1 -1
  28. package/src/components/generic/GenericAuth.jsx +197 -21
  29. package/src/components/generic/GenericDetail.jsx +95 -3
  30. package/src/components/generic/GenericMain.jsx +7 -0
  31. package/src/components/generic/StandardModal.jsx +71 -158
  32. package/src/components/sms/SmsCampaigns.jsx +2517 -0
  33. package/src/components/sms/SmsInbox.jsx +20 -5
  34. package/src/components/sms/SmsLineSettings.jsx +2 -5
  35. package/src/components/sms/SmsThreadPanel.jsx +20 -0
  36. package/src/components/sms/smsEndpoints.js +23 -0
  37. package/src/components/styles/DataGrid.module.scss +22 -0
  38. package/src/components/styles/EmailCampaigns.module.scss +922 -0
  39. package/src/components/styles/Form.module.scss +100 -2
  40. package/src/components/styles/Navigation.module.scss +322 -5
  41. package/src/components/styles/Notification.module.scss +140 -8
  42. package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
  43. package/src/components/styles/Profile.module.scss +157 -0
  44. package/src/components/styles/Sms.module.scss +484 -4
  45. package/src/components/styles/StandardModal.module.scss +273 -0
  46. package/src/components/styles/Vault.module.scss +5 -3
  47. package/src/components/styles/global.css +38 -7
  48. package/src/components/utils/navCollapsed.js +201 -0
  49. package/src/components/utils/rowActionSettings.js +76 -0
  50. package/src/components/utils/usePasskeysEnabled.js +84 -0
  51. package/src/components/utils/useShellLayout.js +131 -0
  52. package/src/index.js +107 -1
  53. package/src/utils/rememberTab.js +70 -0
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import {
3
3
  Inbox,
4
4
  Loader2,
5
- MessageSquare,
5
+ Megaphone,
6
6
  PenSquare,
7
7
  Unplug,
8
8
  Search,
@@ -103,6 +103,13 @@ const SmsInboxInner = ({
103
103
  echo = null,
104
104
  channelFor = defaultChannelFor,
105
105
  settingsUrl = '/settings/sms',
106
+ // Opt-in, and absent by default: a deployment with no campaigns screen
107
+ // must not grow a link to one. Drawn beside the settings link rather than
108
+ // beside "New message", because a campaign is somewhere you GO and a new
109
+ // message is something you DO — and it is deliberately a ghost button
110
+ // rather than a second primary, since the inbox's own primary action is
111
+ // still writing to one person.
112
+ campaignsUrl = null,
106
113
  clientUrl = (id) => `/clients/${id}`,
107
114
  // Both opt-in, both handed straight to the conversation pane — see
108
115
  // SmsThreadPanel for the shapes. Absent, this page is exactly what it was.
@@ -276,10 +283,8 @@ const SmsInboxInner = ({
276
283
  <div className={styles.inbox}>
277
284
  <div className={styles.inboxHead}>
278
285
  <div className={styles.inboxHeading}>
279
- <h1 className={styles.pageTitle}>
280
- <MessageSquare size={22} strokeWidth={2.2} aria-hidden="true" />
281
- {pageTitle}
282
- </h1>
286
+ {/* No icon: no other page title in a host CRM carries one. */}
287
+ <h1 className={styles.pageTitle}>{pageTitle}</h1>
283
288
  <p className={styles.pageIntro}>
284
289
  Text messages sent and received on the practice's phone lines.
285
290
  Replies come back to the line they were sent from.
@@ -301,6 +306,16 @@ const SmsInboxInner = ({
301
306
  </div>
302
307
 
303
308
  <div className={styles.headActions}>
309
+ {campaignsUrl && (
310
+ <button
311
+ type="button"
312
+ className={styles.ghostButton}
313
+ onClick={() => navigate(campaignsUrl)}
314
+ >
315
+ <Megaphone size={14} strokeWidth={2} aria-hidden="true" />
316
+ Campaigns
317
+ </button>
318
+ )}
304
319
  {canManage && (
305
320
  <button
306
321
  type="button"
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Loader2, MessageSquare, Plus, TriangleAlert, X } from 'lucide-react';
2
+ import { Loader2, Plus, TriangleAlert, X } from 'lucide-react';
3
3
  import { toast } from 'react-toastify';
4
4
 
5
5
  import CustomFetch from '../Fetch';
@@ -264,10 +264,7 @@ const SmsLineSettings = ({
264
264
  {pageTitle !== null && (
265
265
  <div className={styles.settingsHead}>
266
266
  <div>
267
- <h1 className={styles.pageTitle}>
268
- <MessageSquare size={22} strokeWidth={2.2} aria-hidden="true" />
269
- {pageTitle}
270
- </h1>
267
+ <h1 className={styles.pageTitle}>{pageTitle}</h1>
271
268
  <p className={styles.pageIntro}>
272
269
  A line is one Zoom Phone number and the staff who can see its
273
270
  conversations. Everything staff send goes out from the line they
@@ -12,6 +12,7 @@ import {
12
12
  ArrowLeft,
13
13
  CheckSquare,
14
14
  ChevronUp,
15
+ CircleSlash,
15
16
  ExternalLink,
16
17
  FileText,
17
18
  Link2,
@@ -1054,6 +1055,25 @@ const SmsThreadPanel = ({
1054
1055
  </div>
1055
1056
  ) : (
1056
1057
  <div className={styles.composer}>
1058
+ {/*
1059
+ * Opted out of CAMPAIGNS, which is not the same thing as
1060
+ * opted out of being spoken to.
1061
+ *
1062
+ * Nothing is disabled and nothing should be: this person
1063
+ * texted us, and refusing to let a member of staff answer a
1064
+ * question because a bulk send would skip them is the wrong
1065
+ * answer to the wrong question. What the line does is say so,
1066
+ * once, in the muted register — so a reply here is a decision
1067
+ * somebody made rather than one they made unaware.
1068
+ */}
1069
+ {thread?.opted_out && (
1070
+ <p className={styles.optedOutNote}>
1071
+ <CircleSlash size={13} strokeWidth={2} aria-hidden="true" />
1072
+ This number has opted out of messages. A reply is still allowed,
1073
+ but Zoom blocks texts to them until they reply START.
1074
+ </p>
1075
+ )}
1076
+
1057
1077
  <div className={styles.composerBox}>
1058
1078
  <textarea
1059
1079
  ref={composerRef}
@@ -36,6 +36,29 @@ export const makeSmsEndpoints = (base = '/ajax/sms') => ({
36
36
  templates: `${base}/templates`,
37
37
  template: (id) => `${base}/templates/${id}`,
38
38
 
39
+ // Bulk sends. `campaigns` is both the list and the create; everything else
40
+ // hangs off one campaign's id.
41
+ //
42
+ // `campaignPreview` is a POST that only reads, and it is a route rather
43
+ // than a client-side render on purpose: the server owns what `{first_name}`
44
+ // means, and a composer that filled the tokens itself would show a message
45
+ // that differed from the one that went out on any placeholder the two
46
+ // implementations disagreed about.
47
+ campaigns: `${base}/campaigns`,
48
+ campaignPreview: `${base}/campaigns/preview`,
49
+ campaign: (id) => `${base}/campaigns/${id}`,
50
+ campaignRecipients: (id) => `${base}/campaigns/${id}/recipients`,
51
+ campaignStart: (id) => `${base}/campaigns/${id}/start`,
52
+ campaignPause: (id) => `${base}/campaigns/${id}/pause`,
53
+ campaignCancel: (id) => `${base}/campaigns/${id}/cancel`,
54
+ campaignRetryFailed: (id) => `${base}/campaigns/${id}/retry-failed`,
55
+
56
+ // Numbers that must not be sent to again. Kept beside campaigns because a
57
+ // bulk send is the only place the list is applied in anger, but an opt-out
58
+ // is a fact about the number and outlives every campaign that honoured it.
59
+ optOuts: `${base}/opt-outs`,
60
+ optOut: (id) => `${base}/opt-outs/${id}`,
61
+
39
62
  settingsLines: `${base}/settings/lines`,
40
63
  settingsLine: (id) => `${base}/settings/lines/${id}`,
41
64
  });
@@ -478,6 +478,28 @@
478
478
  }
479
479
  }
480
480
 
481
+ /* A row action that is PRESENT but not permitted on this row — `disabled` on a
482
+ settings entry (see resolveRowActionDisabled in utils/rowActionSettings.js).
483
+ The icon stays in the cell so the column does not change shape from row to
484
+ row; it simply stops being an offer.
485
+
486
+ POINTER EVENTS ARE NOT TURNED OFF, and that is deliberate rather than an
487
+ omission: the whole value of this over `condition` is the tooltip that says
488
+ why, and a glyph with `pointer-events: none` can never be hovered to show
489
+ one. The click is swallowed in JS instead, where it can also be stopped from
490
+ reaching the row underneath. */
491
+ .vs-action--disabled {
492
+ cursor: not-allowed;
493
+ opacity: 0.38;
494
+
495
+ /* The hover tint below would otherwise say the icon is still live. */
496
+ .tdaction,
497
+ .tdaction:hover {
498
+ cursor: not-allowed;
499
+ color: #6b7280;
500
+ }
501
+ }
502
+
481
503
  .uploadSection {
482
504
  margin-bottom: 16px;
483
505
  border-bottom: 1px solid #e5e7eb;