@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 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
- return typeof options === "object" && "oneOf" in options;
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 isMultipleCriteriaRequireAll = (options) => {
386
- return typeof options === "object" && "requireAll" in options;
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
- * This is a hook to use the hasAccessTo in host.
390
- * You can use this to ensure the page you link to is available for the user to see.
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 (@link NavigationRuntimeApi)
397
- * @see (@link HasAccessToOptions)
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
- if (!context) {
406
- throw new Error("useHasAccessTo must be used within an NavigationContext");
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
- async function checkAccess() {
435
+ if (!context)
436
+ return;
437
+ let cancelled = false;
438
+ dispatch({ type: "FETCH_START" });
439
+ const checkAccess = async () => {
413
440
  try {
414
- if (isMultipleCriteriaOneOf(options) && isMultipleCriteriaRequireAll(options)) {
415
- throw new Error("Multiple criteria are not allowed, use one of or require all - not both");
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
- setError(new Error("Failed to check access", { cause: e }));
432
- }
433
- finally {
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
- }, [context, options]);
439
- return react.useMemo(() => ({ hasAccess, loading, error }), [hasAccess, loading, error]);
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
- return typeof options === "object" && "oneOf" in options;
382
- };
383
- const isMultipleCriteriaRequireAll = (options) => {
384
- return typeof options === "object" && "requireAll" in options;
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
- * This is a hook to use the hasAccessTo in host.
388
- * You can use this to ensure the page you link to is available for the user to see.
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 (@link NavigationRuntimeApi)
395
- * @see (@link HasAccessToOptions)
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
- if (!context) {
404
- throw new Error("useHasAccessTo must be used within an NavigationContext");
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
- async function checkAccess() {
433
+ if (!context)
434
+ return;
435
+ let cancelled = false;
436
+ dispatch({ type: "FETCH_START" });
437
+ const checkAccess = async () => {
411
438
  try {
412
- if (isMultipleCriteriaOneOf(options) && isMultipleCriteriaRequireAll(options)) {
413
- throw new Error("Multiple criteria are not allowed, use one of or require all - not both");
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
- setError(new Error("Failed to check access", { cause: e }));
430
- }
431
- finally {
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
- }, [context, options]);
437
- return useMemo(() => ({ hasAccess, loading, error }), [hasAccess, loading, error]);
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.51",
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
- "@trackunit/iris-app-runtime-core": "1.13.48",
12
- "@trackunit/iris-app-runtime-core-api": "1.12.46",
13
- "@trackunit/react-core-contexts-api": "1.13.46"
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 interface MultipleCriteriaOneOf {
3
- oneOf: Array<HasAccessToOptions>;
4
- }
5
- export interface MultipleCriteriaRequireAll {
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
- * This is a hook to use the hasAccessTo in host.
10
- * You can use this to ensure the page you link to is available for the user to see.
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 (@link NavigationRuntimeApi)
17
- * @see (@link HasAccessToOptions)
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: HasAccessToOptions | MultipleCriteriaOneOf | MultipleCriteriaRequireAll) => {
24
- hasAccess: boolean | undefined;
25
- loading: boolean;
26
- error: Error | undefined;
27
- };
25
+ export declare const useHasAccessTo: (options: AccessOptions) => UseHasAccessToResult;
26
+ export {};