@eintrek/erp-shell 0.1.21 → 0.1.22
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @eintrek/erp-shell
|
|
2
2
|
|
|
3
|
-
Shared frontend platform for ERP apps (erp-accounting, erp-hr, …).
|
|
3
|
+
Shared frontend platform for ERP apps (erp-accounting, erp-hr, erp-platform, …).
|
|
4
4
|
|
|
5
5
|
Holds the cross-app glue that was previously duplicated in every consumer:
|
|
6
6
|
HTTP client, auth/token plumbing, organization-id context, app chrome
|
|
@@ -81,7 +81,8 @@ Shell now owns the **shapes + helpers** for navigation; apps own the data.
|
|
|
81
81
|
slot for app narrowing), `Tool`, `Company`, `NavigationData`.
|
|
82
82
|
- Helpers: `canAccessNavPermissionRoute`, `filterNavigationToolsByPermission`,
|
|
83
83
|
`filterNavigationModulesByPermission`, `getActiveModule(pathname, modules)`,
|
|
84
|
-
`getActiveSubModule(pathname, modules)`, `buildUrlWithCode(code, url)
|
|
84
|
+
`getActiveSubModule(pathname, modules)`, `buildUrlWithCode(code, url)`
|
|
85
|
+
(absolute `http(s)://` URLs pass through; `{code}` placeholder is replaced).
|
|
85
86
|
- Components: `NavTools` (reads Tool[] + buildUrlWithCode internally),
|
|
86
87
|
`AppBreadcrumb` (accepts `labels` prop so per-app Thai overrides stay local).
|
|
87
88
|
|
package/dist/index.esm.js
CHANGED
|
@@ -692,8 +692,15 @@ function getActiveSubModule(pathname, modules) {
|
|
|
692
692
|
}
|
|
693
693
|
return null;
|
|
694
694
|
}
|
|
695
|
-
/** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`.
|
|
695
|
+
/** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`.
|
|
696
|
+
* Absolute `http(s)://` URLs are left intact; optional `{code}` placeholder is replaced.
|
|
697
|
+
*/
|
|
696
698
|
function buildUrlWithCode(code, url) {
|
|
699
|
+
if (/^https?:\/\//i.test(url)) {
|
|
700
|
+
if (!code)
|
|
701
|
+
return url.split("{code}").join("");
|
|
702
|
+
return url.split("{code}").join(code);
|
|
703
|
+
}
|
|
697
704
|
if (!code)
|
|
698
705
|
return url;
|
|
699
706
|
const cleanUrl = url.startsWith("/") ? url.slice(1) : url;
|
|
@@ -840,7 +847,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
840
847
|
{ code: "zh", label: "中文" },
|
|
841
848
|
{ code: "ja", label: "日本語" },
|
|
842
849
|
].map(({ code, label }) => (jsxs("button", { onClick: () => handleLanguageChange(code), 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: [jsx("span", { className: "flex-1 text-left", children: label }), organization?.language === code && (jsx("div", { className: "h-2 w-2 rounded-full bg-primary" }))] }, code))) }))] }), profileHref && (jsxs("button", { onClick: handleProfile, 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(User, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E2A\u0E48\u0E27\u0E19\u0E15\u0E31\u0E27" })] })), jsxs("button", { onClick: handleSelectOrganization, 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(Building2, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E18\u0E38\u0E23\u0E01\u0E34\u0E08" })] }), 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;
|
|
843
|
-
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: [jsx(AvatarImage, { src: user.avatar, alt: user.name }), jsx(AvatarFallback, { className: "rounded-lg bg-[#E4525F] text-sidebar-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" &&
|
|
850
|
+
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-[#E4525F] text-sidebar-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" &&
|
|
844
851
|
createPortal(dropdownContent, document.body)] }) }));
|
|
845
852
|
}
|
|
846
853
|
|
|
@@ -919,6 +926,17 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, })
|
|
|
919
926
|
}) }) }));
|
|
920
927
|
}
|
|
921
928
|
|
|
929
|
+
function OrgMark({ logoUrl, size = "md", }) {
|
|
930
|
+
const [failed, setFailed] = useState(false);
|
|
931
|
+
const box = size === "sm"
|
|
932
|
+
? "flex size-6 items-center justify-center rounded-md border overflow-hidden bg-background"
|
|
933
|
+
: "flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground overflow-hidden";
|
|
934
|
+
const icon = size === "sm" ? "size-3.5 shrink-0" : "size-4";
|
|
935
|
+
if (logoUrl && !failed) {
|
|
936
|
+
return (jsx("div", { className: cn(box, size === "md" && "bg-white"), children: jsx("img", { src: logoUrl, alt: "", className: "size-full object-contain p-0.5", onError: () => setFailed(true) }) }));
|
|
937
|
+
}
|
|
938
|
+
return (jsx("div", { className: box, children: jsx(Building2, { className: icon }) }));
|
|
939
|
+
}
|
|
922
940
|
function OrganizationSwitcher({ activeOrganization, isContextLoading = false, organizations, isLoadingOrgs = false, setOrganizationCookie, registerOrganizationHref = "/register-organization", className, }) {
|
|
923
941
|
const { isMobile } = useSidebarToggle();
|
|
924
942
|
const router = useRouter();
|
|
@@ -964,10 +982,10 @@ function OrganizationSwitcher({ activeOrganization, isContextLoading = false, or
|
|
|
964
982
|
return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx(Skeleton, { className: "size-8 rounded-lg" }), jsx(Skeleton, { className: "h-4 w-24" })] }) }) }));
|
|
965
983
|
}
|
|
966
984
|
if (organizations.length <= 1) {
|
|
967
|
-
return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx(
|
|
985
|
+
return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx(OrgMark, { logoUrl: activeOrganization?.logoUrl }), jsx("span", { className: "truncate font-semibold text-sm min-w-0 flex-1", children: activeOrganization?.name || "ไม่มีองค์กร" })] }) }) }));
|
|
968
986
|
}
|
|
969
|
-
return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [jsx(
|
|
970
|
-
"bg-accent"), children: [jsx(
|
|
987
|
+
return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [jsx(OrgMark, { logoUrl: activeOrganization?.logoUrl }), jsx("span", { className: "truncate font-semibold text-sm flex-1 min-w-0 text-left", children: activeOrganization?.name || "เลือกองค์กร" }), isPending ? (jsx(Loader2, { className: "ml-auto size-4 animate-spin" })) : (jsx(ChevronsUpDown, { className: "ml-auto size-4" }))] }) }), jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" }), organizations.map(org => (jsxs(DropdownMenuItem, { onClick: () => handleOrganizationChange(org), disabled: isPending, className: cn("gap-2 p-2 cursor-pointer", String(activeOrganization?.id) === String(org.id) &&
|
|
988
|
+
"bg-accent"), children: [jsx(OrgMark, { logoUrl: org.logoUrl, size: "sm" }), jsx("span", { className: "truncate", children: org.name })] }, org.id))), jsx(DropdownMenuSeparator, {}), jsxs(DropdownMenuItem, { className: "gap-2 p-2 cursor-pointer", onClick: () => router.push(registerOrganizationHref), children: [jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-transparent", children: jsx(Plus, { className: "size-4" }) }), jsx("span", { className: "text-muted-foreground font-medium", children: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] })] }) }) }));
|
|
971
989
|
}
|
|
972
990
|
|
|
973
991
|
function SidebarHeader({ children, className }) {
|
|
@@ -1419,7 +1437,9 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
|
|
|
1419
1437
|
if (mapped) {
|
|
1420
1438
|
const existing = queryClient.getQueryData(queryKey);
|
|
1421
1439
|
if (!existing) {
|
|
1422
|
-
|
|
1440
|
+
// updatedAt: 0 → immediately stale so observers still GET /myprofile
|
|
1441
|
+
// for fields the JWT seed omits (avatar/signature paths).
|
|
1442
|
+
queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
|
|
1423
1443
|
}
|
|
1424
1444
|
else {
|
|
1425
1445
|
// Only overwrite when the mapped id changed — we don't know T's shape
|
|
@@ -1427,7 +1447,7 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
|
|
|
1427
1447
|
const existingId = existing?.id;
|
|
1428
1448
|
const mappedId = mapped?.id;
|
|
1429
1449
|
if (existingId !== mappedId) {
|
|
1430
|
-
queryClient.setQueryData(queryKey, mapped);
|
|
1450
|
+
queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
|
|
1431
1451
|
}
|
|
1432
1452
|
}
|
|
1433
1453
|
}
|