@visns-studio/visns-components 6.6.3 → 6.15.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.
Files changed (100) hide show
  1. package/README.md +1658 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +619 -158
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/AutoGrowCell.jsx +141 -0
  32. package/src/components/columns/ColumnRenderers.jsx +53 -7
  33. package/src/components/generic/GenericAuth.jsx +163 -96
  34. package/src/components/generic/GenericDetail.jsx +215 -90
  35. package/src/components/generic/GenericIndex.jsx +20 -47
  36. package/src/components/generic/GenericMain.jsx +5 -0
  37. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  38. package/src/components/generic/StandardModal.jsx +17 -5
  39. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  40. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  41. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  42. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  44. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  45. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  46. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  47. package/src/components/navActive.js +60 -0
  48. package/src/components/notify/desktopNotifications.js +256 -0
  49. package/src/components/sketch/SketchField.jsx +12 -2
  50. package/src/components/sms/SmsComposeModal.jsx +495 -0
  51. package/src/components/sms/SmsInbox.jsx +596 -0
  52. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  53. package/src/components/sms/SmsLineSettings.jsx +854 -0
  54. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  55. package/src/components/sms/smsEndpoints.js +56 -0
  56. package/src/components/sms/smsHelpers.js +1075 -0
  57. package/src/components/sms/smsLiveState.js +132 -0
  58. package/src/components/sms/useSmsLive.js +307 -0
  59. package/src/components/styles/CallQueuePop.module.scss +646 -0
  60. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  61. package/src/components/styles/ClientAuth.module.scss +229 -0
  62. package/src/components/styles/DataGrid.module.scss +10 -0
  63. package/src/components/styles/GenericDetail.module.scss +153 -26
  64. package/src/components/styles/GenericIndex.module.scss +13 -0
  65. package/src/components/styles/GenericMain.module.scss +19 -2
  66. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  67. package/src/components/styles/Login.module.scss +13 -122
  68. package/src/components/styles/LogoutScreen.module.scss +89 -0
  69. package/src/components/styles/Navigation.module.scss +509 -223
  70. package/src/components/styles/Notification.module.scss +103 -0
  71. package/src/components/styles/Reset.module.scss +52 -186
  72. package/src/components/styles/Sms.module.scss +2382 -0
  73. package/src/components/styles/TableFilter.module.scss +118 -98
  74. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  75. package/src/components/styles/Vault.module.scss +1983 -0
  76. package/src/components/styles/Verify.module.scss +57 -180
  77. package/src/components/styles/_authBrandPanel.scss +163 -0
  78. package/src/components/styles/_authShell.scss +369 -0
  79. package/src/components/styles/global-datagrid.css +29 -0
  80. package/src/components/utils/buildEnv.js +67 -0
  81. package/src/components/utils/displayValue.js +94 -0
  82. package/src/components/utils/useDensity.js +345 -9
  83. package/src/components/vault/OtpChip.jsx +208 -0
  84. package/src/components/vault/PasswordGenerator.jsx +145 -0
  85. package/src/components/vault/QrScanner.jsx +326 -0
  86. package/src/components/vault/VaultAccessLog.jsx +225 -0
  87. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  88. package/src/components/vault/VaultEntryForm.jsx +740 -0
  89. package/src/components/vault/VaultManager.jsx +1000 -0
  90. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  91. package/src/components/vault/qrDecode.js +257 -0
  92. package/src/components/vault/useDebouncedValue.js +22 -0
  93. package/src/components/vault/useVaultReveal.js +160 -0
  94. package/src/components/vault/vaultClipboard.js +33 -0
  95. package/src/components/vault/vaultEndpoints.js +40 -0
  96. package/src/components/vault/vaultFit.js +116 -0
  97. package/src/components/vault/vaultHelpers.js +547 -0
  98. package/src/components/vault/vaultNavigation.jsx +58 -0
  99. package/src/components/vault/vaultOtp.js +105 -0
  100. package/src/index.js +233 -1
@@ -4,6 +4,9 @@ import { trackPromise } from 'react-promise-tracker';
4
4
  import { toast } from 'react-toastify';
5
5
  import parse from 'html-react-parser';
