@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
|
@@ -34,11 +34,10 @@ export interface PermissionProviderProps {
|
|
|
34
34
|
* into the host's `usePermission` probe, and renders one of three states:
|
|
35
35
|
*
|
|
36
36
|
* 1. while rules or probe are still loading → centered spinner card
|
|
37
|
-
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
38
|
-
* user roles for debugging
|
|
37
|
+
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
39
38
|
* 3. otherwise → children
|
|
40
39
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Role codes are intentionally not shown on the deny screen (they are
|
|
41
|
+
* internal identifiers like `fin_controller` and confuse end users).
|
|
43
42
|
*/
|
|
44
43
|
export declare function PermissionProvider({ children, usePermission, fallbackRulesMap, }: PermissionProviderProps): import("react/jsx-runtime").JSX.Element | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { useSidebarToggle } from "./navigation/use-sidebar-toggle";
|
|
|
13
13
|
export { TapMenu } from "./navigation/tap-menu";
|
|
14
14
|
export { NavTools } from "./navigation/nav-tools";
|
|
15
15
|
export { AppBreadcrumb } from "./navigation/app-breadcrumb";
|
|
16
|
-
export { NavUser, type NavUserProps } from "./navigation/nav-user";
|
|
16
|
+
export { NavUser, type NavUserProps, type NavUserSystemLink } from "./navigation/nav-user";
|
|
17
17
|
export { NavMain, type NavMainProps } from "./navigation/nav-main";
|
|
18
18
|
export { OrganizationSwitcher, type OrganizationSwitcherProps, type OrganizationLike, } from "./navigation/organization-switcher";
|
|
19
19
|
export { SidebarHeader } from "./navigation/sidebar-header";
|
package/dist/index.esm.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useSession, signOut } from 'next-auth/react';
|
|
|
5
5
|
import { useQueryClient, MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
6
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
7
7
|
import { cn, Button, Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage, Avatar, AvatarImage, AvatarFallback, Collapsible, CollapsibleTrigger, CollapsibleContent, Badge, Skeleton, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, Card, CardHeader, CardContent } from '@eintrek/erp-theme';
|
|
8
|
-
import { PanelLeftClose, Menu, ChevronRight, Building2, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
|
|
8
|
+
import { PanelLeftClose, Menu, ChevronRight, Building2, ExternalLink, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
|
|
9
9
|
import { create } from 'zustand';
|
|
10
10
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
11
11
|
import { useRouter, usePathname, useParams } from 'next/navigation';
|
|
@@ -740,7 +740,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
|
|
|
740
740
|
})] }) }));
|
|
741
741
|
}
|
|
742
742
|
|
|
743
|
-
function
|
|
743
|
+
function isExternalHref(href) {
|
|
744
|
+
return /^https?:\/\//i.test(href) || href.startsWith("//");
|
|
745
|
+
}
|
|
746
|
+
function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, } = {}) {
|
|
744
747
|
const { data: session, status } = useSession();
|
|
745
748
|
const router = useRouter();
|
|
746
749
|
const { theme, setTheme } = useTheme();
|
|
@@ -792,6 +795,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
792
795
|
if (profileHref)
|
|
793
796
|
router.push(profileHref);
|
|
794
797
|
};
|
|
798
|
+
const handleSystemLink = (href) => {
|
|
799
|
+
setIsOpen(false);
|
|
800
|
+
if (isExternalHref(href)) {
|
|
801
|
+
window.location.assign(href);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
router.push(href);
|
|
805
|
+
};
|
|
795
806
|
const handleThemeChange = (newTheme) => {
|
|
796
807
|
setTheme(newTheme);
|
|
797
808
|
setShowThemeMenu(false);
|
|
@@ -824,6 +835,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
824
835
|
}
|
|
825
836
|
if (!user)
|
|
826
837
|
return null;
|
|
838
|
+
const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
|
|
827
839
|
const dropdownContent = isOpen ? (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 && (jsx("div", { className: "p-4 border-b border-sidebar-border bg-sidebar-accent/30", children: jsxs("div", { className: "flex items-center gap-3", children: [organization.logoUrl ? (
|
|
828
840
|
// eslint-disable-next-line @next/next/no-img-element
|
|
829
841
|
jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
|
|
@@ -831,7 +843,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
831
843
|
// sidebar; drop back to the generic icon's empty slot.
|
|
832
844
|
onError: e => {
|
|
833
845
|
e.currentTarget.style.display = "none";
|
|
834
|
-
} })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
|
|
846
|
+
} })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [visibleSystemLinks.length > 0 ? (jsxs(Fragment$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.5 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("div", { className: "my-1 border-t border-sidebar-border" })] })) : null, jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
|
|
835
847
|
setShowThemeMenu(!showThemeMenu);
|
|
836
848
|
setShowLanguageMenu(false);
|
|
837
849
|
}, 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(Palette, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E18\u0E35\u0E21" }), jsx(ChevronUp, { className: `h-4 w-4 transition-transform ${showThemeMenu ? "rotate-180" : ""}` })] }), showThemeMenu && (jsx("div", { className: "mt-1 ml-4 space-y-1", children: [
|
|
@@ -1265,12 +1277,11 @@ function getRequiredChildGroups(pathname, rulesMap) {
|
|
|
1265
1277
|
* into the host's `usePermission` probe, and renders one of three states:
|
|
1266
1278
|
*
|
|
1267
1279
|
* 1. while rules or probe are still loading → centered spinner card
|
|
1268
|
-
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
1269
|
-
* user roles for debugging
|
|
1280
|
+
* 2. probe says no access → centered "ไม่มีสิทธิ์" card
|
|
1270
1281
|
* 3. otherwise → children
|
|
1271
1282
|
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1283
|
+
* Role codes are intentionally not shown on the deny screen (they are
|
|
1284
|
+
* internal identifiers like `fin_controller` and confuse end users).
|
|
1274
1285
|
*/
|
|
1275
1286
|
function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
1276
1287
|
const pathname = usePathname();
|
|
@@ -1284,7 +1295,7 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1284
1295
|
const requiredRoles = useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
|
|
1285
1296
|
// Hook must run unconditionally (rules of hooks); we just ignore the
|
|
1286
1297
|
// result on the fast path below.
|
|
1287
|
-
const { hasPermission, isLoading
|
|
1298
|
+
const { hasPermission, isLoading } = usePermission({
|
|
1288
1299
|
requiredRoles,
|
|
1289
1300
|
});
|
|
1290
1301
|
// Fast path — no rule covers this pathname, anyone can see it. Skip the
|
|
@@ -1305,13 +1316,83 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
|
|
|
1305
1316
|
return null;
|
|
1306
1317
|
}
|
|
1307
1318
|
if (!hasPermission) {
|
|
1308
|
-
return (jsx(Card, { className: "p-6", children: jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxs("div", { className: "space-y-1", children: [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" }), 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
|
|
1319
|
+
return (jsx(Card, { className: "p-6", children: jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxs("div", { className: "space-y-1", children: [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" }), 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" })] })] }) }));
|
|
1309
1320
|
}
|
|
1310
1321
|
return jsx(Fragment$1, { children: children });
|
|
1311
1322
|
}
|
|
1312
1323
|
|
|
1324
|
+
/** Cookie name for the cross-subdomain theme choice. */
|
|
1325
|
+
const THEME_COOKIE = "erp-theme";
|
|
1326
|
+
/**
|
|
1327
|
+
* Parent domain to scope the cookie to, so every `*.antniti.com` app reads the
|
|
1328
|
+
* same value. Returns `null` on localhost / bare IPs (host-only cookie).
|
|
1329
|
+
*/
|
|
1330
|
+
function sharedCookieDomain() {
|
|
1331
|
+
if (typeof window === "undefined")
|
|
1332
|
+
return null;
|
|
1333
|
+
const host = window.location.hostname;
|
|
1334
|
+
if (host === "localhost" || /^[0-9.]+$/.test(host))
|
|
1335
|
+
return null;
|
|
1336
|
+
const parts = host.split(".");
|
|
1337
|
+
if (parts.length < 2)
|
|
1338
|
+
return null;
|
|
1339
|
+
return "." + parts.slice(-2).join(".");
|
|
1340
|
+
}
|
|
1341
|
+
function readThemeCookie() {
|
|
1342
|
+
if (typeof document === "undefined")
|
|
1343
|
+
return null;
|
|
1344
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + THEME_COOKIE + "=([^;]*)"));
|
|
1345
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
1346
|
+
}
|
|
1347
|
+
function writeThemeCookie(value) {
|
|
1348
|
+
if (typeof document === "undefined")
|
|
1349
|
+
return;
|
|
1350
|
+
const domain = sharedCookieDomain();
|
|
1351
|
+
const parts = [
|
|
1352
|
+
`${THEME_COOKIE}=${encodeURIComponent(value)}`,
|
|
1353
|
+
"path=/",
|
|
1354
|
+
"max-age=31536000", // 1 year
|
|
1355
|
+
"SameSite=Lax",
|
|
1356
|
+
];
|
|
1357
|
+
if (domain)
|
|
1358
|
+
parts.push(`domain=${domain}`);
|
|
1359
|
+
document.cookie = parts.join("; ");
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Bridges next-themes (per-origin localStorage) with a cookie scoped to the
|
|
1363
|
+
* parent domain, so the light/dark/system choice is shared across the
|
|
1364
|
+
* accounting / HR / platform subdomains. On load it adopts the shared cookie;
|
|
1365
|
+
* on every later change it writes the cookie back.
|
|
1366
|
+
*/
|
|
1367
|
+
function ThemeCookieSync() {
|
|
1368
|
+
const { theme, setTheme } = useTheme();
|
|
1369
|
+
const skipFirstWrite = React.useRef(true);
|
|
1370
|
+
React.useEffect(() => {
|
|
1371
|
+
const cookie = readThemeCookie();
|
|
1372
|
+
if (cookie) {
|
|
1373
|
+
if (cookie !== theme)
|
|
1374
|
+
setTheme(cookie);
|
|
1375
|
+
}
|
|
1376
|
+
else if (theme) {
|
|
1377
|
+
writeThemeCookie(theme); // seed the shared cookie the first time
|
|
1378
|
+
}
|
|
1379
|
+
// Run once on mount — `theme` is intentionally not a dependency here.
|
|
1380
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1381
|
+
}, []);
|
|
1382
|
+
React.useEffect(() => {
|
|
1383
|
+
// Skip the initial render; only persist genuine changes (incl. the
|
|
1384
|
+
// setTheme() triggered by adopting the cookie above — harmless re-write).
|
|
1385
|
+
if (skipFirstWrite.current) {
|
|
1386
|
+
skipFirstWrite.current = false;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (theme)
|
|
1390
|
+
writeThemeCookie(theme);
|
|
1391
|
+
}, [theme]);
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1313
1394
|
function ThemeProvider({ children, ...props }) {
|
|
1314
|
-
return
|
|
1395
|
+
return (jsxs(ThemeProvider$1, { ...props, children: [jsx(ThemeCookieSync, {}), children] }));
|
|
1315
1396
|
}
|
|
1316
1397
|
|
|
1317
1398
|
const defaultFlags = {
|