xladmin 0.5.0 → 0.6.0

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
@@ -34,6 +34,7 @@ You also need one router adapter:
34
34
  - `createAxiosAdminClient(...)`
35
35
  - `createFetchAdminClient(...)`
36
36
  - `createBrowserAdminRouter(...)`
37
+ - `AdminCurrentUser`
37
38
  - admin types, i18n helpers, and default theme
38
39
 
39
40
  ## Minimal Example
@@ -67,6 +68,48 @@ export function AdminApp() {
67
68
 
68
69
  For framework routing, use one of the adapter packages instead of the default browser router.
69
70
 
71
+ ## Current User And Logout
72
+
73
+ `Shell` shows a compact current-user panel pinned to the bottom of the sidebar when a user is available.
74
+ By default it calls:
75
+
76
+ - `client.getCurrentUser()` -> `GET /xladmin/me/`
77
+ - `client.logout()` -> `POST /xladmin/logout/`
78
+
79
+ After logout, `Shell` redirects to `/login`.
80
+
81
+ ```tsx
82
+ <Shell
83
+ client={client}
84
+ models={models}
85
+ blocks={blocks}
86
+ basePath="/admin"
87
+ loginPath="/login"
88
+ >
89
+ {content}
90
+ </Shell>
91
+ ```
92
+
93
+ If your application already has the user or a custom logout flow, pass them explicitly:
94
+
95
+ ```tsx
96
+ <Shell
97
+ client={client}
98
+ models={models}
99
+ blocks={blocks}
100
+ basePath="/admin"
101
+ currentUser={{login: auth.user.email}}
102
+ onLogout={async () => {
103
+ await auth.logout();
104
+ }}
105
+ loginPath="/sign-in"
106
+ >
107
+ {content}
108
+ </Shell>
109
+ ```
110
+
111
+ Set `currentUser={null}` to hide the sidebar user panel.
112
+
70
113
  ## Development
71
114
 
72
115
  ```bash
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AdminChoicesResponse, AdminDeletePreviewResponse, AdminDetailResponse, AdminListResponse, AdminModelMeta, AdminModelsResponse, AdminObjectActionResponse } from './types';
1
+ import type { AdminChoicesResponse, AdminDeletePreviewResponse, AdminDetailResponse, AdminCurrentUser, AdminListResponse, AdminModelMeta, AdminModelsResponse, AdminObjectActionResponse } from './types';
2
2
  export type AdminRequestOptions = {
3
3
  signal?: AbortSignal;
4
4
  };
@@ -11,6 +11,8 @@ export type AdminSelectionOptions = {
11
11
  selectionScope?: AdminSelectionScope;
12
12
  };
13
13
  export type AdminClient = {
14
+ getCurrentUser: () => Promise<AdminCurrentUser>;
15
+ logout: () => Promise<void>;
14
16
  getModels: () => Promise<AdminModelsResponse>;
15
17
  getModel: (slug: string) => Promise<AdminModelMeta>;
16
18
  getItems: (slug: string, params?: {
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import { type Theme } from '@mui/material/styles';
3
3
  import type { AdminClient } from '../client';
4
4
  import type { AdminRouter } from '../router';
5
- import type { AdminModelMeta, AdminModelsBlockMeta } from '../types';
5
+ import type { AdminCurrentUser, AdminModelMeta, AdminModelsBlockMeta } from '../types';
6
6
  type AdminShellProps = {
7
7
  client: AdminClient;
8
8
  models: AdminModelMeta[];
@@ -12,7 +12,10 @@ type AdminShellProps = {
12
12
  children: ReactNode;
13
13
  theme?: Theme;
14
14
  router?: AdminRouter;
15
+ currentUser?: AdminCurrentUser | string | null;
16
+ loginPath?: string | null;
17
+ onLogout?: () => void | Promise<void>;
15
18
  };
16
19
  export type ShellProps = AdminShellProps;
17
- export declare function Shell({ client, models, blocks, basePath, locale, children, theme, router }: ShellProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function Shell({ client, models, blocks, basePath, locale, children, theme, router, currentUser, loginPath, onLogout, }: ShellProps): import("react/jsx-runtime").JSX.Element;
18
21
  export {};
@@ -1,8 +1,11 @@
1
- import type { AdminModelMeta, AdminModelsBlockMeta } from '@xladmin-core/types';
1
+ import type { AdminCurrentUser, AdminModelMeta, AdminModelsBlockMeta } from '@xladmin-core/types';
2
2
  type SidebarProps = {
3
3
  models: AdminModelMeta[];
4
4
  blocks: AdminModelsBlockMeta[];
5
5
  basePath: string;
6
+ currentUser?: AdminCurrentUser | null;
7
+ isLoggingOut?: boolean;
8
+ onLogout?: () => void;
6
9
  };
7
10
  export declare const Sidebar: import("react").NamedExoticComponent<SidebarProps>;
8
11
  export {};
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  // src/client.ts
2
2
  function createAdminClient(transport) {
3
3
  return {
4
+ async getCurrentUser() {
5
+ return await transportGet(transport, "/xladmin/me/");
6
+ },
7
+ async logout() {
8
+ await transportPost(transport, "/xladmin/logout/", {});
9
+ },
4
10
  async getModels() {
5
11
  return await transportGet(transport, "/xladmin/models/");
6
12
  },
@@ -148,7 +154,14 @@ function isWrappedTransportResponse(response) {
148
154
  }
149
155
  async function requestJson(fetchImpl, config, method, url, body, params, signal) {
150
156
  const response = await requestRaw(fetchImpl, config, method, url, body, params, signal);
151
- return await response.json();
157
+ if (response.status === 204) {
158
+ return void 0;
159
+ }
160
+ const text = await response.text();
161
+ if (!text.trim()) {
162
+ return void 0;
163
+ }
164
+ return JSON.parse(text);
152
165
  }
153
166
  async function requestRaw(fetchImpl, config, method, url, body, params, signal) {
154
167
  var _a;
@@ -5063,7 +5076,7 @@ function ObjectPage({ client, slug, id, router }) {
5063
5076
  }
5064
5077
 
5065
5078
  // src/components/Shell.tsx
5066
- import { useEffect as useEffect14, useMemo as useMemo11, useState as useState13 } from "react";
5079
+ import { useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo11, useState as useState13 } from "react";
5067
5080
  import { Box as Box17, CssBaseline, Drawer, GlobalStyles, Stack as Stack14, useMediaQuery as useMediaQuery4 } from "@mui/material";
5068
5081
  import { ThemeProvider } from "@mui/material/styles";
5069
5082
 
@@ -5234,7 +5247,8 @@ function Main({ children }) {
5234
5247
 
5235
5248
  // src/components/layout/Sidebar.tsx
5236
5249
  import { memo as memo8, useEffect as useEffect13, useRef as useRef7 } from "react";
5237
- import { Box as Box16, ListItemButton as ListItemButton3, Typography as Typography8 } from "@mui/material";
5250
+ import LogoutIcon from "@mui/icons-material/Logout";
5251
+ import { Box as Box16, IconButton as IconButton5, ListItemButton as ListItemButton3, Tooltip as Tooltip2, Typography as Typography8 } from "@mui/material";
5238
5252
  import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
5239
5253
  function getOffsetTopWithinContainer(element, container) {
5240
5254
  let offsetTop = element.offsetTop;
@@ -5245,7 +5259,14 @@ function getOffsetTopWithinContainer(element, container) {
5245
5259
  }
5246
5260
  return offsetTop;
5247
5261
  }
5248
- var Sidebar = memo8(function Sidebar2({ models, blocks, basePath }) {
5262
+ var Sidebar = memo8(function Sidebar2({
5263
+ models,
5264
+ blocks,
5265
+ basePath,
5266
+ currentUser,
5267
+ isLoggingOut = false,
5268
+ onLogout
5269
+ }) {
5249
5270
  var _a;
5250
5271
  const t = useAdminTranslation();
5251
5272
  const { pathname } = useAdminLocation();
@@ -5328,59 +5349,123 @@ var Sidebar = memo8(function Sidebar2({ models, blocks, basePath }) {
5328
5349
  }
5329
5350
  };
5330
5351
  }, [activeModelSlug, blocks]);
5331
- return /* @__PURE__ */ jsx27(Box16, { sx: { height: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx27(
5352
+ return /* @__PURE__ */ jsxs19(Box16, { sx: { height: "100%", overflow: "hidden", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
5353
+ /* @__PURE__ */ jsx27(
5354
+ Box16,
5355
+ {
5356
+ ref: scrollContainerRef,
5357
+ sx: {
5358
+ flex: 1,
5359
+ minHeight: 0,
5360
+ overflowY: "scroll",
5361
+ overflowX: "hidden",
5362
+ scrollbarGutter: "stable",
5363
+ direction: "rtl",
5364
+ ml: 0,
5365
+ pl: 0
5366
+ },
5367
+ children: /* @__PURE__ */ jsxs19(Box16, { sx: { direction: "ltr", pl: 1 }, children: [
5368
+ /* @__PURE__ */ jsx27(
5369
+ NavLink,
5370
+ {
5371
+ href: basePath,
5372
+ style: { textDecoration: "none", display: "block" },
5373
+ onClick: () => startPendingNavigation(basePath, "overview"),
5374
+ children: /* @__PURE__ */ jsx27(
5375
+ ListItemButton3,
5376
+ {
5377
+ selected: isOverviewActive,
5378
+ sx: {
5379
+ mb: 2,
5380
+ borderRadius: "8px",
5381
+ backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.18)" : "rgba(255, 255, 255, 0.035)",
5382
+ boxShadow: isOverviewActive ? "inset 0 0 0 1px rgba(255, 255, 255, 0.08)" : "none",
5383
+ "&:hover": {
5384
+ backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.2)" : "rgba(255, 255, 255, 0.055)"
5385
+ }
5386
+ },
5387
+ children: /* @__PURE__ */ jsx27(Typography8, { variant: "subtitle1", sx: { fontWeight: 700 }, children: t("overview") })
5388
+ }
5389
+ )
5390
+ }
5391
+ ),
5392
+ /* @__PURE__ */ jsx27(
5393
+ ModelsBlocks,
5394
+ {
5395
+ models,
5396
+ blocks,
5397
+ basePath,
5398
+ variant: "sidebar",
5399
+ activeModelSlug,
5400
+ onModelNavigate: (href) => startPendingNavigation(href, "model")
5401
+ }
5402
+ )
5403
+ ] })
5404
+ }
5405
+ ),
5406
+ /* @__PURE__ */ jsx27(SidebarCurrentUser, { currentUser, isLoggingOut, onLogout })
5407
+ ] });
5408
+ });
5409
+ function SidebarCurrentUser({ currentUser, isLoggingOut, onLogout }) {
5410
+ if (!currentUser) {
5411
+ return null;
5412
+ }
5413
+ return /* @__PURE__ */ jsxs19(
5332
5414
  Box16,
5333
5415
  {
5334
- ref: scrollContainerRef,
5335
5416
  sx: {
5336
- height: "100%",
5337
- overflowY: "scroll",
5338
- overflowX: "hidden",
5339
- scrollbarGutter: "stable",
5340
- direction: "rtl",
5341
- ml: 0,
5342
- pl: 0
5417
+ flexShrink: 0,
5418
+ ml: 1,
5419
+ mt: 1.25,
5420
+ borderRadius: "8px",
5421
+ backgroundColor: "rgba(255, 255, 255, 0.02)",
5422
+ boxShadow: "inset 0 0 0 1px rgba(255, 255, 255, 0.03)",
5423
+ display: "flex",
5424
+ alignItems: "center",
5425
+ minHeight: 44,
5426
+ px: 1,
5427
+ gap: 1
5343
5428
  },
5344
- children: /* @__PURE__ */ jsxs19(Box16, { sx: { direction: "ltr", pl: 1 }, children: [
5429
+ children: [
5345
5430
  /* @__PURE__ */ jsx27(
5346
- NavLink,
5431
+ Typography8,
5347
5432
  {
5348
- href: basePath,
5349
- style: { textDecoration: "none", display: "block" },
5350
- onClick: () => startPendingNavigation(basePath, "overview"),
5351
- children: /* @__PURE__ */ jsx27(
5352
- ListItemButton3,
5353
- {
5354
- selected: isOverviewActive,
5355
- sx: {
5356
- mb: 2,
5357
- borderRadius: "8px",
5358
- backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.18)" : "rgba(255, 255, 255, 0.035)",
5359
- boxShadow: isOverviewActive ? "inset 0 0 0 1px rgba(255, 255, 255, 0.08)" : "none",
5360
- "&:hover": {
5361
- backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.2)" : "rgba(255, 255, 255, 0.055)"
5362
- }
5363
- },
5364
- children: /* @__PURE__ */ jsx27(Typography8, { variant: "subtitle1", sx: { fontWeight: 700 }, children: t("overview") })
5365
- }
5366
- )
5433
+ title: currentUser.login,
5434
+ sx: {
5435
+ flex: 1,
5436
+ minWidth: 0,
5437
+ overflow: "hidden",
5438
+ textOverflow: "ellipsis",
5439
+ whiteSpace: "nowrap",
5440
+ fontSize: 13,
5441
+ fontWeight: 650
5442
+ },
5443
+ children: currentUser.login
5367
5444
  }
5368
5445
  ),
5369
- /* @__PURE__ */ jsx27(
5370
- ModelsBlocks,
5446
+ onLogout ? /* @__PURE__ */ jsx27(Tooltip2, { title: "Logout", children: /* @__PURE__ */ jsx27("span", { children: /* @__PURE__ */ jsx27(
5447
+ IconButton5,
5371
5448
  {
5372
- models,
5373
- blocks,
5374
- basePath,
5375
- variant: "sidebar",
5376
- activeModelSlug,
5377
- onModelNavigate: (href) => startPendingNavigation(href, "model")
5449
+ "aria-label": "Logout",
5450
+ size: "small",
5451
+ disabled: isLoggingOut,
5452
+ onClick: onLogout,
5453
+ sx: {
5454
+ width: 32,
5455
+ height: 32,
5456
+ color: "text.secondary",
5457
+ "&:hover": {
5458
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
5459
+ color: "text.primary"
5460
+ }
5461
+ },
5462
+ children: /* @__PURE__ */ jsx27(LogoutIcon, { sx: { fontSize: 18 } })
5378
5463
  }
5379
- )
5380
- ] })
5464
+ ) }) }) : null
5465
+ ]
5381
5466
  }
5382
- ) });
5383
- });
5467
+ );
5468
+ }
5384
5469
 
5385
5470
  // src/components/Shell.tsx
5386
5471
  import { jsx as jsx28, jsxs as jsxs20 } from "react/jsx-runtime";
@@ -5388,8 +5473,19 @@ function normalizeAdminPath(path) {
5388
5473
  const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
5389
5474
  return normalizedPath.replace(/^\/(ru|en)(?=\/|$)/, "") || "/";
5390
5475
  }
5391
- function Shell({ client, models, blocks, basePath, locale, children, theme, router }) {
5392
- void client;
5476
+ function Shell({
5477
+ client,
5478
+ models,
5479
+ blocks,
5480
+ basePath,
5481
+ locale,
5482
+ children,
5483
+ theme,
5484
+ router,
5485
+ currentUser,
5486
+ loginPath = "/login",
5487
+ onLogout
5488
+ }) {
5393
5489
  const activeTheme = theme != null ? theme : defaultAdminTheme;
5394
5490
  const resolvedRouter = useAdminRouter(router);
5395
5491
  const location = useAdminLocation(resolvedRouter);
@@ -5398,6 +5494,12 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5398
5494
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState13(false);
5399
5495
  const [pendingPath, setPendingPath] = useState13(null);
5400
5496
  const [pendingView, setPendingView] = useState13(null);
5497
+ const [loadedCurrentUser, setLoadedCurrentUser] = useState13(null);
5498
+ const [isLoggingOut, setIsLoggingOut] = useState13(false);
5499
+ const sidebarCurrentUser = useMemo11(
5500
+ () => normalizeCurrentUser(currentUser === void 0 ? loadedCurrentUser : currentUser),
5501
+ [currentUser, loadedCurrentUser]
5502
+ );
5401
5503
  useEffect14(() => {
5402
5504
  if (isDesktopSidebar) {
5403
5505
  setIsMobileSidebarOpen(false);
@@ -5406,6 +5508,40 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5406
5508
  useEffect14(() => {
5407
5509
  setIsMobileSidebarOpen(false);
5408
5510
  }, [pathname]);
5511
+ useEffect14(() => {
5512
+ if (currentUser !== void 0) {
5513
+ return;
5514
+ }
5515
+ let isMounted = true;
5516
+ client.getCurrentUser().then((response) => {
5517
+ if (!isMounted) return;
5518
+ setLoadedCurrentUser(response);
5519
+ }).catch(() => {
5520
+ if (!isMounted) return;
5521
+ setLoadedCurrentUser(null);
5522
+ });
5523
+ return () => {
5524
+ isMounted = false;
5525
+ };
5526
+ }, [client, currentUser]);
5527
+ const handleLogout = useCallback8(async () => {
5528
+ if (isLoggingOut) {
5529
+ return;
5530
+ }
5531
+ setIsLoggingOut(true);
5532
+ try {
5533
+ if (onLogout) {
5534
+ await onLogout();
5535
+ } else {
5536
+ await client.logout();
5537
+ }
5538
+ if (loginPath) {
5539
+ resolvedRouter.replace(loginPath);
5540
+ }
5541
+ } finally {
5542
+ setIsLoggingOut(false);
5543
+ }
5544
+ }, [client, isLoggingOut, loginPath, onLogout, resolvedRouter]);
5409
5545
  const shellContextValue = useMemo11(() => ({
5410
5546
  isMobile: !isDesktopSidebar,
5411
5547
  openMobileSidebar: () => setIsMobileSidebarOpen(true),
@@ -5500,7 +5636,17 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5500
5636
  pl: 0,
5501
5637
  pr: 1
5502
5638
  },
5503
- children: /* @__PURE__ */ jsx28(Sidebar, { models, blocks, basePath })
5639
+ children: /* @__PURE__ */ jsx28(
5640
+ Sidebar,
5641
+ {
5642
+ models,
5643
+ blocks,
5644
+ basePath,
5645
+ currentUser: sidebarCurrentUser,
5646
+ isLoggingOut,
5647
+ onLogout: handleLogout
5648
+ }
5649
+ )
5504
5650
  }
5505
5651
  ),
5506
5652
  /* @__PURE__ */ jsx28(
@@ -5544,11 +5690,31 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5544
5690
  }
5545
5691
  }
5546
5692
  },
5547
- children: /* @__PURE__ */ jsx28(Box17, { sx: { height: "100%", minHeight: 0, pr: 1.5 }, children: /* @__PURE__ */ jsx28(Sidebar, { models, blocks, basePath }) })
5693
+ children: /* @__PURE__ */ jsx28(Box17, { sx: { height: "100%", minHeight: 0, pr: 1.5 }, children: /* @__PURE__ */ jsx28(
5694
+ Sidebar,
5695
+ {
5696
+ models,
5697
+ blocks,
5698
+ basePath,
5699
+ currentUser: sidebarCurrentUser,
5700
+ isLoggingOut,
5701
+ onLogout: handleLogout
5702
+ }
5703
+ ) })
5548
5704
  }
5549
5705
  )
5550
5706
  ] }) }) }) }) }) });
5551
5707
  }
5708
+ function normalizeCurrentUser(user) {
5709
+ if (typeof user === "string") {
5710
+ const login = user.trim();
5711
+ return login ? { login } : null;
5712
+ }
5713
+ if (!user || !user.login.trim()) {
5714
+ return null;
5715
+ }
5716
+ return user;
5717
+ }
5552
5718
  export {
5553
5719
  AdminLocaleProvider,
5554
5720
  AdminRouterProvider,
@@ -135,6 +135,12 @@ export type AdminModelsResponse = {
135
135
  items: AdminModelMeta[];
136
136
  blocks: AdminModelsBlockMeta[];
137
137
  };
138
+ export type AdminCurrentUser = {
139
+ id?: string | number | null;
140
+ login: string;
141
+ email?: string | null;
142
+ name?: string | null;
143
+ };
138
144
  export type AdminChoicesResponse = {
139
145
  items: Array<{
140
146
  id: string | number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xladmin",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-agnostic React + MUI admin frontend for xladmin.",
5
5
  "license": "MIT",
6
6
  "type": "module",