@nubitio/admin 0.5.16 → 0.5.20
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/index.cjs +223 -5
- package/dist/index.d.cts +131 -19
- package/dist/index.d.mts +131 -19
- package/dist/index.mjs +223 -9
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,10 @@ react = __toESM(react, 1);
|
|
|
26
26
|
let react_router_dom = require("react-router-dom");
|
|
27
27
|
let _nubitio_ui = require("@nubitio/ui");
|
|
28
28
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
let _tanstack_react_query = require("@tanstack/react-query");
|
|
30
|
+
let _nubitio_core = require("@nubitio/core");
|
|
31
|
+
let _nubitio_crud = require("@nubitio/crud");
|
|
32
|
+
let _nubitio_hydra = require("@nubitio/hydra");
|
|
29
33
|
//#region packages/admin/AdminHeader.tsx
|
|
30
34
|
function ActionPopover({ action }) {
|
|
31
35
|
const { open, toggle, setOpen, containerRef } = (0, _nubitio_ui.useFloatingPanel)();
|
|
@@ -385,14 +389,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
385
389
|
//#endregion
|
|
386
390
|
//#region packages/admin/auth/SessionContext.tsx
|
|
387
391
|
const SessionContext = (0, react.createContext)(null);
|
|
388
|
-
function joinApiPath$
|
|
392
|
+
function joinApiPath$2(apiBaseUrl, path) {
|
|
389
393
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
390
394
|
}
|
|
391
395
|
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
392
396
|
const [session, setSession] = (0, react.useState)({ status: "loading" });
|
|
393
397
|
const refresh = (0, react.useCallback)(async () => {
|
|
394
398
|
try {
|
|
395
|
-
const response = await fetch(joinApiPath$
|
|
399
|
+
const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
|
|
396
400
|
if (!response.ok) {
|
|
397
401
|
setSession({ status: "anonymous" });
|
|
398
402
|
return;
|
|
@@ -409,7 +413,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
409
413
|
refresh();
|
|
410
414
|
}, [refresh]);
|
|
411
415
|
const logout = (0, react.useCallback)(async () => {
|
|
412
|
-
await fetch(joinApiPath$
|
|
416
|
+
await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
|
|
413
417
|
method: "POST",
|
|
414
418
|
credentials: "include"
|
|
415
419
|
});
|
|
@@ -438,7 +442,7 @@ function useSession() {
|
|
|
438
442
|
}
|
|
439
443
|
//#endregion
|
|
440
444
|
//#region packages/admin/auth/LoginPage.tsx
|
|
441
|
-
function joinApiPath(apiBaseUrl, path) {
|
|
445
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
442
446
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
443
447
|
}
|
|
444
448
|
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
@@ -451,7 +455,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
451
455
|
setBusy(true);
|
|
452
456
|
setError(null);
|
|
453
457
|
try {
|
|
454
|
-
const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
|
|
458
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
|
|
455
459
|
method: "POST",
|
|
456
460
|
headers: { "Content-Type": "application/json" },
|
|
457
461
|
credentials: "include",
|
|
@@ -558,6 +562,48 @@ function useAppRuntime() {
|
|
|
558
562
|
};
|
|
559
563
|
}
|
|
560
564
|
//#endregion
|
|
565
|
+
//#region packages/admin/runtime/useRuntimeConfig.ts
|
|
566
|
+
function joinApiPath(apiBaseUrl, path) {
|
|
567
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
568
|
+
}
|
|
569
|
+
function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabled = true } = {}) {
|
|
570
|
+
const [state, setState] = (0, react.useState)({ status: "idle" });
|
|
571
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
572
|
+
if (!enabled) {
|
|
573
|
+
setState({ status: "idle" });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
setState({ status: "loading" });
|
|
577
|
+
try {
|
|
578
|
+
const response = await fetch(joinApiPath(apiBaseUrl, path), { credentials: "include" });
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
setState({ status: "error" });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
setState({
|
|
584
|
+
status: "ready",
|
|
585
|
+
config: await response.json()
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
setState({ status: "error" });
|
|
589
|
+
}
|
|
590
|
+
}, [
|
|
591
|
+
apiBaseUrl,
|
|
592
|
+
enabled,
|
|
593
|
+
path
|
|
594
|
+
]);
|
|
595
|
+
(0, react.useEffect)(() => {
|
|
596
|
+
refresh();
|
|
597
|
+
}, [refresh]);
|
|
598
|
+
return {
|
|
599
|
+
state,
|
|
600
|
+
config: state.status === "ready" ? state.config : null,
|
|
601
|
+
loading: state.status === "loading",
|
|
602
|
+
error: state.status === "error",
|
|
603
|
+
refresh
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
561
607
|
//#region packages/admin/runtime/ToastHost.tsx
|
|
562
608
|
const TYPE_CLASS = {
|
|
563
609
|
success: "nb-toast--success",
|
|
@@ -583,13 +629,185 @@ function ToastHost({ toasts, onDismiss }) {
|
|
|
583
629
|
});
|
|
584
630
|
}
|
|
585
631
|
//#endregion
|
|
632
|
+
//#region packages/admin/app/filterMenuByRoles.ts
|
|
633
|
+
function hasAnyRole(required, roles) {
|
|
634
|
+
if (required === void 0) return true;
|
|
635
|
+
return (Array.isArray(required) ? required : [required]).some((role) => roles.includes(role));
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
|
|
639
|
+
* `roles` are filtered; empty parent groups are dropped.
|
|
640
|
+
*/
|
|
641
|
+
function filterMenuByRoles(items, roles) {
|
|
642
|
+
const filtered = [];
|
|
643
|
+
for (const item of items) {
|
|
644
|
+
if (!hasAnyRole(item.roles, roles)) continue;
|
|
645
|
+
if (!item.items) {
|
|
646
|
+
filtered.push({
|
|
647
|
+
text: item.text,
|
|
648
|
+
path: item.path,
|
|
649
|
+
icon: item.icon
|
|
650
|
+
});
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const subItems = item.items.filter((subItem) => hasAnyRole(subItem.roles, roles)).map(({ text, path }) => ({
|
|
654
|
+
text,
|
|
655
|
+
path
|
|
656
|
+
}));
|
|
657
|
+
if (subItems.length === 0) continue;
|
|
658
|
+
filtered.push({
|
|
659
|
+
text: item.text,
|
|
660
|
+
icon: item.icon,
|
|
661
|
+
path: item.path,
|
|
662
|
+
items: subItems
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return filtered;
|
|
666
|
+
}
|
|
667
|
+
function resolveAppMenu(menu, ctx) {
|
|
668
|
+
return typeof menu === "function" ? menu(ctx) : menu;
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region packages/admin/app/createNubitApp.tsx
|
|
672
|
+
function defaultUserMenu({ username, close, logout }) {
|
|
673
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
674
|
+
style: {
|
|
675
|
+
display: "flex",
|
|
676
|
+
flexDirection: "column",
|
|
677
|
+
gap: 8,
|
|
678
|
+
minWidth: 180
|
|
679
|
+
},
|
|
680
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
681
|
+
style: {
|
|
682
|
+
color: "var(--text-secondary)",
|
|
683
|
+
fontSize: "0.875rem"
|
|
684
|
+
},
|
|
685
|
+
children: username ?? "User"
|
|
686
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
687
|
+
type: "button",
|
|
688
|
+
onClick: () => {
|
|
689
|
+
close();
|
|
690
|
+
logout();
|
|
691
|
+
},
|
|
692
|
+
children: "Sign out"
|
|
693
|
+
})]
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function buildMenuContext(session) {
|
|
697
|
+
const profile = session.session.status === "authenticated" ? session.session.profile : void 0;
|
|
698
|
+
return {
|
|
699
|
+
roles: session.roles,
|
|
700
|
+
username: session.username,
|
|
701
|
+
session: session.session,
|
|
702
|
+
appProfile: profile?.appProfile,
|
|
703
|
+
logout: session.logout
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function resolveShellMenu(config, ctx) {
|
|
707
|
+
const declared = resolveAppMenu(config.menu, ctx);
|
|
708
|
+
const roleScoped = declared.some((item) => item.roles !== void 0 || item.items?.some((sub) => sub.roles !== void 0)) ? filterMenuByRoles(declared, ctx.roles) : declared.map(({ text, path, icon, items }) => ({
|
|
709
|
+
text,
|
|
710
|
+
path,
|
|
711
|
+
icon,
|
|
712
|
+
items
|
|
713
|
+
}));
|
|
714
|
+
return config.filterMenu ? config.filterMenu(roleScoped, ctx) : roleScoped;
|
|
715
|
+
}
|
|
716
|
+
function NubitAuthenticatedApp({ config }) {
|
|
717
|
+
const session = useSession();
|
|
718
|
+
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
719
|
+
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
720
|
+
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
721
|
+
const menuContext = (0, react.useMemo)(() => buildMenuContext(session), [session]);
|
|
722
|
+
const menuItems = (0, react.useMemo)(() => session.session.status === "authenticated" ? resolveShellMenu(config, menuContext) : [], [
|
|
723
|
+
config,
|
|
724
|
+
menuContext,
|
|
725
|
+
session.session.status
|
|
726
|
+
]);
|
|
727
|
+
const renderThemeSwitcher = config.renderThemeSwitcher ?? (() => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeSwitcher, {}));
|
|
728
|
+
const renderUserMenu = config.renderUserMenu ?? defaultUserMenu;
|
|
729
|
+
const Wrapper = config.Wrapper ?? react.default.Fragment;
|
|
730
|
+
if (session.session.status === "loading") return null;
|
|
731
|
+
const shell = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminShell, {
|
|
732
|
+
title: config.title,
|
|
733
|
+
menuItems,
|
|
734
|
+
headerActions: config.shell?.headerActions,
|
|
735
|
+
footer: config.shell?.footer,
|
|
736
|
+
renderThemeSwitcher,
|
|
737
|
+
renderUserMenu: ({ close }) => renderUserMenu({
|
|
738
|
+
...menuContext,
|
|
739
|
+
close
|
|
740
|
+
}),
|
|
741
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_router_dom.Routes, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Route, {
|
|
742
|
+
path: "/",
|
|
743
|
+
element: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Navigate, {
|
|
744
|
+
to: homePath,
|
|
745
|
+
replace: true
|
|
746
|
+
})
|
|
747
|
+
}), config.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Route, {
|
|
748
|
+
path: route.path,
|
|
749
|
+
element: route.element
|
|
750
|
+
}, route.path))] })
|
|
751
|
+
});
|
|
752
|
+
const authenticated = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.CoreProvider, {
|
|
753
|
+
http: {
|
|
754
|
+
baseUrl: apiBaseUrl,
|
|
755
|
+
refreshPath: "auth/refresh",
|
|
756
|
+
loginPath: "auth/login"
|
|
757
|
+
},
|
|
758
|
+
runtime,
|
|
759
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.CoreConfigProvider, {
|
|
760
|
+
apiBaseUrl,
|
|
761
|
+
locale: config.locale ?? "en",
|
|
762
|
+
timezone: config.timezone ?? "UTC",
|
|
763
|
+
currency: config.currency ?? "USD",
|
|
764
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_nubitio_crud.SmartCrudRolesProvider, {
|
|
765
|
+
roles: session.roles,
|
|
766
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.BrowserRouter, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wrapper, { children: config.hydra === false ? shell : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.MercureProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.SchemaProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.HydraResourceSchemaProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.HydraResourceStoreProvider, { children: shell }) }) }) }) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastHost, {
|
|
767
|
+
toasts,
|
|
768
|
+
onDismiss: dismiss
|
|
769
|
+
})]
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
});
|
|
773
|
+
if (session.session.status === "authenticated") return authenticated;
|
|
774
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.BrowserRouter, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoginPage, {
|
|
775
|
+
apiBaseUrl,
|
|
776
|
+
title: config.login?.title ?? config.title,
|
|
777
|
+
hint: config.login?.hint,
|
|
778
|
+
defaultUsername: config.login?.defaultUsername,
|
|
779
|
+
onLoggedIn: () => void session.refresh()
|
|
780
|
+
}) });
|
|
781
|
+
}
|
|
782
|
+
function createNubitApp(config) {
|
|
783
|
+
const queryClient = config.queryClient ?? new _tanstack_react_query.QueryClient({ defaultOptions: { queries: {
|
|
784
|
+
retry: 1,
|
|
785
|
+
staleTime: 3e4
|
|
786
|
+
} } });
|
|
787
|
+
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
788
|
+
function App() {
|
|
789
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tanstack_react_query.QueryClientProvider, {
|
|
790
|
+
client: queryClient,
|
|
791
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
|
|
792
|
+
apiBaseUrl,
|
|
793
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NubitAuthenticatedApp, { config })
|
|
794
|
+
}) })
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return { App };
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
586
800
|
exports.AdminHeader = AdminHeader;
|
|
587
801
|
exports.AdminShell = AdminShell;
|
|
588
802
|
exports.AdminSidebarMenu = AdminSidebarMenu;
|
|
589
803
|
exports.LoginPage = LoginPage;
|
|
590
804
|
exports.SessionProvider = SessionProvider;
|
|
591
805
|
exports.ToastHost = ToastHost;
|
|
806
|
+
exports.createNubitApp = createNubitApp;
|
|
807
|
+
exports.filterMenuByRoles = filterMenuByRoles;
|
|
808
|
+
exports.hasAnyRole = hasAnyRole;
|
|
592
809
|
exports.useAppRuntime = useAppRuntime;
|
|
810
|
+
exports.useRuntimeConfig = useRuntimeConfig;
|
|
593
811
|
exports.useScreenSize = useScreenSize;
|
|
594
812
|
exports.useScreenSizeClass = useScreenSizeClass;
|
|
595
813
|
exports.useSession = useSession;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React$1, { ReactNode } from "react";
|
|
2
2
|
import { CoreRuntime } from "@nubitio/core";
|
|
3
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
4
|
|
|
4
5
|
//#region packages/admin/AdminHeader.d.ts
|
|
5
6
|
interface AdminHeaderAction {
|
|
@@ -11,18 +12,18 @@ interface AdminHeaderAction {
|
|
|
11
12
|
onClick?: () => void;
|
|
12
13
|
renderPanel?: (props: {
|
|
13
14
|
close: () => void;
|
|
14
|
-
}) => React.ReactNode;
|
|
15
|
+
}) => React$1.ReactNode;
|
|
15
16
|
}
|
|
16
17
|
interface AdminHeaderProps {
|
|
17
18
|
title?: string;
|
|
18
19
|
menuToggleEnabled?: boolean;
|
|
19
|
-
toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
20
|
+
toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
|
|
20
21
|
className?: string;
|
|
21
22
|
actions?: AdminHeaderAction[];
|
|
22
23
|
renderUserMenu?: (props: {
|
|
23
24
|
close: () => void;
|
|
24
|
-
}) => React.ReactNode;
|
|
25
|
-
renderThemeSwitcher?: () => React.ReactNode;
|
|
25
|
+
}) => React$1.ReactNode;
|
|
26
|
+
renderThemeSwitcher?: () => React$1.ReactNode;
|
|
26
27
|
}
|
|
27
28
|
declare const AdminHeader: ({
|
|
28
29
|
title,
|
|
@@ -32,7 +33,7 @@ declare const AdminHeader: ({
|
|
|
32
33
|
actions,
|
|
33
34
|
renderUserMenu,
|
|
34
35
|
renderThemeSwitcher
|
|
35
|
-
}: AdminHeaderProps) => React.JSX.Element;
|
|
36
|
+
}: AdminHeaderProps) => React$1.JSX.Element;
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region packages/admin/AdminSidebarMenu.d.ts
|
|
38
39
|
interface AdminMenuSubItem {
|
|
@@ -48,14 +49,14 @@ interface AdminMenuItem {
|
|
|
48
49
|
interface AdminSidebarMenuSelectEvent {
|
|
49
50
|
path?: string;
|
|
50
51
|
selected: boolean;
|
|
51
|
-
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
52
|
+
event: React$1.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
52
53
|
}
|
|
53
54
|
interface AdminSidebarMenuProps {
|
|
54
55
|
items: AdminMenuItem[];
|
|
55
56
|
compactMode?: boolean;
|
|
56
57
|
selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
|
|
57
|
-
openMenu?: (e: React.PointerEvent) => void;
|
|
58
|
-
footer?: React.ReactNode;
|
|
58
|
+
openMenu?: (e: React$1.PointerEvent) => void;
|
|
59
|
+
footer?: React$1.ReactNode;
|
|
59
60
|
}
|
|
60
61
|
declare const AdminSidebarMenu: ({
|
|
61
62
|
items,
|
|
@@ -63,7 +64,7 @@ declare const AdminSidebarMenu: ({
|
|
|
63
64
|
selectedItemChanged,
|
|
64
65
|
openMenu,
|
|
65
66
|
footer
|
|
66
|
-
}: AdminSidebarMenuProps) => React.JSX.Element;
|
|
67
|
+
}: AdminSidebarMenuProps) => React$1.JSX.Element;
|
|
67
68
|
//#endregion
|
|
68
69
|
//#region packages/admin/AdminShell.d.ts
|
|
69
70
|
interface AdminShellProps {
|
|
@@ -72,10 +73,10 @@ interface AdminShellProps {
|
|
|
72
73
|
headerActions?: AdminHeaderAction[];
|
|
73
74
|
renderUserMenu?: (props: {
|
|
74
75
|
close: () => void;
|
|
75
|
-
}) => React.ReactNode;
|
|
76
|
-
renderThemeSwitcher?: () => React.ReactNode;
|
|
77
|
-
footer?: React.ReactNode;
|
|
78
|
-
children: React.ReactNode;
|
|
76
|
+
}) => React$1.ReactNode;
|
|
77
|
+
renderThemeSwitcher?: () => React$1.ReactNode;
|
|
78
|
+
footer?: React$1.ReactNode;
|
|
79
|
+
children: React$1.ReactNode;
|
|
79
80
|
}
|
|
80
81
|
declare const AdminShell: ({
|
|
81
82
|
title,
|
|
@@ -85,7 +86,7 @@ declare const AdminShell: ({
|
|
|
85
86
|
renderThemeSwitcher,
|
|
86
87
|
footer,
|
|
87
88
|
children
|
|
88
|
-
}: AdminShellProps) => React.JSX.Element;
|
|
89
|
+
}: AdminShellProps) => React$1.JSX.Element;
|
|
89
90
|
//#endregion
|
|
90
91
|
//#region packages/admin/useScreenSize.d.ts
|
|
91
92
|
declare const useScreenSize: () => {
|
|
@@ -97,9 +98,22 @@ declare const useScreenSize: () => {
|
|
|
97
98
|
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
98
99
|
//#endregion
|
|
99
100
|
//#region packages/admin/auth/SessionContext.d.ts
|
|
101
|
+
type AppProfile = 'internal' | 'saas' | 'hybrid';
|
|
102
|
+
type SessionTenant = {
|
|
103
|
+
id?: number;
|
|
104
|
+
name?: string;
|
|
105
|
+
domain?: string | null;
|
|
106
|
+
};
|
|
107
|
+
type SessionFeatureEntitlement = {
|
|
108
|
+
enabled: boolean;
|
|
109
|
+
config: Record<string, unknown>;
|
|
110
|
+
};
|
|
100
111
|
type SessionProfile = {
|
|
101
112
|
username: string;
|
|
102
|
-
roles: string[];
|
|
113
|
+
roles: string[]; /** Present when the backend runs nubitio/admin-bundle ≥ 0.1 session contract. */
|
|
114
|
+
appProfile?: AppProfile;
|
|
115
|
+
tenant?: SessionTenant;
|
|
116
|
+
features?: Record<string, SessionFeatureEntitlement>;
|
|
103
117
|
};
|
|
104
118
|
type SessionState = {
|
|
105
119
|
status: 'loading';
|
|
@@ -130,8 +144,8 @@ declare function SessionProvider({
|
|
|
130
144
|
logoutPath,
|
|
131
145
|
children
|
|
132
146
|
}: SessionProviderConfig & {
|
|
133
|
-
children: React.ReactNode;
|
|
134
|
-
}): React.JSX.Element;
|
|
147
|
+
children: React$1.ReactNode;
|
|
148
|
+
}): React$1.JSX.Element;
|
|
135
149
|
declare function useSession(): SessionContextValue;
|
|
136
150
|
//#endregion
|
|
137
151
|
//#region packages/admin/auth/LoginPage.d.ts
|
|
@@ -165,6 +179,39 @@ declare function useAppRuntime(): {
|
|
|
165
179
|
dismiss: (id: number) => void;
|
|
166
180
|
};
|
|
167
181
|
//#endregion
|
|
182
|
+
//#region packages/admin/runtime/useRuntimeConfig.d.ts
|
|
183
|
+
/** Free-form JSON from GET /api/runtime-config — each app defines the shape. */
|
|
184
|
+
type RuntimeConfig = Record<string, unknown>;
|
|
185
|
+
type RuntimeConfigState = {
|
|
186
|
+
status: 'idle';
|
|
187
|
+
} | {
|
|
188
|
+
status: 'loading';
|
|
189
|
+
} | {
|
|
190
|
+
status: 'ready';
|
|
191
|
+
config: RuntimeConfig;
|
|
192
|
+
} | {
|
|
193
|
+
status: 'error';
|
|
194
|
+
};
|
|
195
|
+
interface UseRuntimeConfigOptions {
|
|
196
|
+
/** API base URL, e.g. `/api/`. */
|
|
197
|
+
apiBaseUrl?: string;
|
|
198
|
+
/** Endpoint relative to apiBaseUrl. @default `runtime-config` */
|
|
199
|
+
path?: string;
|
|
200
|
+
/** When false, skips the fetch (e.g. endpoint not enabled). @default true */
|
|
201
|
+
enabled?: boolean;
|
|
202
|
+
}
|
|
203
|
+
declare function useRuntimeConfig({
|
|
204
|
+
apiBaseUrl,
|
|
205
|
+
path,
|
|
206
|
+
enabled
|
|
207
|
+
}?: UseRuntimeConfigOptions): {
|
|
208
|
+
state: RuntimeConfigState;
|
|
209
|
+
config: RuntimeConfig | null;
|
|
210
|
+
loading: boolean;
|
|
211
|
+
error: boolean;
|
|
212
|
+
refresh: () => Promise<void>;
|
|
213
|
+
};
|
|
214
|
+
//#endregion
|
|
168
215
|
//#region packages/admin/runtime/ToastHost.d.ts
|
|
169
216
|
interface ToastHostProps {
|
|
170
217
|
toasts: ToastItem[];
|
|
@@ -175,4 +222,69 @@ declare function ToastHost({
|
|
|
175
222
|
onDismiss
|
|
176
223
|
}: ToastHostProps): import("react").JSX.Element | null;
|
|
177
224
|
//#endregion
|
|
178
|
-
|
|
225
|
+
//#region packages/admin/app/types.d.ts
|
|
226
|
+
type NubitAppRoute = {
|
|
227
|
+
path: string;
|
|
228
|
+
element: ReactNode;
|
|
229
|
+
};
|
|
230
|
+
type NubitAppMenuSubItem = AdminMenuSubItem & {
|
|
231
|
+
/** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
|
|
232
|
+
};
|
|
233
|
+
type NubitAppMenuItem = Omit<AdminMenuItem, 'items'> & {
|
|
234
|
+
/** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
|
|
235
|
+
items?: NubitAppMenuSubItem[];
|
|
236
|
+
};
|
|
237
|
+
type NubitAppMenuContext = {
|
|
238
|
+
roles: string[];
|
|
239
|
+
username: string | null;
|
|
240
|
+
session: SessionState;
|
|
241
|
+
appProfile?: AppProfile;
|
|
242
|
+
logout: () => Promise<void>;
|
|
243
|
+
};
|
|
244
|
+
type NubitAppUserMenuContext = NubitAppMenuContext & {
|
|
245
|
+
close: () => void;
|
|
246
|
+
};
|
|
247
|
+
type CreateNubitAppConfig = {
|
|
248
|
+
title: string;
|
|
249
|
+
apiBaseUrl?: string;
|
|
250
|
+
homePath?: string;
|
|
251
|
+
locale?: string;
|
|
252
|
+
timezone?: string;
|
|
253
|
+
currency?: string;
|
|
254
|
+
menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
|
|
255
|
+
routes: NubitAppRoute[];
|
|
256
|
+
/**
|
|
257
|
+
* Final menu filter — runs after optional {@link filterMenuByRoles} when items
|
|
258
|
+
* declare `roles`. Use for app-specific rules (tenant features, runtime config).
|
|
259
|
+
*/
|
|
260
|
+
filterMenu?: (items: AdminMenuItem[], ctx: NubitAppMenuContext) => AdminMenuItem[];
|
|
261
|
+
login?: {
|
|
262
|
+
title?: string;
|
|
263
|
+
hint?: string;
|
|
264
|
+
defaultUsername?: string;
|
|
265
|
+
};
|
|
266
|
+
shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
|
|
267
|
+
renderThemeSwitcher?: () => ReactNode;
|
|
268
|
+
renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode; /** Wraps authenticated shell content (inside Hydra providers). */
|
|
269
|
+
Wrapper?: React.ComponentType<{
|
|
270
|
+
children: ReactNode;
|
|
271
|
+
}>;
|
|
272
|
+
queryClient?: QueryClient; /** Register Mercure + Hydra schema/store providers. Default true. */
|
|
273
|
+
hydra?: boolean;
|
|
274
|
+
};
|
|
275
|
+
type NubitApp = {
|
|
276
|
+
App: React.ComponentType;
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region packages/admin/app/createNubitApp.d.ts
|
|
280
|
+
declare function createNubitApp(config: CreateNubitAppConfig): NubitApp;
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region packages/admin/app/filterMenuByRoles.d.ts
|
|
283
|
+
declare function hasAnyRole(required: string | string[] | undefined, roles: string[]): boolean;
|
|
284
|
+
/**
|
|
285
|
+
* Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
|
|
286
|
+
* `roles` are filtered; empty parent groups are dropped.
|
|
287
|
+
*/
|
|
288
|
+
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
289
|
+
//#endregion
|
|
290
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React$1, { ReactNode } from "react";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
3
|
import { CoreRuntime } from "@nubitio/core";
|
|
3
4
|
|
|
4
5
|
//#region packages/admin/AdminHeader.d.ts
|
|
@@ -11,18 +12,18 @@ interface AdminHeaderAction {
|
|
|
11
12
|
onClick?: () => void;
|
|
12
13
|
renderPanel?: (props: {
|
|
13
14
|
close: () => void;
|
|
14
|
-
}) => React.ReactNode;
|
|
15
|
+
}) => React$1.ReactNode;
|
|
15
16
|
}
|
|
16
17
|
interface AdminHeaderProps {
|
|
17
18
|
title?: string;
|
|
18
19
|
menuToggleEnabled?: boolean;
|
|
19
|
-
toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
20
|
+
toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
|
|
20
21
|
className?: string;
|
|
21
22
|
actions?: AdminHeaderAction[];
|
|
22
23
|
renderUserMenu?: (props: {
|
|
23
24
|
close: () => void;
|
|
24
|
-
}) => React.ReactNode;
|
|
25
|
-
renderThemeSwitcher?: () => React.ReactNode;
|
|
25
|
+
}) => React$1.ReactNode;
|
|
26
|
+
renderThemeSwitcher?: () => React$1.ReactNode;
|
|
26
27
|
}
|
|
27
28
|
declare const AdminHeader: ({
|
|
28
29
|
title,
|
|
@@ -32,7 +33,7 @@ declare const AdminHeader: ({
|
|
|
32
33
|
actions,
|
|
33
34
|
renderUserMenu,
|
|
34
35
|
renderThemeSwitcher
|
|
35
|
-
}: AdminHeaderProps) => React.JSX.Element;
|
|
36
|
+
}: AdminHeaderProps) => React$1.JSX.Element;
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region packages/admin/AdminSidebarMenu.d.ts
|
|
38
39
|
interface AdminMenuSubItem {
|
|
@@ -48,14 +49,14 @@ interface AdminMenuItem {
|
|
|
48
49
|
interface AdminSidebarMenuSelectEvent {
|
|
49
50
|
path?: string;
|
|
50
51
|
selected: boolean;
|
|
51
|
-
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
52
|
+
event: React$1.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
52
53
|
}
|
|
53
54
|
interface AdminSidebarMenuProps {
|
|
54
55
|
items: AdminMenuItem[];
|
|
55
56
|
compactMode?: boolean;
|
|
56
57
|
selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
|
|
57
|
-
openMenu?: (e: React.PointerEvent) => void;
|
|
58
|
-
footer?: React.ReactNode;
|
|
58
|
+
openMenu?: (e: React$1.PointerEvent) => void;
|
|
59
|
+
footer?: React$1.ReactNode;
|
|
59
60
|
}
|
|
60
61
|
declare const AdminSidebarMenu: ({
|
|
61
62
|
items,
|
|
@@ -63,7 +64,7 @@ declare const AdminSidebarMenu: ({
|
|
|
63
64
|
selectedItemChanged,
|
|
64
65
|
openMenu,
|
|
65
66
|
footer
|
|
66
|
-
}: AdminSidebarMenuProps) => React.JSX.Element;
|
|
67
|
+
}: AdminSidebarMenuProps) => React$1.JSX.Element;
|
|
67
68
|
//#endregion
|
|
68
69
|
//#region packages/admin/AdminShell.d.ts
|
|
69
70
|
interface AdminShellProps {
|
|
@@ -72,10 +73,10 @@ interface AdminShellProps {
|
|
|
72
73
|
headerActions?: AdminHeaderAction[];
|
|
73
74
|
renderUserMenu?: (props: {
|
|
74
75
|
close: () => void;
|
|
75
|
-
}) => React.ReactNode;
|
|
76
|
-
renderThemeSwitcher?: () => React.ReactNode;
|
|
77
|
-
footer?: React.ReactNode;
|
|
78
|
-
children: React.ReactNode;
|
|
76
|
+
}) => React$1.ReactNode;
|
|
77
|
+
renderThemeSwitcher?: () => React$1.ReactNode;
|
|
78
|
+
footer?: React$1.ReactNode;
|
|
79
|
+
children: React$1.ReactNode;
|
|
79
80
|
}
|
|
80
81
|
declare const AdminShell: ({
|
|
81
82
|
title,
|
|
@@ -85,7 +86,7 @@ declare const AdminShell: ({
|
|
|
85
86
|
renderThemeSwitcher,
|
|
86
87
|
footer,
|
|
87
88
|
children
|
|
88
|
-
}: AdminShellProps) => React.JSX.Element;
|
|
89
|
+
}: AdminShellProps) => React$1.JSX.Element;
|
|
89
90
|
//#endregion
|
|
90
91
|
//#region packages/admin/useScreenSize.d.ts
|
|
91
92
|
declare const useScreenSize: () => {
|
|
@@ -97,9 +98,22 @@ declare const useScreenSize: () => {
|
|
|
97
98
|
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
98
99
|
//#endregion
|
|
99
100
|
//#region packages/admin/auth/SessionContext.d.ts
|
|
101
|
+
type AppProfile = 'internal' | 'saas' | 'hybrid';
|
|
102
|
+
type SessionTenant = {
|
|
103
|
+
id?: number;
|
|
104
|
+
name?: string;
|
|
105
|
+
domain?: string | null;
|
|
106
|
+
};
|
|
107
|
+
type SessionFeatureEntitlement = {
|
|
108
|
+
enabled: boolean;
|
|
109
|
+
config: Record<string, unknown>;
|
|
110
|
+
};
|
|
100
111
|
type SessionProfile = {
|
|
101
112
|
username: string;
|
|
102
|
-
roles: string[];
|
|
113
|
+
roles: string[]; /** Present when the backend runs nubitio/admin-bundle ≥ 0.1 session contract. */
|
|
114
|
+
appProfile?: AppProfile;
|
|
115
|
+
tenant?: SessionTenant;
|
|
116
|
+
features?: Record<string, SessionFeatureEntitlement>;
|
|
103
117
|
};
|
|
104
118
|
type SessionState = {
|
|
105
119
|
status: 'loading';
|
|
@@ -130,8 +144,8 @@ declare function SessionProvider({
|
|
|
130
144
|
logoutPath,
|
|
131
145
|
children
|
|
132
146
|
}: SessionProviderConfig & {
|
|
133
|
-
children: React.ReactNode;
|
|
134
|
-
}): React.JSX.Element;
|
|
147
|
+
children: React$1.ReactNode;
|
|
148
|
+
}): React$1.JSX.Element;
|
|
135
149
|
declare function useSession(): SessionContextValue;
|
|
136
150
|
//#endregion
|
|
137
151
|
//#region packages/admin/auth/LoginPage.d.ts
|
|
@@ -165,6 +179,39 @@ declare function useAppRuntime(): {
|
|
|
165
179
|
dismiss: (id: number) => void;
|
|
166
180
|
};
|
|
167
181
|
//#endregion
|
|
182
|
+
//#region packages/admin/runtime/useRuntimeConfig.d.ts
|
|
183
|
+
/** Free-form JSON from GET /api/runtime-config — each app defines the shape. */
|
|
184
|
+
type RuntimeConfig = Record<string, unknown>;
|
|
185
|
+
type RuntimeConfigState = {
|
|
186
|
+
status: 'idle';
|
|
187
|
+
} | {
|
|
188
|
+
status: 'loading';
|
|
189
|
+
} | {
|
|
190
|
+
status: 'ready';
|
|
191
|
+
config: RuntimeConfig;
|
|
192
|
+
} | {
|
|
193
|
+
status: 'error';
|
|
194
|
+
};
|
|
195
|
+
interface UseRuntimeConfigOptions {
|
|
196
|
+
/** API base URL, e.g. `/api/`. */
|
|
197
|
+
apiBaseUrl?: string;
|
|
198
|
+
/** Endpoint relative to apiBaseUrl. @default `runtime-config` */
|
|
199
|
+
path?: string;
|
|
200
|
+
/** When false, skips the fetch (e.g. endpoint not enabled). @default true */
|
|
201
|
+
enabled?: boolean;
|
|
202
|
+
}
|
|
203
|
+
declare function useRuntimeConfig({
|
|
204
|
+
apiBaseUrl,
|
|
205
|
+
path,
|
|
206
|
+
enabled
|
|
207
|
+
}?: UseRuntimeConfigOptions): {
|
|
208
|
+
state: RuntimeConfigState;
|
|
209
|
+
config: RuntimeConfig | null;
|
|
210
|
+
loading: boolean;
|
|
211
|
+
error: boolean;
|
|
212
|
+
refresh: () => Promise<void>;
|
|
213
|
+
};
|
|
214
|
+
//#endregion
|
|
168
215
|
//#region packages/admin/runtime/ToastHost.d.ts
|
|
169
216
|
interface ToastHostProps {
|
|
170
217
|
toasts: ToastItem[];
|
|
@@ -175,4 +222,69 @@ declare function ToastHost({
|
|
|
175
222
|
onDismiss
|
|
176
223
|
}: ToastHostProps): import("react").JSX.Element | null;
|
|
177
224
|
//#endregion
|
|
178
|
-
|
|
225
|
+
//#region packages/admin/app/types.d.ts
|
|
226
|
+
type NubitAppRoute = {
|
|
227
|
+
path: string;
|
|
228
|
+
element: ReactNode;
|
|
229
|
+
};
|
|
230
|
+
type NubitAppMenuSubItem = AdminMenuSubItem & {
|
|
231
|
+
/** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
|
|
232
|
+
};
|
|
233
|
+
type NubitAppMenuItem = Omit<AdminMenuItem, 'items'> & {
|
|
234
|
+
/** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
|
|
235
|
+
items?: NubitAppMenuSubItem[];
|
|
236
|
+
};
|
|
237
|
+
type NubitAppMenuContext = {
|
|
238
|
+
roles: string[];
|
|
239
|
+
username: string | null;
|
|
240
|
+
session: SessionState;
|
|
241
|
+
appProfile?: AppProfile;
|
|
242
|
+
logout: () => Promise<void>;
|
|
243
|
+
};
|
|
244
|
+
type NubitAppUserMenuContext = NubitAppMenuContext & {
|
|
245
|
+
close: () => void;
|
|
246
|
+
};
|
|
247
|
+
type CreateNubitAppConfig = {
|
|
248
|
+
title: string;
|
|
249
|
+
apiBaseUrl?: string;
|
|
250
|
+
homePath?: string;
|
|
251
|
+
locale?: string;
|
|
252
|
+
timezone?: string;
|
|
253
|
+
currency?: string;
|
|
254
|
+
menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
|
|
255
|
+
routes: NubitAppRoute[];
|
|
256
|
+
/**
|
|
257
|
+
* Final menu filter — runs after optional {@link filterMenuByRoles} when items
|
|
258
|
+
* declare `roles`. Use for app-specific rules (tenant features, runtime config).
|
|
259
|
+
*/
|
|
260
|
+
filterMenu?: (items: AdminMenuItem[], ctx: NubitAppMenuContext) => AdminMenuItem[];
|
|
261
|
+
login?: {
|
|
262
|
+
title?: string;
|
|
263
|
+
hint?: string;
|
|
264
|
+
defaultUsername?: string;
|
|
265
|
+
};
|
|
266
|
+
shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
|
|
267
|
+
renderThemeSwitcher?: () => ReactNode;
|
|
268
|
+
renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode; /** Wraps authenticated shell content (inside Hydra providers). */
|
|
269
|
+
Wrapper?: React.ComponentType<{
|
|
270
|
+
children: ReactNode;
|
|
271
|
+
}>;
|
|
272
|
+
queryClient?: QueryClient; /** Register Mercure + Hydra schema/store providers. Default true. */
|
|
273
|
+
hydra?: boolean;
|
|
274
|
+
};
|
|
275
|
+
type NubitApp = {
|
|
276
|
+
App: React.ComponentType;
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region packages/admin/app/createNubitApp.d.ts
|
|
280
|
+
declare function createNubitApp(config: CreateNubitAppConfig): NubitApp;
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region packages/admin/app/filterMenuByRoles.d.ts
|
|
283
|
+
declare function hasAnyRole(required: string | string[] | undefined, roles: string[]): boolean;
|
|
284
|
+
/**
|
|
285
|
+
* Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
|
|
286
|
+
* `roles` are filtered; empty parent groups are dropped.
|
|
287
|
+
*/
|
|
288
|
+
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
289
|
+
//#endregion
|
|
290
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
|
-
import { Badge, Button, Card, IconButton, TextField, useFloatingPanel } from "@nubitio/ui";
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
|
3
|
+
import { Badge, Button, Card, IconButton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
import { CoreConfigProvider, CoreProvider, MercureProvider } from "@nubitio/core";
|
|
7
|
+
import { SmartCrudRolesProvider } from "@nubitio/crud";
|
|
8
|
+
import { HydraResourceSchemaProvider, HydraResourceStoreProvider, SchemaProvider } from "@nubitio/hydra";
|
|
5
9
|
//#region packages/admin/AdminHeader.tsx
|
|
6
10
|
function ActionPopover({ action }) {
|
|
7
11
|
const { open, toggle, setOpen, containerRef } = useFloatingPanel();
|
|
@@ -361,14 +365,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
361
365
|
//#endregion
|
|
362
366
|
//#region packages/admin/auth/SessionContext.tsx
|
|
363
367
|
const SessionContext = createContext(null);
|
|
364
|
-
function joinApiPath$
|
|
368
|
+
function joinApiPath$2(apiBaseUrl, path) {
|
|
365
369
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
366
370
|
}
|
|
367
371
|
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
368
372
|
const [session, setSession] = useState({ status: "loading" });
|
|
369
373
|
const refresh = useCallback(async () => {
|
|
370
374
|
try {
|
|
371
|
-
const response = await fetch(joinApiPath$
|
|
375
|
+
const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
|
|
372
376
|
if (!response.ok) {
|
|
373
377
|
setSession({ status: "anonymous" });
|
|
374
378
|
return;
|
|
@@ -385,7 +389,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
385
389
|
refresh();
|
|
386
390
|
}, [refresh]);
|
|
387
391
|
const logout = useCallback(async () => {
|
|
388
|
-
await fetch(joinApiPath$
|
|
392
|
+
await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
|
|
389
393
|
method: "POST",
|
|
390
394
|
credentials: "include"
|
|
391
395
|
});
|
|
@@ -414,7 +418,7 @@ function useSession() {
|
|
|
414
418
|
}
|
|
415
419
|
//#endregion
|
|
416
420
|
//#region packages/admin/auth/LoginPage.tsx
|
|
417
|
-
function joinApiPath(apiBaseUrl, path) {
|
|
421
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
418
422
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
419
423
|
}
|
|
420
424
|
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
@@ -427,7 +431,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
427
431
|
setBusy(true);
|
|
428
432
|
setError(null);
|
|
429
433
|
try {
|
|
430
|
-
const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
|
|
434
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
|
|
431
435
|
method: "POST",
|
|
432
436
|
headers: { "Content-Type": "application/json" },
|
|
433
437
|
credentials: "include",
|
|
@@ -534,6 +538,48 @@ function useAppRuntime() {
|
|
|
534
538
|
};
|
|
535
539
|
}
|
|
536
540
|
//#endregion
|
|
541
|
+
//#region packages/admin/runtime/useRuntimeConfig.ts
|
|
542
|
+
function joinApiPath(apiBaseUrl, path) {
|
|
543
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
544
|
+
}
|
|
545
|
+
function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabled = true } = {}) {
|
|
546
|
+
const [state, setState] = useState({ status: "idle" });
|
|
547
|
+
const refresh = useCallback(async () => {
|
|
548
|
+
if (!enabled) {
|
|
549
|
+
setState({ status: "idle" });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
setState({ status: "loading" });
|
|
553
|
+
try {
|
|
554
|
+
const response = await fetch(joinApiPath(apiBaseUrl, path), { credentials: "include" });
|
|
555
|
+
if (!response.ok) {
|
|
556
|
+
setState({ status: "error" });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
setState({
|
|
560
|
+
status: "ready",
|
|
561
|
+
config: await response.json()
|
|
562
|
+
});
|
|
563
|
+
} catch {
|
|
564
|
+
setState({ status: "error" });
|
|
565
|
+
}
|
|
566
|
+
}, [
|
|
567
|
+
apiBaseUrl,
|
|
568
|
+
enabled,
|
|
569
|
+
path
|
|
570
|
+
]);
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
refresh();
|
|
573
|
+
}, [refresh]);
|
|
574
|
+
return {
|
|
575
|
+
state,
|
|
576
|
+
config: state.status === "ready" ? state.config : null,
|
|
577
|
+
loading: state.status === "loading",
|
|
578
|
+
error: state.status === "error",
|
|
579
|
+
refresh
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
537
583
|
//#region packages/admin/runtime/ToastHost.tsx
|
|
538
584
|
const TYPE_CLASS = {
|
|
539
585
|
success: "nb-toast--success",
|
|
@@ -559,4 +605,172 @@ function ToastHost({ toasts, onDismiss }) {
|
|
|
559
605
|
});
|
|
560
606
|
}
|
|
561
607
|
//#endregion
|
|
562
|
-
|
|
608
|
+
//#region packages/admin/app/filterMenuByRoles.ts
|
|
609
|
+
function hasAnyRole(required, roles) {
|
|
610
|
+
if (required === void 0) return true;
|
|
611
|
+
return (Array.isArray(required) ? required : [required]).some((role) => roles.includes(role));
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
|
|
615
|
+
* `roles` are filtered; empty parent groups are dropped.
|
|
616
|
+
*/
|
|
617
|
+
function filterMenuByRoles(items, roles) {
|
|
618
|
+
const filtered = [];
|
|
619
|
+
for (const item of items) {
|
|
620
|
+
if (!hasAnyRole(item.roles, roles)) continue;
|
|
621
|
+
if (!item.items) {
|
|
622
|
+
filtered.push({
|
|
623
|
+
text: item.text,
|
|
624
|
+
path: item.path,
|
|
625
|
+
icon: item.icon
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const subItems = item.items.filter((subItem) => hasAnyRole(subItem.roles, roles)).map(({ text, path }) => ({
|
|
630
|
+
text,
|
|
631
|
+
path
|
|
632
|
+
}));
|
|
633
|
+
if (subItems.length === 0) continue;
|
|
634
|
+
filtered.push({
|
|
635
|
+
text: item.text,
|
|
636
|
+
icon: item.icon,
|
|
637
|
+
path: item.path,
|
|
638
|
+
items: subItems
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return filtered;
|
|
642
|
+
}
|
|
643
|
+
function resolveAppMenu(menu, ctx) {
|
|
644
|
+
return typeof menu === "function" ? menu(ctx) : menu;
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region packages/admin/app/createNubitApp.tsx
|
|
648
|
+
function defaultUserMenu({ username, close, logout }) {
|
|
649
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
650
|
+
style: {
|
|
651
|
+
display: "flex",
|
|
652
|
+
flexDirection: "column",
|
|
653
|
+
gap: 8,
|
|
654
|
+
minWidth: 180
|
|
655
|
+
},
|
|
656
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
657
|
+
style: {
|
|
658
|
+
color: "var(--text-secondary)",
|
|
659
|
+
fontSize: "0.875rem"
|
|
660
|
+
},
|
|
661
|
+
children: username ?? "User"
|
|
662
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
663
|
+
type: "button",
|
|
664
|
+
onClick: () => {
|
|
665
|
+
close();
|
|
666
|
+
logout();
|
|
667
|
+
},
|
|
668
|
+
children: "Sign out"
|
|
669
|
+
})]
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function buildMenuContext(session) {
|
|
673
|
+
const profile = session.session.status === "authenticated" ? session.session.profile : void 0;
|
|
674
|
+
return {
|
|
675
|
+
roles: session.roles,
|
|
676
|
+
username: session.username,
|
|
677
|
+
session: session.session,
|
|
678
|
+
appProfile: profile?.appProfile,
|
|
679
|
+
logout: session.logout
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function resolveShellMenu(config, ctx) {
|
|
683
|
+
const declared = resolveAppMenu(config.menu, ctx);
|
|
684
|
+
const roleScoped = declared.some((item) => item.roles !== void 0 || item.items?.some((sub) => sub.roles !== void 0)) ? filterMenuByRoles(declared, ctx.roles) : declared.map(({ text, path, icon, items }) => ({
|
|
685
|
+
text,
|
|
686
|
+
path,
|
|
687
|
+
icon,
|
|
688
|
+
items
|
|
689
|
+
}));
|
|
690
|
+
return config.filterMenu ? config.filterMenu(roleScoped, ctx) : roleScoped;
|
|
691
|
+
}
|
|
692
|
+
function NubitAuthenticatedApp({ config }) {
|
|
693
|
+
const session = useSession();
|
|
694
|
+
const { runtime, toasts, dismiss } = useAppRuntime();
|
|
695
|
+
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
696
|
+
const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
|
|
697
|
+
const menuContext = useMemo(() => buildMenuContext(session), [session]);
|
|
698
|
+
const menuItems = useMemo(() => session.session.status === "authenticated" ? resolveShellMenu(config, menuContext) : [], [
|
|
699
|
+
config,
|
|
700
|
+
menuContext,
|
|
701
|
+
session.session.status
|
|
702
|
+
]);
|
|
703
|
+
const renderThemeSwitcher = config.renderThemeSwitcher ?? (() => /* @__PURE__ */ jsx(ThemeSwitcher, {}));
|
|
704
|
+
const renderUserMenu = config.renderUserMenu ?? defaultUserMenu;
|
|
705
|
+
const Wrapper = config.Wrapper ?? React.Fragment;
|
|
706
|
+
if (session.session.status === "loading") return null;
|
|
707
|
+
const shell = /* @__PURE__ */ jsx(AdminShell, {
|
|
708
|
+
title: config.title,
|
|
709
|
+
menuItems,
|
|
710
|
+
headerActions: config.shell?.headerActions,
|
|
711
|
+
footer: config.shell?.footer,
|
|
712
|
+
renderThemeSwitcher,
|
|
713
|
+
renderUserMenu: ({ close }) => renderUserMenu({
|
|
714
|
+
...menuContext,
|
|
715
|
+
close
|
|
716
|
+
}),
|
|
717
|
+
children: /* @__PURE__ */ jsxs(Routes, { children: [/* @__PURE__ */ jsx(Route, {
|
|
718
|
+
path: "/",
|
|
719
|
+
element: /* @__PURE__ */ jsx(Navigate, {
|
|
720
|
+
to: homePath,
|
|
721
|
+
replace: true
|
|
722
|
+
})
|
|
723
|
+
}), config.routes.map((route) => /* @__PURE__ */ jsx(Route, {
|
|
724
|
+
path: route.path,
|
|
725
|
+
element: route.element
|
|
726
|
+
}, route.path))] })
|
|
727
|
+
});
|
|
728
|
+
const authenticated = /* @__PURE__ */ jsx(CoreProvider, {
|
|
729
|
+
http: {
|
|
730
|
+
baseUrl: apiBaseUrl,
|
|
731
|
+
refreshPath: "auth/refresh",
|
|
732
|
+
loginPath: "auth/login"
|
|
733
|
+
},
|
|
734
|
+
runtime,
|
|
735
|
+
children: /* @__PURE__ */ jsx(CoreConfigProvider, {
|
|
736
|
+
apiBaseUrl,
|
|
737
|
+
locale: config.locale ?? "en",
|
|
738
|
+
timezone: config.timezone ?? "UTC",
|
|
739
|
+
currency: config.currency ?? "USD",
|
|
740
|
+
children: /* @__PURE__ */ jsxs(SmartCrudRolesProvider, {
|
|
741
|
+
roles: session.roles,
|
|
742
|
+
children: [/* @__PURE__ */ jsx(BrowserRouter, { children: /* @__PURE__ */ jsx(Wrapper, { children: config.hydra === false ? shell : /* @__PURE__ */ jsx(MercureProvider, { children: /* @__PURE__ */ jsx(SchemaProvider, { children: /* @__PURE__ */ jsx(HydraResourceSchemaProvider, { children: /* @__PURE__ */ jsx(HydraResourceStoreProvider, { children: shell }) }) }) }) }) }), /* @__PURE__ */ jsx(ToastHost, {
|
|
743
|
+
toasts,
|
|
744
|
+
onDismiss: dismiss
|
|
745
|
+
})]
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
});
|
|
749
|
+
if (session.session.status === "authenticated") return authenticated;
|
|
750
|
+
return /* @__PURE__ */ jsx(BrowserRouter, { children: /* @__PURE__ */ jsx(LoginPage, {
|
|
751
|
+
apiBaseUrl,
|
|
752
|
+
title: config.login?.title ?? config.title,
|
|
753
|
+
hint: config.login?.hint,
|
|
754
|
+
defaultUsername: config.login?.defaultUsername,
|
|
755
|
+
onLoggedIn: () => void session.refresh()
|
|
756
|
+
}) });
|
|
757
|
+
}
|
|
758
|
+
function createNubitApp(config) {
|
|
759
|
+
const queryClient = config.queryClient ?? new QueryClient({ defaultOptions: { queries: {
|
|
760
|
+
retry: 1,
|
|
761
|
+
staleTime: 3e4
|
|
762
|
+
} } });
|
|
763
|
+
const apiBaseUrl = config.apiBaseUrl ?? "/api/";
|
|
764
|
+
function App() {
|
|
765
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
766
|
+
client: queryClient,
|
|
767
|
+
children: /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(SessionProvider, {
|
|
768
|
+
apiBaseUrl,
|
|
769
|
+
children: /* @__PURE__ */ jsx(NubitAuthenticatedApp, { config })
|
|
770
|
+
}) })
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return { App };
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
776
|
+
export { AdminHeader, AdminShell, AdminSidebarMenu, LoginPage, SessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubitio/admin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Admin shell layout components: responsive sidebar, header, and screen size utilities for Nubit apps.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,10 +49,13 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
+
"@tanstack/react-query": "^5.0.0",
|
|
52
53
|
"react": "^19.0.0",
|
|
53
54
|
"react-dom": "^19.0.0",
|
|
54
55
|
"react-router-dom": "^6.0.0",
|
|
55
|
-
"@nubitio/core": "^0.5.
|
|
56
|
-
"@nubitio/
|
|
56
|
+
"@nubitio/core": "^0.5.20",
|
|
57
|
+
"@nubitio/hydra": "^0.5.20",
|
|
58
|
+
"@nubitio/ui": "^0.5.20",
|
|
59
|
+
"@nubitio/crud": "^0.5.20"
|
|
57
60
|
}
|
|
58
61
|
}
|