@trackunit/react-core-hooks 1.12.51 → 1.12.52
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/index.cjs.js +64 -39
- package/index.esm.js +66 -41
- package/package.json +5 -4
- package/src/navigation/useHasAccessTo.d.ts +19 -20
package/index.cjs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var reactCoreContextsApi = require('@trackunit/react-core-contexts-api');
|
|
4
4
|
var react = require('react');
|
|
5
|
+
var esToolkit = require('es-toolkit');
|
|
5
6
|
var irisAppRuntimeCore = require('@trackunit/iris-app-runtime-core');
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -379,64 +380,88 @@ const useModalDialogContext = () => {
|
|
|
379
380
|
return modalDialogContext;
|
|
380
381
|
};
|
|
381
382
|
|
|
382
|
-
const isMultipleCriteriaOneOf = (options) =>
|
|
383
|
-
|
|
383
|
+
const isMultipleCriteriaOneOf = (options) => typeof options === "object" && "oneOf" in options;
|
|
384
|
+
const isMultipleCriteriaRequireAll = (options) => typeof options === "object" && "requireAll" in options;
|
|
385
|
+
const resolveAccess = async (context, options) => {
|
|
386
|
+
if (isMultipleCriteriaOneOf(options) && isMultipleCriteriaRequireAll(options)) {
|
|
387
|
+
throw new Error("Multiple criteria are not allowed, use one of or require all - not both");
|
|
388
|
+
}
|
|
389
|
+
if (isMultipleCriteriaOneOf(options)) {
|
|
390
|
+
const results = await Promise.all(options.oneOf.map(option => context.hasAccessTo(option)));
|
|
391
|
+
return results.some(Boolean);
|
|
392
|
+
}
|
|
393
|
+
if (isMultipleCriteriaRequireAll(options)) {
|
|
394
|
+
const results = await Promise.all(options.requireAll.map(option => context.hasAccessTo(option)));
|
|
395
|
+
return results.every(Boolean);
|
|
396
|
+
}
|
|
397
|
+
return context.hasAccessTo(options);
|
|
384
398
|
};
|
|
385
|
-
const
|
|
386
|
-
|
|
399
|
+
const INITIAL_STATE = {
|
|
400
|
+
status: "loading",
|
|
401
|
+
hasAccess: undefined,
|
|
402
|
+
error: undefined,
|
|
403
|
+
};
|
|
404
|
+
const accessReducer = (_state, action) => {
|
|
405
|
+
switch (action.type) {
|
|
406
|
+
case "FETCH_START":
|
|
407
|
+
return INITIAL_STATE;
|
|
408
|
+
case "FETCH_SUCCESS":
|
|
409
|
+
return { status: "success", hasAccess: action.hasAccess, error: undefined };
|
|
410
|
+
case "FETCH_ERROR":
|
|
411
|
+
return { status: "error", hasAccess: undefined, error: action.error };
|
|
412
|
+
default:
|
|
413
|
+
throw new Error(`${action} is not a known action`);
|
|
414
|
+
}
|
|
387
415
|
};
|
|
388
416
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
417
|
+
* Hook to check if the current user has access to a navigation target.
|
|
418
|
+
* Useful for conditionally rendering links based on user permissions.
|
|
391
419
|
*
|
|
392
420
|
* @requires NavigationContext
|
|
393
421
|
* @example
|
|
394
422
|
* import { useHasAccessTo } from "@trackunit/react-core-hooks";
|
|
395
423
|
* useHasAccessTo({ assetId: assetInfo?.assetId, page: "movement" })
|
|
396
|
-
* @see
|
|
397
|
-
* @see
|
|
398
|
-
* @see (@link HasAccessToOptions.assetId)
|
|
399
|
-
* @see (@link HasAccessToOptions.page)
|
|
400
|
-
* @see (@link HasAccessToOptions.siteId)
|
|
401
|
-
* @see (@link NavigationRuntimeApi)
|
|
424
|
+
* @see {@link NavigationRuntimeApi}
|
|
425
|
+
* @see {@link HasAccessToOptions}
|
|
402
426
|
*/
|
|
403
427
|
const useHasAccessTo = (options) => {
|
|
404
428
|
const context = react.useContext(reactCoreContextsApi.NavigationContext);
|
|
405
|
-
|
|
406
|
-
|
|
429
|
+
const [stableOptions, setStableOptions] = react.useState(options);
|
|
430
|
+
const [state, dispatch] = react.useReducer(accessReducer, INITIAL_STATE);
|
|
431
|
+
if (!esToolkit.isEqual(stableOptions, options)) {
|
|
432
|
+
setStableOptions(options);
|
|
407
433
|
}
|
|
408
|
-
const [hasAccess, setHasAccess] = react.useState();
|
|
409
|
-
const [loading, setLoading] = react.useState(true);
|
|
410
|
-
const [error, setError] = react.useState();
|
|
411
434
|
react.useEffect(() => {
|
|
412
|
-
|
|
435
|
+
if (!context)
|
|
436
|
+
return;
|
|
437
|
+
let cancelled = false;
|
|
438
|
+
dispatch({ type: "FETCH_START" });
|
|
439
|
+
const checkAccess = async () => {
|
|
413
440
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (isMultipleCriteriaOneOf(options)) {
|
|
418
|
-
const doHaveAccess = await Promise.all(options.oneOf.map(option => context?.hasAccessTo(option)));
|
|
419
|
-
setHasAccess(doHaveAccess.some(access => access));
|
|
420
|
-
}
|
|
421
|
-
else if (isMultipleCriteriaRequireAll(options)) {
|
|
422
|
-
const doHaveAccess = await Promise.all(options.requireAll.map(option => context?.hasAccessTo(option)));
|
|
423
|
-
setHasAccess(doHaveAccess.every(access => access));
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
const doHaveAccess = await context?.hasAccessTo(options);
|
|
427
|
-
setHasAccess(doHaveAccess);
|
|
441
|
+
const accessResult = await resolveAccess(context, stableOptions);
|
|
442
|
+
if (!cancelled) {
|
|
443
|
+
dispatch({ type: "FETCH_SUCCESS", hasAccess: accessResult });
|
|
428
444
|
}
|
|
429
445
|
}
|
|
430
446
|
catch (e) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
setLoading(false);
|
|
447
|
+
if (!cancelled) {
|
|
448
|
+
dispatch({ type: "FETCH_ERROR", error: new Error("Failed to check access", { cause: e }) });
|
|
449
|
+
}
|
|
435
450
|
}
|
|
436
|
-
}
|
|
451
|
+
};
|
|
437
452
|
void checkAccess();
|
|
438
|
-
|
|
439
|
-
|
|
453
|
+
return () => {
|
|
454
|
+
cancelled = true;
|
|
455
|
+
};
|
|
456
|
+
}, [context, stableOptions]);
|
|
457
|
+
if (!context) {
|
|
458
|
+
throw new Error("useHasAccessTo must be used within a NavigationContext");
|
|
459
|
+
}
|
|
460
|
+
return react.useMemo(() => ({
|
|
461
|
+
hasAccess: state.hasAccess,
|
|
462
|
+
loading: state.status === "loading",
|
|
463
|
+
error: state.error,
|
|
464
|
+
}), [state]);
|
|
440
465
|
};
|
|
441
466
|
|
|
442
467
|
/**
|
package/index.esm.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AnalyticsContext, AssetSortingContext, ConfirmationDialogContext, EnvironmentContext, ErrorHandlingContext, ExportDataContext, FeatureFlagContext, FilterBarContext, TokenContext, ModalDialogContext, NavigationContext, OemBrandingContext, UserSubscriptionContext, TimeRangeContext, ToastContext, CurrentUserContext, CurrentUserPreferenceContext, WidgetConfigContext } from '@trackunit/react-core-contexts-api';
|
|
2
|
-
import { useContext, useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useContext, useMemo, useState, useCallback, useReducer, useEffect, useRef } from 'react';
|
|
3
|
+
import { isEqual } from 'es-toolkit';
|
|
3
4
|
import { AssetRuntime, CustomerRuntime, EventRuntime, ParamsRuntime, SiteRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -377,64 +378,88 @@ const useModalDialogContext = () => {
|
|
|
377
378
|
return modalDialogContext;
|
|
378
379
|
};
|
|
379
380
|
|
|
380
|
-
const isMultipleCriteriaOneOf = (options) =>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
const isMultipleCriteriaOneOf = (options) => typeof options === "object" && "oneOf" in options;
|
|
382
|
+
const isMultipleCriteriaRequireAll = (options) => typeof options === "object" && "requireAll" in options;
|
|
383
|
+
const resolveAccess = async (context, options) => {
|
|
384
|
+
if (isMultipleCriteriaOneOf(options) && isMultipleCriteriaRequireAll(options)) {
|
|
385
|
+
throw new Error("Multiple criteria are not allowed, use one of or require all - not both");
|
|
386
|
+
}
|
|
387
|
+
if (isMultipleCriteriaOneOf(options)) {
|
|
388
|
+
const results = await Promise.all(options.oneOf.map(option => context.hasAccessTo(option)));
|
|
389
|
+
return results.some(Boolean);
|
|
390
|
+
}
|
|
391
|
+
if (isMultipleCriteriaRequireAll(options)) {
|
|
392
|
+
const results = await Promise.all(options.requireAll.map(option => context.hasAccessTo(option)));
|
|
393
|
+
return results.every(Boolean);
|
|
394
|
+
}
|
|
395
|
+
return context.hasAccessTo(options);
|
|
396
|
+
};
|
|
397
|
+
const INITIAL_STATE = {
|
|
398
|
+
status: "loading",
|
|
399
|
+
hasAccess: undefined,
|
|
400
|
+
error: undefined,
|
|
401
|
+
};
|
|
402
|
+
const accessReducer = (_state, action) => {
|
|
403
|
+
switch (action.type) {
|
|
404
|
+
case "FETCH_START":
|
|
405
|
+
return INITIAL_STATE;
|
|
406
|
+
case "FETCH_SUCCESS":
|
|
407
|
+
return { status: "success", hasAccess: action.hasAccess, error: undefined };
|
|
408
|
+
case "FETCH_ERROR":
|
|
409
|
+
return { status: "error", hasAccess: undefined, error: action.error };
|
|
410
|
+
default:
|
|
411
|
+
throw new Error(`${action} is not a known action`);
|
|
412
|
+
}
|
|
385
413
|
};
|
|
386
414
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
415
|
+
* Hook to check if the current user has access to a navigation target.
|
|
416
|
+
* Useful for conditionally rendering links based on user permissions.
|
|
389
417
|
*
|
|
390
418
|
* @requires NavigationContext
|
|
391
419
|
* @example
|
|
392
420
|
* import { useHasAccessTo } from "@trackunit/react-core-hooks";
|
|
393
421
|
* useHasAccessTo({ assetId: assetInfo?.assetId, page: "movement" })
|
|
394
|
-
* @see
|
|
395
|
-
* @see
|
|
396
|
-
* @see (@link HasAccessToOptions.assetId)
|
|
397
|
-
* @see (@link HasAccessToOptions.page)
|
|
398
|
-
* @see (@link HasAccessToOptions.siteId)
|
|
399
|
-
* @see (@link NavigationRuntimeApi)
|
|
422
|
+
* @see {@link NavigationRuntimeApi}
|
|
423
|
+
* @see {@link HasAccessToOptions}
|
|
400
424
|
*/
|
|
401
425
|
const useHasAccessTo = (options) => {
|
|
402
426
|
const context = useContext(NavigationContext);
|
|
403
|
-
|
|
404
|
-
|
|
427
|
+
const [stableOptions, setStableOptions] = useState(options);
|
|
428
|
+
const [state, dispatch] = useReducer(accessReducer, INITIAL_STATE);
|
|
429
|
+
if (!isEqual(stableOptions, options)) {
|
|
430
|
+
setStableOptions(options);
|
|
405
431
|
}
|
|
406
|
-
const [hasAccess, setHasAccess] = useState();
|
|
407
|
-
const [loading, setLoading] = useState(true);
|
|
408
|
-
const [error, setError] = useState();
|
|
409
432
|
useEffect(() => {
|
|
410
|
-
|
|
433
|
+
if (!context)
|
|
434
|
+
return;
|
|
435
|
+
let cancelled = false;
|
|
436
|
+
dispatch({ type: "FETCH_START" });
|
|
437
|
+
const checkAccess = async () => {
|
|
411
438
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (isMultipleCriteriaOneOf(options)) {
|
|
416
|
-
const doHaveAccess = await Promise.all(options.oneOf.map(option => context?.hasAccessTo(option)));
|
|
417
|
-
setHasAccess(doHaveAccess.some(access => access));
|
|
418
|
-
}
|
|
419
|
-
else if (isMultipleCriteriaRequireAll(options)) {
|
|
420
|
-
const doHaveAccess = await Promise.all(options.requireAll.map(option => context?.hasAccessTo(option)));
|
|
421
|
-
setHasAccess(doHaveAccess.every(access => access));
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
const doHaveAccess = await context?.hasAccessTo(options);
|
|
425
|
-
setHasAccess(doHaveAccess);
|
|
439
|
+
const accessResult = await resolveAccess(context, stableOptions);
|
|
440
|
+
if (!cancelled) {
|
|
441
|
+
dispatch({ type: "FETCH_SUCCESS", hasAccess: accessResult });
|
|
426
442
|
}
|
|
427
443
|
}
|
|
428
444
|
catch (e) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
setLoading(false);
|
|
445
|
+
if (!cancelled) {
|
|
446
|
+
dispatch({ type: "FETCH_ERROR", error: new Error("Failed to check access", { cause: e }) });
|
|
447
|
+
}
|
|
433
448
|
}
|
|
434
|
-
}
|
|
449
|
+
};
|
|
435
450
|
void checkAccess();
|
|
436
|
-
|
|
437
|
-
|
|
451
|
+
return () => {
|
|
452
|
+
cancelled = true;
|
|
453
|
+
};
|
|
454
|
+
}, [context, stableOptions]);
|
|
455
|
+
if (!context) {
|
|
456
|
+
throw new Error("useHasAccessTo must be used within a NavigationContext");
|
|
457
|
+
}
|
|
458
|
+
return useMemo(() => ({
|
|
459
|
+
hasAccess: state.hasAccess,
|
|
460
|
+
loading: state.status === "loading",
|
|
461
|
+
error: state.error,
|
|
462
|
+
}), [state]);
|
|
438
463
|
};
|
|
439
464
|
|
|
440
465
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-core-hooks",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.52",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"react": "19.0.0",
|
|
11
|
-
"
|
|
12
|
-
"@trackunit/iris-app-runtime-core
|
|
13
|
-
"@trackunit/
|
|
11
|
+
"es-toolkit": "^1.39.10",
|
|
12
|
+
"@trackunit/iris-app-runtime-core": "1.13.49",
|
|
13
|
+
"@trackunit/iris-app-runtime-core-api": "1.12.47",
|
|
14
|
+
"@trackunit/react-core-contexts-api": "1.13.47"
|
|
14
15
|
},
|
|
15
16
|
"module": "./index.esm.js",
|
|
16
17
|
"main": "./index.cjs.js",
|
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import { HasAccessToOptions } from "@trackunit/iris-app-runtime-core-api";
|
|
2
|
-
export
|
|
3
|
-
oneOf: Array<HasAccessToOptions>;
|
|
4
|
-
}
|
|
5
|
-
export
|
|
6
|
-
requireAll: Array<HasAccessToOptions>;
|
|
7
|
-
}
|
|
1
|
+
import type { HasAccessToOptions } from "@trackunit/iris-app-runtime-core-api";
|
|
2
|
+
export type MultipleCriteriaOneOf = {
|
|
3
|
+
readonly oneOf: Array<HasAccessToOptions>;
|
|
4
|
+
};
|
|
5
|
+
export type MultipleCriteriaRequireAll = {
|
|
6
|
+
readonly requireAll: Array<HasAccessToOptions>;
|
|
7
|
+
};
|
|
8
|
+
type AccessOptions = HasAccessToOptions | MultipleCriteriaOneOf | MultipleCriteriaRequireAll;
|
|
9
|
+
type UseHasAccessToResult = {
|
|
10
|
+
readonly hasAccess: boolean | undefined;
|
|
11
|
+
readonly loading: boolean;
|
|
12
|
+
readonly error: Error | undefined;
|
|
13
|
+
};
|
|
8
14
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
15
|
+
* Hook to check if the current user has access to a navigation target.
|
|
16
|
+
* Useful for conditionally rendering links based on user permissions.
|
|
11
17
|
*
|
|
12
18
|
* @requires NavigationContext
|
|
13
19
|
* @example
|
|
14
20
|
* import { useHasAccessTo } from "@trackunit/react-core-hooks";
|
|
15
21
|
* useHasAccessTo({ assetId: assetInfo?.assetId, page: "movement" })
|
|
16
|
-
* @see
|
|
17
|
-
* @see
|
|
18
|
-
* @see (@link HasAccessToOptions.assetId)
|
|
19
|
-
* @see (@link HasAccessToOptions.page)
|
|
20
|
-
* @see (@link HasAccessToOptions.siteId)
|
|
21
|
-
* @see (@link NavigationRuntimeApi)
|
|
22
|
+
* @see {@link NavigationRuntimeApi}
|
|
23
|
+
* @see {@link HasAccessToOptions}
|
|
22
24
|
*/
|
|
23
|
-
export declare const useHasAccessTo: (options:
|
|
24
|
-
|
|
25
|
-
loading: boolean;
|
|
26
|
-
error: Error | undefined;
|
|
27
|
-
};
|
|
25
|
+
export declare const useHasAccessTo: (options: AccessOptions) => UseHasAccessToResult;
|
|
26
|
+
export {};
|