@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.
- package/README.md +1658 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +619 -158
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/AutoGrowCell.jsx +141 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +29 -0
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
package/src/components/Fetch.jsx
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
package/src/components/Form.jsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
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 {
|
|
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 {
|