@eintrek/erp-shell 0.1.30 → 0.1.32

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.
@@ -33,7 +33,9 @@ export interface PermissionProviderProps {
33
33
  * over the optional `fallbackRulesMap`), passes the resolved required roles
34
34
  * into the host's `usePermission` probe, and renders one of:
35
35
  *
36
- * 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
36
+ * 1. rules / probe still loading → `null` on FIRST load only (let Suspense /
37
+ * loading.tsx show); once this subtree has been granted access, a later
38
+ * loading state keeps the children mounted — see the note below
37
39
  * 2. no rule covers this path → deny (fail closed)
38
40
  * 3. rule is explicit public (`[]`) → children
39
41
  * 4. probe says no access → deny card
package/dist/index.esm.js CHANGED
@@ -882,7 +882,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
882
882
  setShowAppsMenu(!showAppsMenu);
883
883
  setShowThemeMenu(false);
884
884
  setShowLanguageMenu(false);
885
- }, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsx(LayoutGrid, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E41\u0E2D\u0E1B\u0E1E\u0E25\u0E34\u0E40\u0E04\u0E0A\u0E31\u0E19" }), jsx(ChevronUp, { className: `h-4 w-4 transition-transform ${showAppsMenu ? "rotate-180" : ""}` })] }), showAppsMenu && (jsx("div", { className: "mt-1 ml-4 space-y-1", children: visibleSystemLinks.map(link => (jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), className: "flex w-full items-center gap-3 px-3 py-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [link.icon ?? jsx(ExternalLink, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: link.label })] }, `${link.label}:${link.href}`))) }))] })), jsx("button", { onClick: handleSignOut, disabled: isSigningOut, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed mt-1", children: isSigningOut ? (jsxs(Fragment$1, { children: [jsx(Loader2, { className: "h-4 w-4 animate-spin" }), jsx("span", { className: "flex-1 text-left", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A..." })] })) : (jsxs(Fragment$1, { children: [jsx(LogOut, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A" })] })) })] })] })) : null;
885
+ }, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsx(LayoutGrid, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E41\u0E2D\u0E1B\u0E1E\u0E25\u0E34\u0E40\u0E04\u0E0A\u0E31\u0E19" }), jsx(ChevronUp, { className: `h-4 w-4 transition-transform ${showAppsMenu ? "rotate-180" : ""}` })] }), showAppsMenu && (jsx("div", { className: "mt-1 ml-4 space-y-1", children: visibleSystemLinks.map(link => (jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), className: "flex w-full items-center gap-3 px-3 py-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [link.icon ?? jsx(ExternalLink, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: link.label }), link.active && (jsx("div", { className: "h-2 w-2 rounded-full bg-primary" }))] }, `${link.label}:${link.href}`))) }))] })), jsx("button", { onClick: handleSignOut, disabled: isSigningOut, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed mt-1", children: isSigningOut ? (jsxs(Fragment$1, { children: [jsx(Loader2, { className: "h-4 w-4 animate-spin" }), jsx("span", { className: "flex-1 text-left", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A..." })] })) : (jsxs(Fragment$1, { children: [jsx(LogOut, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A" })] })) })] })] })) : null;
886
886
  return (jsx("div", { className: "p-2", children: jsxs("div", { className: "relative", children: [jsxs("button", { ref: buttonRef, onClick: () => setIsOpen(!isOpen), className: "flex w-full items-center gap-2 p-2 rounded-md transition-colors", children: [jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [user.avatar ? (jsx(AvatarImage, { src: user.avatar, alt: user.name })) : null, jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground w-8 h-8", children: getInitials(user.name) })] }), jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [jsx("span", { className: "truncate font-medium", title: user.name, children: user.name }), jsx("span", { className: "truncate text-xs text-sidebar-foreground/70", title: user.email, children: user.email })] }), jsx(ChevronUp, { className: `ml-auto size-4 transition-transform ${isOpen ? "rotate-180" : ""}` })] }), typeof window !== "undefined" &&
887
887
  createPortal(dropdownContent, document.body)] }) }));
888
888
  }
@@ -1349,7 +1349,9 @@ function DeniedCard() {
1349
1349
  * over the optional `fallbackRulesMap`), passes the resolved required roles
1350
1350
  * into the host's `usePermission` probe, and renders one of:
1351
1351
  *
1352
- * 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
1352
+ * 1. rules / probe still loading → `null` on FIRST load only (let Suspense /
1353
+ * loading.tsx show); once this subtree has been granted access, a later
1354
+ * loading state keeps the children mounted — see the note below
1353
1355
  * 2. no rule covers this path → deny (fail closed)
1354
1356
  * 3. rule is explicit public (`[]`) → children
1355
1357
  * 4. probe says no access → deny card
@@ -1369,7 +1371,25 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1369
1371
  const { hasPermission, isLoading } = usePermission({
1370
1372
  requiredRoles: lookup.roles,
1371
1373
  });
1372
- if (rulesLoading) {
1374
+ // Returning null unmounts the page. That is fine before anything has been
1375
+ // shown, and destructive afterwards: next-auth flips its status back to
1376
+ // "loading" on every session update() — one runs every 4 minutes — and a
1377
+ // rules hook that reports loading on that transient took the page down with
1378
+ // it. Users filling a long form watched everything they had typed vanish,
1379
+ // which reads as a spontaneous page refresh.
1380
+ //
1381
+ // So a loading state only blanks the subtree until access has been granted
1382
+ // once. After that the children stay mounted while the check re-settles; the
1383
+ // API still enforces access on every request, and a genuine loss of
1384
+ // permission surfaces as a denied response rather than a wiped form.
1385
+ //
1386
+ // The grant is remembered per pathname, not per mount. This provider lives in
1387
+ // the layout, so it survives every in-app navigation — a bare boolean would
1388
+ // carry one page's grant onto the next route and flash a page the user may
1389
+ // not be allowed to see while its own check is still running.
1390
+ const grantedForPathRef = useRef(null);
1391
+ const grantedHere = grantedForPathRef.current === pathname;
1392
+ if (rulesLoading && !grantedHere) {
1373
1393
  return null;
1374
1394
  }
1375
1395
  // Unknown route → deny. Only explicit `required_child_groups: []` is public.
@@ -1377,15 +1397,16 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1377
1397
  return jsx(DeniedCard, {});
1378
1398
  }
1379
1399
  if (lookup.roles.length === 0) {
1400
+ grantedForPathRef.current = pathname;
1380
1401
  return jsx(Fragment$1, { children: children });
1381
1402
  }
1382
- const isLoadingCombined = isLoading;
1383
- if (isLoadingCombined) {
1403
+ if (isLoading && !grantedHere) {
1384
1404
  return null;
1385
1405
  }
1386
- if (!hasPermission) {
1406
+ if (!hasPermission && !isLoading) {
1387
1407
  return jsx(DeniedCard, {});
1388
1408
  }
1409
+ grantedForPathRef.current = pathname;
1389
1410
  return jsx(Fragment$1, { children: children });
1390
1411
  }
1391
1412