6
6
 
7
+ // import.meta.env is Vite-only; this reads it safely under any bundler.
8
+ import { readBuildEnv } from './utils/buildEnv';
9
+
7
10
  // Create a singleton QueryClient instance for the entire visns-components ecosystem
8
11
  const createQueryClient = () => {
9
12
  return new QueryClient({
@@ -165,7 +168,8 @@ const originalCustomFetch = (
165
168
  method,
166
169
  formData,
167
170
  successCallback = null,
168
- errorCallback = null
171
+ errorCallback = null,
172
+ options = {}
169
173
  ) => {
170
174
  const baseUrl = '';
171
175
 
@@ -191,7 +195,7 @@ const originalCustomFetch = (
191
195
  headers['X-XSRF-TOKEN'] = csrfToken;
192
196
  }
193
197
  const MAX_PAYLOAD_SIZE_MB =
194
- parseFloat(import.meta.env.VITE_MAX_PAYLOAD_SIZE_MB) || 4.5;
198
+ parseFloat(readBuildEnv('VITE_MAX_PAYLOAD_SIZE_MB')) || 4.5;
195
199
  const MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE_MB * 1024 * 1024; // Convert MB to bytes
196
200
 
197
201
  // Estimate headers size
@@ -304,6 +308,40 @@ const originalCustomFetch = (
304
308
  resolve(responseObj);
305
309
  })
306
310
  .catch((error) => {
311
+ /*
312
+ * `options.silentStatuses` — an opt-in list of HTTP statuses
313
+ * that are part of a flow rather than a failure, and so must
314
+ * not be announced.
315
+ *
316
+ * The vault's reveal endpoint answers 423 to mean "confirm
317
+ * your password first", which the caller handles by drawing a
318
+ * confirm panel. Without this, that perfectly ordinary step
319
+ * either toasts an error at the user or forces the caller to
320
+ * pass an errorCallback that can only see a message string —
321
+ * never the status it needs to branch on.
322
+ *
323
+ * A listed status skips the toast AND the errorCallback, and
324
+ * rejects with the status and parsed body hung directly off
325
+ * the error so `catch (e) { e.status; e.data }` works. Opt-in,
326
+ * so every existing caller behaves exactly as before.
327
+ */
328
+ const silentStatuses = Array.isArray(options.silentStatuses)
329
+ ? options.silentStatuses
330
+ : [];
331
+ const responseStatus = error.response
332
+ ? error.response.status
333
+ : undefined;
334
+
335
+ if (
336
+ responseStatus !== undefined &&
337
+ silentStatuses.includes(responseStatus)
338
+ ) {
339
+ error.status = responseStatus;
340
+ error.data = error.response.data;
341
+ reject(error);
342
+ return;
343
+ }
344
+
307
345
  let errorMessage = '';
308
346
 
309
347
  if (error.response && error.response.data) {
@@ -323,6 +361,21 @@ const originalCustomFetch = (
323
361
  errorMessage = data.message;
324
362
  }
325
363
  }
364
+
365
+ // Last resort, and only when nothing above found anything:
366
+ // this library's own backends answer a rejected auth
367
+ // attempt with `{error: '…'}` and no `message` at all, and
368
+ // on a non-200 that body used to be dropped entirely — the
369
+ // caller got an empty toast and no idea why it failed.
370
+ // Ordered after `message` so nothing that already surfaced
371
+ // changes.
372
+ if (
373
+ !errorMessage &&
374
+ typeof data.error === 'string' &&
375
+ data.error !== ''
376
+ ) {
377
+ errorMessage = data.error;
378
+ }
326
379
  } else {
327
380
  errorMessage = error.message;
328
381
  }
@@ -386,6 +439,15 @@ const CustomFetch = (
386
439
  } else if (data.message) {
387
440
  errorMessage = data.message;
388
441
  }
442
+ // Same last-resort `{error}` fallback as the
443
+ // uncached path above.
444
+ if (
445
+ !errorMessage &&
446
+ typeof data.error === 'string' &&
447
+ data.error !== ''
448
+ ) {
449
+ errorMessage = data.error;
450
+ }
389
451
  } else {
390
452
  errorMessage = error.message;
391
453
  }
@@ -408,6 +470,14 @@ const CustomFetch = (
408
470
  errorMessage = data.message;
409
471
  }
410
472
  }
473
+ // Same last-resort `{error}` fallback as above.
474
+ if (
475
+ !errorMessage &&
476
+ typeof data.error === 'string' &&
477
+ data.error !== ''
478
+ ) {
479
+ errorMessage = data.error;
480
+ }
411
481
  } else {
412
482
  errorMessage = error.message;
413
483
  }
@@ -424,7 +494,14 @@ const CustomFetch = (
424
494
  } else {
425
495
  // Use original implementation for non-cacheable requests (mutations)
426
496
  const result = trackPromise(
427
- originalCustomFetch(url, method, formData, successCallback, errorCallback)
497
+ originalCustomFetch(
498
+ url,
499
+ method,
500
+ formData,
501
+ successCallback,
502
+ errorCallback,
503
+ options
504
+ )
428
505
  );
429
506
 
430
507
  // Auto-invalidate related caches after mutations
@@ -23,6 +23,7 @@ import SumEqualsSummary, {
23
23
  sumEqualsHintFor,
24
24
  } from './SumEqualsSummary';
25
25
  import Table from './DataGrid';
26
+ import { SECONDARY_GRID_HEIGHT } from './utils/useDensity';
26
27
 
27
28
  import styles from './styles/Form.module.scss';
28
29
 
@@ -3195,6 +3196,14 @@ function Form({
3195
3196
  mapbox={mapbox}
3196
3197
  paramValue={paramValue}
3197
3198
  settings={item.settings}
3199
+ /* A form is almost always inside
3200
+ a modal, so this grid must not
3201
+ take the room between itself and
3202
+ the bottom of the window — that
3203
+ is measured from a box that is
3204
+ already centred, and pushes the
3205
+ pager below the fold. */
3206
+ gridHeight={SECONDARY_GRID_HEIGHT}
3198
3207
  style={
3199
3208
  userProfile?.settings?.style ||
3200
3209
  {}
@@ -21,11 +21,16 @@ import {
21
21
  Shield,
22
22
  Image,
23
23
  Phone,
24
+ KeyRound,
25
+ Lock,
26
+ MessageSquare,
27
+ MessagesSquare,
24
28
  } from 'lucide-react';
25
29
  import CustomFetch from './Fetch';
26
30
  import Notification from './Notification';
27
31
  import SwitchAccount from './SwitchAccount';
28
32
  import BusinessCardOcr from './BusinessCardOcr';
33
+ import { isNavActive } from './navActive';
29
34
  import styles from './styles/Navigation.module.scss';
30
35
 
31
36
  // Opt-in icons for the sidebar (simple/cms) layouts: a nav entry names one
@@ -56,6 +61,10 @@ const NAV_ICONS = {
56
61
  const SETTING_ICONS = {
57
62
  bug: Bug,
58
63
  calendar: Calendar,
64
+ key: KeyRound,
65
+ lock: Lock,
66
+ message: MessageSquare,
67
+ messages: MessagesSquare,
59
68
  draft: FileText,
60
69
  image: Image,
61
70
  peopleGroup: Users,
@@ -76,6 +85,31 @@ const RAIL_ACTION_LABELS = {
76
85
  notifications: 'Notifications',
77
86
  };
78
87
 
88
+ /**
89
+ * A chip is an icon and nothing else, so its name has to come from somewhere:
90
+ * it is the tooltip, and it is the only thing a screen reader has to go on.
91
+ * A config entry that forgot its `label` used to produce a nameless button —
92
+ * so the id, which is always there, is humanised as the last resort
93
+ * (`callRecordings` -> `Call Recordings`).
94
+ */
95
+ const actionLabel = (n) => {
96
+ if (n.label) {
97
+ return n.label;
98
+ }
99
+ if (RAIL_ACTION_LABELS[n.id]) {
100
+ return RAIL_ACTION_LABELS[n.id];
101
+ }
102
+ if (!n.id) {
103
+ return '';
104
+ }
105
+ return n.id
106
+ .replace(/[-_]+/g, ' ')
107
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
108
+ .replace(/\s+/g, ' ')
109
+ .trim()
110
+ .replace(/^./, (c) => c.toUpperCase());
111
+ };
112
+
79
113
  const SearchComponent = ({
80
114
  navigate,
81
115
  search,
@@ -242,6 +276,22 @@ function Navigation({
242
276
  themeType,
243
277
  userProfile,
244
278
  debugIcons = false, // Add debug flag
279
+ /*
280
+ * Per-id renderers for account actions: `{ [setting.id]: ({ setting,
281
+ * userProfile, setSystemAuth }) => node }`.
282
+ *
283
+ * The chain below is hard-coded because every action it knows about ships
284
+ * with this library. An action that lives in a consuming app — or in a
285
+ * module the library carries but the header should not import eagerly —
286
+ * has nowhere to go. This is that slot: a matching id renders the
287
+ * supplied node inside the same row wrapper the built-in branches use,
288
+ * on both the header chips and the sidebar rail.
289
+ *
290
+ * Checked before the built-in chain, so a project can also override a
291
+ * shipped action. Empty by default, so nothing changes for anyone who
292
+ * does not pass it.
293
+ */
294
+ renderers = {},
245
295
  }) {
246
296
  const [search, setSearch] = useState('');
247
297
  const [searchResultShow, setSearchResultShow] = useState(false);
@@ -304,6 +354,19 @@ function Navigation({
304
354
  // Add active class if the setting is active
305
355
  const activeClass = isActive ? styles.active : '';
306
356
 
357
+ // A consumer-supplied renderer wins over the built-in chain.
358
+ if (typeof renderers[n.id] === 'function') {
359
+ return (
360
+ <li>
361
+ {renderers[n.id]({
362
+ setting: n,
363
+ userProfile,
364
+ setSystemAuth,
365
+ })}
366
+ </li>
367
+ );
368
+ }
369
+
307
370
  if (n.id === 'notifications') {
308
371
  return (
309
372
  <li>
@@ -314,8 +377,11 @@ function Navigation({
314
377
  return (
315
378
  <li>
316
379
  <button
380
+ type="button"
317
381
  data-tooltip-id="system-tooltip"
382
+ data-tooltip-place="bottom"
318
383
  data-tooltip-content="Scan Business Card"
384
+ aria-label="Scan Business Card"
319
385
  onClick={() => setShowBusinessCardOcr(true)}
320
386
  className={activeClass}
321
387
  >
@@ -335,11 +401,15 @@ function Navigation({
335
401
  );
336
402
  } else if (n.id === 'logout') {
337
403
  const IconComponent = iconComponents[n.icon];
404
+ const label = actionLabel(n);
338
405
  return (
339
406
  <li>
340
407
  <button
408
+ type="button"
341
409
  data-tooltip-id="system-tooltip"
342
- data-tooltip-content={n.label}
410
+ data-tooltip-place="bottom"
411
+ data-tooltip-content={label}
412
+ aria-label={label}
343
413
  onClick={handleLogout}
344
414
  className={activeClass} // Add active class to the logout button
345
415
  >
@@ -349,13 +419,16 @@ function Navigation({
349
419
  );
350
420
  } else {
351
421
  const IconComponent = iconComponents[n.icon];
422
+ const label = actionLabel(n);
352
423
 
353
424
  return (
354
425
  <li>
355
426
  <Link
356
427
  to={n.url}
357
428
  data-tooltip-id="system-tooltip"
358
- data-tooltip-content={n.label}
429
+ data-tooltip-place="bottom"
430
+ data-tooltip-content={label}
431
+ aria-label={label}
359
432
  className={`${activeClass}`}
360
433
  target={n.target ? n.target : '_self'}
361
434
  >
@@ -385,11 +458,25 @@ function Navigation({
385
458
  const renderRailSetting = (n) => {
386
459
  const isActive = n.url && n.url === `/${currentPage}`;
387
460
  const activeClass = isActive ? styles.active : '';
388
- const label = n.label || RAIL_ACTION_LABELS[n.id] || '';
461
+ const label = actionLabel(n);
389
462
  // A CSS string, quoted and escaped, for `content`.
390
463
  const labelVar = { '--nav-action-label': JSON.stringify(label) };
391
464
  const IconComponent = SETTING_ICONS[n.icon];
392
465
 
466
+ // Same slot as the header's, publishing the row label the same way a
467
+ // mounted component gets one (see the note above renderRailSetting).
468
+ if (typeof renderers[n.id] === 'function') {
469
+ return (
470
+ <li style={labelVar}>
471
+ {renderers[n.id]({
472
+ setting: n,
473
+ userProfile,
474
+ setSystemAuth,
475
+ })}
476
+ </li>
477
+ );
478
+ }
479
+
393
480
  if (n.id === 'notifications') {
394
481
  return (
395
482
  <li style={labelVar}>
@@ -510,9 +597,8 @@ function Navigation({
510
597
  <li key={`nav-child-${childKey}`}>
511
598
  <Link
512
599
  to={child.url}
513
- title={child.label}
514
600
  className={
515
- location.pathname === child.url
601
+ isNavActive(child.url, [], currentPage)
516
602
  ? styles.active
517
603
  : ''
518
604
  }
@@ -543,7 +629,7 @@ function Navigation({
543
629
  {n.label}
544
630
  </Link>
545
631
  ) : (
546
- <span title={n.label}>{n.label}</span>
632
+ <span>{n.label}</span>
547
633
  )}
548
634
  {renderChildren()}
549
635
  </li>
@@ -685,52 +771,25 @@ function Navigation({
685
771
 
686
772
  useEffect(() => {
687
773
  setNavData((prevNavData) => {
774
+ /**
775
+ * One rule at every depth — see navActive.js, which holds it and
776
+ * is tested directly. This used to branch on how many segments the
777
+ * URL happened to have, only consulted an item's children in one
778
+ * of those branches, and compared paths with `String.includes`.
779
+ * The visible cost: standing on `/reports`, which Admin owns as a
780
+ * child, nothing in the bar was marked at all.
781
+ */
688
782
  const updateNavItems = (nav) => {
689
- const cleanUrl = (url) => {
690
- if (url) {
691
- // Ensure the URL starts with a slash. If it doesn't, prepend it.
692
- return url.startsWith('/') ? url : `/${url}`;
693
- }
694
- return '';
695
- };
696
-
697
- // Ensure both URLs have the leading slash for consistent comparison
698
- const currentUrl = cleanUrl(currentPage);
699
- const navUrl = nav.url ? cleanUrl(nav.url) : 'dashboard';
700
-
701
- // Split current URL into segments
702
- const currentUrlSegments = currentUrl
703
- .split('/')
704
- .filter(Boolean);
705
-
706
- // Handle the case where navUrl is just '/'
707
- if (
708
- navUrl === '/' &&
709
- (currentUrl === '/dashboard' || currentUrl === '/')
710
- ) {
711
- return { ...nav, class: 'nav-item active' }; // Mark as active for dashboard if navUrl is '/'
712
- }
713
-
714
- // If current URL has 2 or fewer segments, check if navUrl is contained in currentUrl
715
- if (currentUrlSegments.length <= 2) {
716
- if (currentUrl.includes(navUrl) && nav.id !== 'dashboard') {
717
- return { ...nav, class: 'nav-item active' }; // Mark as active if contained
718
- }
719
- } else {
720
- // If more than 2 segments, use exact match logic
721
- if (
722
- navUrl === currentUrl ||
723
- (nav.children &&
724
- nav.children.some(
725
- (child) =>
726
- cleanUrl(child.url) === `/${currentPage}`
727
- ))
728
- ) {
729
- return { ...nav, class: 'nav-item active' }; // Mark as active
730
- }
731
- }
783
+ const childUrls = (nav.children || []).map(
784
+ (child) => child.url
785
+ );
732
786
 
733
- return { ...nav, class: 'nav-item' }; // Default class
787
+ return {
788
+ ...nav,
789
+ class: isNavActive(nav.url, childUrls, currentPage)
790
+ ? 'nav-item active'
791
+ : 'nav-item',
792
+ };
734
793
  };
735
794
 
736
795
  return {