@eintrek/erp-shell 0.1.10 → 0.1.11
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/dist/auth/permission-gate-shell.d.ts +22 -0
- package/dist/auth/permission-provider.d.ts +44 -0
- package/dist/auth/permission-utils.d.ts +22 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +259 -59
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +260 -55
- package/dist/index.js.map +1 -1
- package/dist/navigation/app-layout-shell.d.ts +59 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -966,6 +966,261 @@ function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, clo
|
|
|
966
966
|
return (jsxRuntime.jsxs("div", { className: erpTheme.cn("bg-sidebar shrink-0 flex flex-col overflow-hidden shadow-2xl h-screen w-[var(--sidebar-width-mobile)]", "!w-76 xl:rounded-b-4xl xl:rounded-tl-4xl xl:m-5 xl:h-[calc(100vh-2.5rem)]", className), children: [jsxRuntime.jsx(SidebarHeader, { children: header }), jsxRuntime.jsxs(SidebarContent, { className: "pl-2 pr-2 flex-1 overflow-y-auto", children: [jsxRuntime.jsx(NavMain, { items: modulesWithActiveState, orgCode: orgCode, approvalCounts: approvalCounts, closeOnNavigation: closeOnNavigation }), jsxRuntime.jsx(NavTools, { tools: tools, closeOnNavigation: closeOnNavigation })] }), jsxRuntime.jsx(SidebarFooter, { className: "pl-2 pr-2", children: footer })] }));
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Top-level app chrome: sticky header, sidebar slot, content gutter and the
|
|
971
|
+
* session/org-context plumbing that both Accounting and HR need. Apps wrap
|
|
972
|
+
* it inside their `SidebarProvider` (server-rendered with the cookie-backed
|
|
973
|
+
* `defaultOpen`) and their `ERPProvider`, then pass app-specific bits as
|
|
974
|
+
* props.
|
|
975
|
+
*
|
|
976
|
+
* The padding contract is deliberately owned here so future style tweaks
|
|
977
|
+
* (e.g. the recent left/right/bottom gutter pass) propagate to both apps via
|
|
978
|
+
* an erp-shell bump — domain components must NOT add their own root padding
|
|
979
|
+
* or content gets indented twice.
|
|
980
|
+
*/
|
|
981
|
+
function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permissionProvider, organizationId, setOrganizationId, organizations, signInUrl, selectOrganizationUrl, }) {
|
|
982
|
+
const { data: session, status } = react.useSession();
|
|
983
|
+
const { open, setOpen } = useSidebar();
|
|
984
|
+
const router = navigation.useRouter();
|
|
985
|
+
const params = navigation.useParams();
|
|
986
|
+
const code = params?.code;
|
|
987
|
+
const extendedSession = session;
|
|
988
|
+
// Find the org from the URL `[code]` so we can detect a context mismatch
|
|
989
|
+
// (the user bookmarked a different org than the cookie remembers).
|
|
990
|
+
const orgFromCode = React__namespace.useMemo(() => {
|
|
991
|
+
if (!code || !organizations.length)
|
|
992
|
+
return null;
|
|
993
|
+
return (organizations.find(org => org.code === code || String(org.id) === code) ?? null);
|
|
994
|
+
}, [code, organizations]);
|
|
995
|
+
// Sync state: when the URL code points at a different org than the cookie,
|
|
996
|
+
// trust the URL and update the cookie. Otherwise do nothing — the user
|
|
997
|
+
// wins ties via the in-app org switcher.
|
|
998
|
+
React.useEffect(() => {
|
|
999
|
+
if (!code)
|
|
1000
|
+
return;
|
|
1001
|
+
if (!organizations.length)
|
|
1002
|
+
return;
|
|
1003
|
+
if (!orgFromCode)
|
|
1004
|
+
return;
|
|
1005
|
+
if (String(organizationId) !== String(orgFromCode.id)) {
|
|
1006
|
+
void setOrganizationId(orgFromCode.id);
|
|
1007
|
+
}
|
|
1008
|
+
}, [
|
|
1009
|
+
orgFromCode,
|
|
1010
|
+
organizationId,
|
|
1011
|
+
setOrganizationId,
|
|
1012
|
+
code,
|
|
1013
|
+
organizations.length,
|
|
1014
|
+
]);
|
|
1015
|
+
// ≤1024px: sidebar renders as a modal overlay (backdrop dims content). >1024px:
|
|
1016
|
+
// sidebar is docked and the content gets a left margin to clear it.
|
|
1017
|
+
const [isSidebarModalLayout, setIsSidebarModalLayout] = React__namespace.useState(false);
|
|
1018
|
+
React.useEffect(() => {
|
|
1019
|
+
const mq = window.matchMedia("(max-width: 1024px)");
|
|
1020
|
+
const sync = () => setIsSidebarModalLayout(mq.matches);
|
|
1021
|
+
sync();
|
|
1022
|
+
mq.addEventListener("change", sync);
|
|
1023
|
+
return () => mq.removeEventListener("change", sync);
|
|
1024
|
+
}, []);
|
|
1025
|
+
// Wait for session to load (initial load only). When refetching/updating we
|
|
1026
|
+
// already have a session — don't flash the full-screen loader.
|
|
1027
|
+
if (status === "loading" && !session) {
|
|
1028
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1029
|
+
}
|
|
1030
|
+
// Session loaded but no accessToken (e.g. token refresh failed) -> bounce
|
|
1031
|
+
// through signin so we don't end up at a "No Keycloak Context" error.
|
|
1032
|
+
if (status === "authenticated" && !extendedSession?.accessToken) {
|
|
1033
|
+
if (typeof window !== "undefined") {
|
|
1034
|
+
const url = new URL(signInUrl, window.location.origin);
|
|
1035
|
+
url.searchParams.set("error", "NoAccessToken");
|
|
1036
|
+
url.searchParams.set("callbackUrl", window.location.pathname);
|
|
1037
|
+
window.location.href = url.toString();
|
|
1038
|
+
}
|
|
1039
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1040
|
+
}
|
|
1041
|
+
if (!extendedSession?.accessToken) {
|
|
1042
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1043
|
+
}
|
|
1044
|
+
if (!code) {
|
|
1045
|
+
router.push(selectOrganizationUrl);
|
|
1046
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent });
|
|
1047
|
+
}
|
|
1048
|
+
// When the sidebar is docked-open it consumes 21.5rem on the left; otherwise
|
|
1049
|
+
// the content takes the full width and we add a left gutter inside.
|
|
1050
|
+
const contentMargin = open && !isSidebarModalLayout ? "ml-[21.5rem]" : "";
|
|
1051
|
+
const needsLeftGutter = !(open && !isSidebarModalLayout);
|
|
1052
|
+
const wrap = permissionProvider ?? ((c) => c);
|
|
1053
|
+
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-6", children: [jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[100px] sm:h-[144px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsxRuntime.jsx("div", { id: "header-title" })] })] }), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6 pb-12", needsLeftGutter && "pl-6"), children: wrap(children) }, `org-${organizationId ?? "no-org"}`) })] }) })] }));
|
|
1054
|
+
}
|
|
1055
|
+
/** Local tiny classnames helper — avoids pulling clsx/tailwind-merge into
|
|
1056
|
+
* the shell just for the layout file. Falsy values are skipped. */
|
|
1057
|
+
function cx(...parts) {
|
|
1058
|
+
return parts.filter(Boolean).join(" ");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
|
|
1062
|
+
/**
|
|
1063
|
+
* Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
|
|
1064
|
+
* Renders `children` only when policy is loaded and access is granted.
|
|
1065
|
+
*/
|
|
1066
|
+
function PermissionGateShell({ policyLoading, hasPolicyAccess, deniedMessage, children, skeletonCount = 6, skeletonGridClassName = "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", skeletonVariant = "kpi", }) {
|
|
1067
|
+
if (policyLoading) {
|
|
1068
|
+
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx("div", { className: skeletonGridClassName, children: Array.from({ length: skeletonCount }).map((_, i) => (jsxRuntime.jsx(erpTheme.Card, { children: skeletonVariant === "kpi" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs(erpTheme.CardHeader, { className: "flex flex-row items-center justify-between pb-2", children: [jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-24" }), jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-4 rounded" })] }), jsxRuntime.jsxs(erpTheme.CardContent, { children: [jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-8 w-32 mb-2" }), jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-3 w-20" })] })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(erpTheme.CardHeader, { className: "pb-2", children: jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-24" }) }), jsxRuntime.jsx(erpTheme.CardContent, { children: jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-8 w-16" }) })] })) }, i))) }) }));
|
|
1069
|
+
}
|
|
1070
|
+
if (!hasPolicyAccess) {
|
|
1071
|
+
return (jsxRuntime.jsx("div", { className: shellClassName, children: jsxRuntime.jsx(erpTheme.Card, { children: jsxRuntime.jsx(erpTheme.CardContent, { className: "text-sm text-muted-foreground", children: deniedMessage }) }) }));
|
|
1072
|
+
}
|
|
1073
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const PermissionRulesContext = React.createContext(undefined);
|
|
1077
|
+
/**
|
|
1078
|
+
* Generic provider — the consumer app passes in `data` (live fetch) and
|
|
1079
|
+
* `fallbackPolicies` (static JSON). All apps share the same mapping logic
|
|
1080
|
+
* (build a flat map of route_key/policy_key → required child groups).
|
|
1081
|
+
*/
|
|
1082
|
+
function PermissionRulesProvider({ children, data, isLoading = false, isError = false, fallbackPolicies = [], }) {
|
|
1083
|
+
const rulesMap = React.useMemo(() => {
|
|
1084
|
+
const m = {};
|
|
1085
|
+
if (!data?.routes)
|
|
1086
|
+
return m;
|
|
1087
|
+
for (const row of data.routes) {
|
|
1088
|
+
m[row.route_key] = row.required_child_groups ?? [];
|
|
1089
|
+
}
|
|
1090
|
+
return m;
|
|
1091
|
+
}, [data]);
|
|
1092
|
+
const policiesMap = React.useMemo(() => {
|
|
1093
|
+
const m = {};
|
|
1094
|
+
for (const row of fallbackPolicies) {
|
|
1095
|
+
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1096
|
+
}
|
|
1097
|
+
if (data?.policies?.length) {
|
|
1098
|
+
for (const row of data.policies) {
|
|
1099
|
+
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return m;
|
|
1103
|
+
}, [data, fallbackPolicies]);
|
|
1104
|
+
const value = React.useMemo(() => ({
|
|
1105
|
+
rulesMap,
|
|
1106
|
+
policiesMap,
|
|
1107
|
+
isLoading,
|
|
1108
|
+
isError,
|
|
1109
|
+
updatedAt: data?.updated_at ?? null,
|
|
1110
|
+
}), [rulesMap, policiesMap, isLoading, isError, data?.updated_at]);
|
|
1111
|
+
return (jsxRuntime.jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Read the permission rules. Returns empty maps when used outside the
|
|
1115
|
+
* provider (lets pages render without a hard-fail in tests).
|
|
1116
|
+
*/
|
|
1117
|
+
function usePermissionRules() {
|
|
1118
|
+
const ctx = React.useContext(PermissionRulesContext);
|
|
1119
|
+
if (!ctx) {
|
|
1120
|
+
return {
|
|
1121
|
+
rulesMap: {},
|
|
1122
|
+
policiesMap: {},
|
|
1123
|
+
isLoading: false,
|
|
1124
|
+
isError: false,
|
|
1125
|
+
updatedAt: null,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return ctx;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Strip the org `[code]` prefix from a pathname so we can look it up in the
|
|
1133
|
+
* permission rules map. `/O12345/setting/foo` → `/setting/foo`. Single-segment
|
|
1134
|
+
* paths (e.g. `/select-organization`) keep their leading slash.
|
|
1135
|
+
*
|
|
1136
|
+
* Also normalizes the user-role subtab aliases — both `purchase-doc` and
|
|
1137
|
+
* `sales-doc` are gated by the same `members` rule because the UI shows
|
|
1138
|
+
* the same management screen with different filters.
|
|
1139
|
+
*/
|
|
1140
|
+
function normalizePathnameForPermissionRules(pathname) {
|
|
1141
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
1142
|
+
const path = parts.length <= 1 ? "/" : "/" + parts.slice(1).join("/");
|
|
1143
|
+
return path.replace(/\/user-role\/(purchase-doc|sales-doc)$/, "/user-role/members");
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Returns the required child-group segments for a page pathname (the user's
|
|
1147
|
+
* JWT must contain at least one matching group). An exact match on the
|
|
1148
|
+
* normalized path wins; otherwise the function falls back to a segment-by-
|
|
1149
|
+
* segment pattern match where `[param]` placeholders accept any non-empty
|
|
1150
|
+
* value. Returns an empty array when no rule covers the path.
|
|
1151
|
+
*
|
|
1152
|
+
* `rulesMap` is the merged map — apps that want a fallback should layer it
|
|
1153
|
+
* here (`{ ...fallbackMap, ...liveMap }`) before calling.
|
|
1154
|
+
*/
|
|
1155
|
+
function getRequiredChildGroups(pathname, rulesMap) {
|
|
1156
|
+
const normalized = normalizePathnameForPermissionRules(pathname);
|
|
1157
|
+
if (rulesMap[normalized]) {
|
|
1158
|
+
return rulesMap[normalized];
|
|
1159
|
+
}
|
|
1160
|
+
const pathSegments = normalized.split("/").filter(Boolean);
|
|
1161
|
+
for (const [pattern, roles] of Object.entries(rulesMap)) {
|
|
1162
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1163
|
+
if (patternSegments.length !== pathSegments.length)
|
|
1164
|
+
continue;
|
|
1165
|
+
let matches = true;
|
|
1166
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
1167
|
+
const patternSegment = patternSegments[i];
|
|
1168
|
+
const pathSegment = pathSegments[i];
|
|
1169
|
+
if (patternSegment.startsWith("[") && patternSegment.endsWith("]")) {
|
|
1170
|
+
if (!pathSegment) {
|
|
1171
|
+
matches = false;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else if (patternSegment !== pathSegment) {
|
|
1176
|
+
matches = false;
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (matches) {
|
|
1181
|
+
return roles;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Permission gate for a page subtree. Looks up the current pathname in the
|
|
1189
|
+
* permission rules map (live rules from `PermissionRulesProvider`, layered
|
|
1190
|
+
* over the optional `fallbackRulesMap`), passes the resolved required roles
|
|
1191
|
+
* into the host's `usePermission` probe, and renders one of three states:
|
|
1192
|
+
*
|
|
1193
|
+
* 1. while rules or probe are still loading → centered spinner card
|
|
1194
|
+
* 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
|
|
1195
|
+
* user roles for debugging
|
|
1196
|
+
* 3. otherwise → children
|
|
1197
|
+
*
|
|
1198
|
+
* The host wraps `<PermissionProvider>` around its page chrome (typically
|
|
1199
|
+
* via `AppLayoutShell`'s `permissionProvider` slot).
|
|
1200
|
+
*/
|
|
1201
|
+
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1202
|
+
const pathname = navigation.usePathname();
|
|
1203
|
+
const { rulesMap, isLoading: rulesLoading } = usePermissionRules();
|
|
1204
|
+
const mergedMap = React.useMemo(() => {
|
|
1205
|
+
if (!fallbackRulesMap)
|
|
1206
|
+
return rulesMap;
|
|
1207
|
+
// live rules win over fallback when both define the same key
|
|
1208
|
+
return { ...fallbackRulesMap, ...rulesMap };
|
|
1209
|
+
}, [rulesMap, fallbackRulesMap]);
|
|
1210
|
+
const requiredRoles = React.useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
|
|
1211
|
+
const { hasPermission, isLoading, userRoles } = usePermission({
|
|
1212
|
+
requiredRoles,
|
|
1213
|
+
});
|
|
1214
|
+
const isLoadingCombined = rulesLoading || isLoading;
|
|
1215
|
+
if (isLoadingCombined) {
|
|
1216
|
+
return (jsxRuntime.jsx(erpTheme.Card, { className: "p-6", children: jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-3", children: [jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }), jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E15\u0E23\u0E27\u0E08\u0E2A\u0E2D\u0E1A\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07..." })] }) }));
|
|
1217
|
+
}
|
|
1218
|
+
if (!hasPermission) {
|
|
1219
|
+
return (jsxRuntime.jsx(erpTheme.Card, { className: "p-6", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxRuntime.jsxs("div", { className: "space-y-1", children: [jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-destructive", children: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49" }), requiredRoles.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E17\u0E35\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: requiredRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-muted px-2 py-0.5 text-xs font-mono", children: role }, idx))) })] })), userRoles.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: userRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-primary/10 px-2 py-0.5 text-xs font-mono text-primary", children: role }, idx))) })] }))] })] }) }));
|
|
1220
|
+
}
|
|
1221
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1222
|
+
}
|
|
1223
|
+
|
|
969
1224
|
function ThemeProvider({ children, ...props }) {
|
|
970
1225
|
return jsxRuntime.jsx(nextThemes.ThemeProvider, { ...props, children: children });
|
|
971
1226
|
}
|
|
@@ -1067,61 +1322,6 @@ function ReactQueryProvider({ children }) {
|
|
|
1067
1322
|
return (jsxRuntime.jsxs(reactQuery.QueryClientProvider, { client: queryClient, children: [children, jsxRuntime.jsx(reactQueryDevtools.ReactQueryDevtools, { client: queryClient, initialIsOpen: false })] }));
|
|
1068
1323
|
}
|
|
1069
1324
|
|
|
1070
|
-
const PermissionRulesContext = React.createContext(undefined);
|
|
1071
|
-
/**
|
|
1072
|
-
* Generic provider — the consumer app passes in `data` (live fetch) and
|
|
1073
|
-
* `fallbackPolicies` (static JSON). All apps share the same mapping logic
|
|
1074
|
-
* (build a flat map of route_key/policy_key → required child groups).
|
|
1075
|
-
*/
|
|
1076
|
-
function PermissionRulesProvider({ children, data, isLoading = false, isError = false, fallbackPolicies = [], }) {
|
|
1077
|
-
const rulesMap = React.useMemo(() => {
|
|
1078
|
-
const m = {};
|
|
1079
|
-
if (!data?.routes)
|
|
1080
|
-
return m;
|
|
1081
|
-
for (const row of data.routes) {
|
|
1082
|
-
m[row.route_key] = row.required_child_groups ?? [];
|
|
1083
|
-
}
|
|
1084
|
-
return m;
|
|
1085
|
-
}, [data]);
|
|
1086
|
-
const policiesMap = React.useMemo(() => {
|
|
1087
|
-
const m = {};
|
|
1088
|
-
for (const row of fallbackPolicies) {
|
|
1089
|
-
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1090
|
-
}
|
|
1091
|
-
if (data?.policies?.length) {
|
|
1092
|
-
for (const row of data.policies) {
|
|
1093
|
-
m[row.policy_key] = row.required_child_groups ?? [];
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return m;
|
|
1097
|
-
}, [data, fallbackPolicies]);
|
|
1098
|
-
const value = React.useMemo(() => ({
|
|
1099
|
-
rulesMap,
|
|
1100
|
-
policiesMap,
|
|
1101
|
-
isLoading,
|
|
1102
|
-
isError,
|
|
1103
|
-
updatedAt: data?.updated_at ?? null,
|
|
1104
|
-
}), [rulesMap, policiesMap, isLoading, isError, data?.updated_at]);
|
|
1105
|
-
return (jsxRuntime.jsx(PermissionRulesContext.Provider, { value: value, children: children }));
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Read the permission rules. Returns empty maps when used outside the
|
|
1109
|
-
* provider (lets pages render without a hard-fail in tests).
|
|
1110
|
-
*/
|
|
1111
|
-
function usePermissionRules() {
|
|
1112
|
-
const ctx = React.useContext(PermissionRulesContext);
|
|
1113
|
-
if (!ctx) {
|
|
1114
|
-
return {
|
|
1115
|
-
rulesMap: {},
|
|
1116
|
-
policiesMap: {},
|
|
1117
|
-
isLoading: false,
|
|
1118
|
-
isError: false,
|
|
1119
|
-
updatedAt: null,
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
return ctx;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
1325
|
/**
|
|
1126
1326
|
* Renders nothing. Runs during render (after SessionProvider, inside
|
|
1127
1327
|
* QueryClientProvider) to seed the cached profile slot from the JWT-backed
|
|
@@ -1436,6 +1636,7 @@ Object.defineProperty(exports, "isAxiosError", {
|
|
|
1436
1636
|
});
|
|
1437
1637
|
exports.ApiError = ApiError;
|
|
1438
1638
|
exports.AppBreadcrumb = AppBreadcrumb;
|
|
1639
|
+
exports.AppLayoutShell = AppLayoutShell;
|
|
1439
1640
|
exports.AppSidebar = AppSidebar;
|
|
1440
1641
|
exports.FeatureFlagsProvider = FeatureFlagsProvider;
|
|
1441
1642
|
exports.HeaderBackground = HeaderBackground;
|
|
@@ -1443,6 +1644,8 @@ exports.NavMain = NavMain;
|
|
|
1443
1644
|
exports.NavTools = NavTools;
|
|
1444
1645
|
exports.NavUser = NavUser;
|
|
1445
1646
|
exports.OrganizationSwitcher = OrganizationSwitcher;
|
|
1647
|
+
exports.PermissionGateShell = PermissionGateShell;
|
|
1648
|
+
exports.PermissionProvider = PermissionProvider;
|
|
1446
1649
|
exports.PermissionRulesProvider = PermissionRulesProvider;
|
|
1447
1650
|
exports.ReactQueryProvider = ReactQueryProvider;
|
|
1448
1651
|
exports.SessionBackedProfileCacheSeeder = SessionBackedProfileCacheSeeder;
|
|
@@ -1468,8 +1671,10 @@ exports.getActiveSubModule = getActiveSubModule;
|
|
|
1468
1671
|
exports.getAxiosErrorMessage = getAxiosErrorMessage;
|
|
1469
1672
|
exports.getOrganizationId = getOrganizationId;
|
|
1470
1673
|
exports.getQueryClientConfig = getQueryClientConfig;
|
|
1674
|
+
exports.getRequiredChildGroups = getRequiredChildGroups;
|
|
1471
1675
|
exports.isLikelyTransientNetworkError = isLikelyTransientNetworkError;
|
|
1472
1676
|
exports.isPathActive = isPathActive;
|
|
1677
|
+
exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;
|
|
1473
1678
|
exports.runGlobalRequestPrecheck = runGlobalRequestPrecheck;
|
|
1474
1679
|
exports.runWithTransientRetry = runWithTransientRetry;
|
|
1475
1680
|
exports.setOrganizationId = setOrganizationId;
|