@eintrek/erp-shell 0.1.22 → 0.1.24
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-provider.d.ts +3 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +91 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +90 -9
- package/dist/index.js.map +1 -1
- package/dist/navigation/nav-user.d.ts +13 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -759,7 +759,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
|
|
|
759
759
|
})] }) }));
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
-
function
|
|
762
|
+
function isExternalHref(href) {
|
|
763
|
+
return /^https?:\/\//i.test(href) || href.startsWith("//");
|
|
764
|
+
}
|
|
765
|
+
function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, } = {}) {
|
|
763
766
|
const { data: session, status } = react.useSession();
|
|
764
767
|
const router = navigation.useRouter();
|
|
765
768
|
const { theme, setTheme } = nextThemes.useTheme();
|
|
@@ -811,6 +814,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
811
814
|
if (profileHref)
|
|
812
815
|
router.push(profileHref);
|
|
813
816
|
};
|
|
817
|
+
const handleSystemLink = (href) => {
|
|
818
|
+
setIsOpen(false);
|
|
819
|
+
if (isExternalHref(href)) {
|
|
820
|
+
window.location.assign(href);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
router.push(href);
|
|
824
|
+
};
|
|
814
825
|
const handleThemeChange = (newTheme) => {
|
|
815
826
|
setTheme(newTheme);
|
|
816
827
|
setShowThemeMenu(false);
|
|
@@ -843,6 +854,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
843
854
|
}
|
|
844
855
|
if (!user)
|
|
845
856
|
return null;
|
|
857
|
+
const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
|
|
846
858
|
const dropdownContent = isOpen ? (jsxRuntime.jsxs("div", { "data-dropdown": "nav-user", className: "fixed w-52 sm:w-60 md:w-80 rounded-lg bg-sidebar border border-sidebar-border shadow-xl z-50 overflow-hidden\n -translate-x-[65%] -translate-y-[110%] sm:translate-x-0 sm:-translate-y-full", style: { top: `${position.top}px`, left: `${position.left}px` }, children: [organization && (jsxRuntime.jsx("div", { className: "p-4 border-b border-sidebar-border bg-sidebar-accent/30", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [organization.logoUrl ? (
|
|
847
859
|
// eslint-disable-next-line @next/next/no-img-element
|
|
848
860
|
jsxRuntime.jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
|
|
@@ -850,7 +862,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
850
862
|
// sidebar; drop back to the generic icon's empty slot.
|
|
851
863
|
onError: e => {
|
|
852
864
|
e.currentTarget.style.display = "none";
|
|
853
|
-
} })) : (jsxRuntime.jsx(lucideReact.Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsxRuntime.jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxRuntime.jsxs("div", { className: "p-2", children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { onClick: () => {
|
|
865
|
+
} })) : (jsxRuntime.jsx(lucideReact.Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsxRuntime.jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxRuntime.jsxs("div", { className: "p-2", children: [visibleSystemLinks.length > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [visibleSystemLinks.map(link => (jsxRuntime.jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), 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: [link.icon ?? jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: link.label })] }, `${link.label}:${link.href}`))), jsxRuntime.jsx("div", { className: "my-1 border-t border-sidebar-border" })] })) : null, jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { onClick: () => {
|
|
854
866
|
setShowThemeMenu(!showThemeMenu);
|
|
855
867
|
setShowLanguageMenu(false);
|
|
856
868
|
}, 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: [jsxRuntime.jsx(lucideReact.Palette, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E18\u0E35\u0E21" }), jsxRuntime.jsx(lucideReact.ChevronUp, { className: `h-4 w-4 transition-transform ${showThemeMenu ? "rotate-180" : ""}` })] }), showThemeMenu && (jsxRuntime.jsx("div", { className: "mt-1 ml-4 space-y-1", children: [
|
|
@@ -1284,12 +1296,11 @@ function getRequiredChildGroups(pathname, rulesMap) {
|
|
|
1284
1296
|
* into the host's `usePermission` probe, and renders one of three states:
|
|
1285
1297
|
*
|
|
1286
1298
|
* 1. while rules or probe are still loading → centered spinner card
|
|
1287
|
-
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
1288
|
-
* user roles for debugging
|
|
1299
|
+
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
1289
1300
|
* 3. otherwise → children
|
|
1290
1301
|
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1302
|
+
* Role codes are intentionally not shown on the deny screen (they are
|
|
1303
|
+
* internal identifiers like `fin_controller` and confuse end users).
|
|
1293
1304
|
*/
|
|
1294
1305
|
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1295
1306
|
const pathname = navigation.usePathname();
|
|
@@ -1303,7 +1314,7 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1303
1314
|
const requiredRoles = React.useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
|
|
1304
1315
|
// Hook must run unconditionally (rules of hooks); we just ignore the
|
|
1305
1316
|
// result on the fast path below.
|
|
1306
|
-
const { hasPermission, isLoading
|
|
1317
|
+
const { hasPermission, isLoading } = usePermission({
|
|
1307
1318
|
requiredRoles,
|
|
1308
1319
|
});
|
|
1309
1320
|
// Fast path — no rule covers this pathname, anyone can see it. Skip the
|
|
@@ -1324,13 +1335,83 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1324
1335
|
return null;
|
|
1325
1336
|
}
|
|
1326
1337
|
if (!hasPermission) {
|
|
1327
|
-
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
|
|
1338
|
+
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 \u0E2B\u0E32\u0E01\u0E04\u0E34\u0E14\u0E27\u0E48\u0E32\u0E04\u0E27\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E44\u0E14\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E23\u0E30\u0E1A\u0E1A\u0E02\u0E2D\u0E07\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] }) }));
|
|
1328
1339
|
}
|
|
1329
1340
|
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
1330
1341
|
}
|
|
1331
1342
|
|
|
1343
|
+
/** Cookie name for the cross-subdomain theme choice. */
|
|
1344
|
+
const THEME_COOKIE = "erp-theme";
|
|
1345
|
+
/**
|
|
1346
|
+
* Parent domain to scope the cookie to, so every `*.antniti.com` app reads the
|
|
1347
|
+
* same value. Returns `null` on localhost / bare IPs (host-only cookie).
|
|
1348
|
+
*/
|
|
1349
|
+
function sharedCookieDomain() {
|
|
1350
|
+
if (typeof window === "undefined")
|
|
1351
|
+
return null;
|
|
1352
|
+
const host = window.location.hostname;
|
|
1353
|
+
if (host === "localhost" || /^[0-9.]+$/.test(host))
|
|
1354
|
+
return null;
|
|
1355
|
+
const parts = host.split(".");
|
|
1356
|
+
if (parts.length < 2)
|
|
1357
|
+
return null;
|
|
1358
|
+
return "." + parts.slice(-2).join(".");
|
|
1359
|
+
}
|
|
1360
|
+
function readThemeCookie() {
|
|
1361
|
+
if (typeof document === "undefined")
|
|
1362
|
+
return null;
|
|
1363
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + THEME_COOKIE + "=([^;]*)"));
|
|
1364
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
1365
|
+
}
|
|
1366
|
+
function writeThemeCookie(value) {
|
|
1367
|
+
if (typeof document === "undefined")
|
|
1368
|
+
return;
|
|
1369
|
+
const domain = sharedCookieDomain();
|
|
1370
|
+
const parts = [
|
|
1371
|
+
`${THEME_COOKIE}=${encodeURIComponent(value)}`,
|
|
1372
|
+
"path=/",
|
|
1373
|
+
"max-age=31536000", // 1 year
|
|
1374
|
+
"SameSite=Lax",
|
|
1375
|
+
];
|
|
1376
|
+
if (domain)
|
|
1377
|
+
parts.push(`domain=${domain}`);
|
|
1378
|
+
document.cookie = parts.join("; ");
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Bridges next-themes (per-origin localStorage) with a cookie scoped to the
|
|
1382
|
+
* parent domain, so the light/dark/system choice is shared across the
|
|
1383
|
+
* accounting / HR / platform subdomains. On load it adopts the shared cookie;
|
|
1384
|
+
* on every later change it writes the cookie back.
|
|
1385
|
+
*/
|
|
1386
|
+
function ThemeCookieSync() {
|
|
1387
|
+
const { theme, setTheme } = nextThemes.useTheme();
|
|
1388
|
+
const skipFirstWrite = React__namespace.useRef(true);
|
|
1389
|
+
React__namespace.useEffect(() => {
|
|
1390
|
+
const cookie = readThemeCookie();
|
|
1391
|
+
if (cookie) {
|
|
1392
|
+
if (cookie !== theme)
|
|
1393
|
+
setTheme(cookie);
|
|
1394
|
+
}
|
|
1395
|
+
else if (theme) {
|
|
1396
|
+
writeThemeCookie(theme); // seed the shared cookie the first time
|
|
1397
|
+
}
|
|
1398
|
+
// Run once on mount — `theme` is intentionally not a dependency here.
|
|
1399
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1400
|
+
}, []);
|
|
1401
|
+
React__namespace.useEffect(() => {
|
|
1402
|
+
// Skip the initial render; only persist genuine changes (incl. the
|
|
1403
|
+
// setTheme() triggered by adopting the cookie above — harmless re-write).
|
|
1404
|
+
if (skipFirstWrite.current) {
|
|
1405
|
+
skipFirstWrite.current = false;
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (theme)
|
|
1409
|
+
writeThemeCookie(theme);
|
|
1410
|
+
}, [theme]);
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1332
1413
|
function ThemeProvider({ children, ...props }) {
|
|
1333
|
-
return jsxRuntime.
|
|
1414
|
+
return (jsxRuntime.jsxs(nextThemes.ThemeProvider, { ...props, children: [jsxRuntime.jsx(ThemeCookieSync, {}), children] }));
|
|
1334
1415
|
}
|
|
1335
1416
|
|
|
1336
1417
|
const defaultFlags = {
|