xladmin 0.4.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;
@@ -876,9 +889,13 @@ var FieldEditor = memo(function FieldEditor2({
876
889
  size: "small",
877
890
  required: field.required,
878
891
  sx: buildRequiredLabelSx(),
879
- placeholder: (_a = field.placeholder) != null ? _a : void 0,
880
- helperText: (_b = field.help_text) != null ? _b : void 0,
881
- slotProps: { htmlInput: buildHtmlInputProps(field) }
892
+ helperText: (_a = field.help_text) != null ? _a : void 0,
893
+ slotProps: {
894
+ htmlInput: {
895
+ ...buildHtmlInputProps(field),
896
+ placeholder: (_b = field.placeholder) != null ? _b : void 0
897
+ }
898
+ }
882
899
  }
883
900
  }
884
901
  }
@@ -910,9 +927,13 @@ var FieldEditor = memo(function FieldEditor2({
910
927
  size: "small",
911
928
  required: field.required,
912
929
  sx: buildRequiredLabelSx(),
913
- placeholder: (_c = field.placeholder) != null ? _c : void 0,
914
- helperText: (_d = field.help_text) != null ? _d : void 0,
915
- slotProps: { htmlInput: buildHtmlInputProps(field) }
930
+ helperText: (_c = field.help_text) != null ? _c : void 0,
931
+ slotProps: {
932
+ htmlInput: {
933
+ ...buildHtmlInputProps(field),
934
+ placeholder: (_d = field.placeholder) != null ? _d : void 0
935
+ }
936
+ }
916
937
  }
917
938
  }
918
939
  }
@@ -1044,8 +1065,8 @@ function renderChoiceEditor({
1044
1065
  listbox: { sx: { maxHeight: 240 } }
1045
1066
  },
1046
1067
  renderInput: (params) => (() => {
1047
- var _a2, _b;
1048
- const { InputProps, inputProps, ...textFieldParams } = params;
1068
+ var _a2, _b, _c;
1069
+ const { slotProps, ...textFieldParams } = params;
1049
1070
  return /* @__PURE__ */ jsx3(
1050
1071
  TextField,
1051
1072
  {
@@ -1056,15 +1077,16 @@ function renderChoiceEditor({
1056
1077
  placeholder: (_a2 = field.placeholder) != null ? _a2 : searchPlaceholder,
1057
1078
  helperText: (_b = field.help_text) != null ? _b : void 0,
1058
1079
  slotProps: {
1080
+ inputLabel: slotProps.inputLabel,
1059
1081
  input: {
1060
- ...InputProps,
1082
+ ...slotProps.input,
1061
1083
  endAdornment: /* @__PURE__ */ jsxs(Fragment, { children: [
1062
1084
  isLoadingChoices ? /* @__PURE__ */ jsx3(CircularProgress, { color: "inherit", size: 16 }) : null,
1063
- InputProps.endAdornment
1085
+ (_c = slotProps.input) == null ? void 0 : _c.endAdornment
1064
1086
  ] })
1065
1087
  },
1066
1088
  htmlInput: {
1067
- ...inputProps,
1089
+ ...slotProps.htmlInput,
1068
1090
  autoComplete: "new-password",
1069
1091
  name: `admin-choice-${field.name}`
1070
1092
  }
@@ -1104,8 +1126,8 @@ function renderChoiceEditor({
1104
1126
  listbox: { sx: { maxHeight: 240 } }
1105
1127
  },
1106
1128
  renderInput: (params) => (() => {
1107
- var _a2, _b;
1108
- const { InputProps, inputProps, ...textFieldParams } = params;
1129
+ var _a2, _b, _c;
1130
+ const { slotProps, ...textFieldParams } = params;
1109
1131
  return /* @__PURE__ */ jsx3(
1110
1132
  TextField,
1111
1133
  {
@@ -1116,15 +1138,16 @@ function renderChoiceEditor({
1116
1138
  placeholder: (_a2 = field.placeholder) != null ? _a2 : searchPlaceholder,
1117
1139
  helperText: (_b = field.help_text) != null ? _b : void 0,
1118
1140
  slotProps: {
1141
+ inputLabel: slotProps.inputLabel,
1119
1142
  input: {
1120
- ...InputProps,
1143
+ ...slotProps.input,
1121
1144
  endAdornment: /* @__PURE__ */ jsxs(Fragment, { children: [
1122
1145
  isLoadingChoices ? /* @__PURE__ */ jsx3(CircularProgress, { color: "inherit", size: 16 }) : null,
1123
- InputProps.endAdornment
1146
+ (_c = slotProps.input) == null ? void 0 : _c.endAdornment
1124
1147
  ] })
1125
1148
  },
1126
1149
  htmlInput: {
1127
- ...inputProps,
1150
+ ...slotProps.htmlInput,
1128
1151
  autoComplete: "new-password",
1129
1152
  name: `admin-choice-${field.name}`
1130
1153
  }
@@ -1294,8 +1317,8 @@ function stringifyJsonValue(value) {
1294
1317
 
1295
1318
  // src/components/layout/AdminMessageContext.tsx
1296
1319
  import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
1297
- import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
1298
- import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
1320
+ import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined";
1321
+ import ErrorOutlinedIcon from "@mui/icons-material/ErrorOutlined";
1299
1322
  import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
1300
1323
  import { Box, Paper, Portal, Stack, Typography } from "@mui/material";
1301
1324
  import { alpha, useTheme } from "@mui/material/styles";
@@ -1435,7 +1458,7 @@ function AdminMessageBubble({
1435
1458
  }
1436
1459
  }
1437
1460
  },
1438
- children: /* @__PURE__ */ jsxs2(Stack, { direction: "row", spacing: 1.125, alignItems: "center", children: [
1461
+ children: /* @__PURE__ */ jsxs2(Stack, { direction: "row", spacing: 1.125, sx: { alignItems: "center" }, children: [
1439
1462
  /* @__PURE__ */ jsx4(
1440
1463
  Box,
1441
1464
  {
@@ -1477,7 +1500,7 @@ function AdminMessageBubble({
1477
1500
  function getBubbleAppearance(theme, kind) {
1478
1501
  if (kind === "success") {
1479
1502
  return {
1480
- icon: CheckCircleOutlineIcon,
1503
+ icon: CheckCircleOutlinedIcon,
1481
1504
  backgroundColor: alpha(theme.palette.success.main, 0.6),
1482
1505
  iconColor: theme.palette.common.white,
1483
1506
  iconBackgroundColor: alpha(theme.palette.common.white, 0.14),
@@ -1486,7 +1509,7 @@ function getBubbleAppearance(theme, kind) {
1486
1509
  }
1487
1510
  if (kind === "error") {
1488
1511
  return {
1489
- icon: ErrorOutlineIcon,
1512
+ icon: ErrorOutlinedIcon,
1490
1513
  backgroundColor: alpha(theme.palette.error.main, 0.6),
1491
1514
  iconColor: theme.palette.common.white,
1492
1515
  iconBackgroundColor: alpha(theme.palette.common.white, 0.14),
@@ -2150,84 +2173,64 @@ function MainHeader({ title, subtitle, details, error, beforeTitle, beforeSubtit
2150
2173
  },
2151
2174
  children: [
2152
2175
  /* @__PURE__ */ jsxs7(Stack5, { spacing: 0.5, children: [
2153
- /* @__PURE__ */ jsxs7(
2154
- Stack5,
2155
- {
2156
- direction: "row",
2157
- spacing: 1.5,
2158
- alignItems: "center",
2159
- justifyContent: "space-between",
2160
- sx: { minWidth: 0 },
2161
- children: [
2162
- /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 1, alignItems: "center", sx: { minWidth: 0, flex: 1 }, children: [
2163
- isMobile ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: /* @__PURE__ */ jsx13(
2164
- IconButton,
2165
- {
2166
- "aria-label": t("menu"),
2167
- onClick: openMobileSidebar,
2168
- size: "small",
2169
- sx: { ml: -0.5 },
2170
- children: /* @__PURE__ */ jsx13(MenuIcon, { fontSize: "small" })
2171
- }
2172
- ) }) : null,
2173
- beforeTitle ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforeTitle }) : null,
2174
- /* @__PURE__ */ jsx13(
2175
- Typography3,
2176
- {
2177
- variant: "h5",
2178
- sx: {
2179
- fontWeight: 600,
2180
- lineHeight: "2.125rem",
2181
- minWidth: 0
2182
- },
2183
- children: title
2184
- }
2185
- )
2186
- ] }),
2187
- actions ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: actions }) : null
2188
- ]
2189
- }
2190
- ),
2191
- subtitle && !details ? /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 0.5, alignItems: "center", sx: { minWidth: 0 }, children: [
2176
+ /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 1.5, sx: { minWidth: 0, alignItems: "center", justifyContent: "space-between" }, children: [
2177
+ /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 1, sx: { minWidth: 0, flex: 1, alignItems: "center" }, children: [
2178
+ isMobile ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: /* @__PURE__ */ jsx13(
2179
+ IconButton,
2180
+ {
2181
+ "aria-label": t("menu"),
2182
+ onClick: openMobileSidebar,
2183
+ size: "small",
2184
+ sx: { ml: -0.5 },
2185
+ children: /* @__PURE__ */ jsx13(MenuIcon, { fontSize: "small" })
2186
+ }
2187
+ ) }) : null,
2188
+ beforeTitle ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforeTitle }) : null,
2189
+ /* @__PURE__ */ jsx13(
2190
+ Typography3,
2191
+ {
2192
+ variant: "h5",
2193
+ sx: {
2194
+ fontWeight: 600,
2195
+ lineHeight: "2.125rem",
2196
+ minWidth: 0
2197
+ },
2198
+ children: title
2199
+ }
2200
+ )
2201
+ ] }),
2202
+ actions ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: actions }) : null
2203
+ ] }),
2204
+ subtitle && !details ? /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 0.5, sx: { minWidth: 0, alignItems: "center" }, children: [
2192
2205
  beforeSubtitle ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforeSubtitle }) : null,
2193
2206
  /* @__PURE__ */ jsx13(Typography3, { color: "text.secondary", sx: { lineHeight: "1rem", minWidth: 0 }, children: subtitle })
2194
2207
  ] }) : null,
2195
2208
  details ? /* @__PURE__ */ jsxs7(Stack5, { spacing: 0.5, children: [
2196
- /* @__PURE__ */ jsxs7(
2197
- Stack5,
2198
- {
2199
- direction: "row",
2200
- spacing: 0.5,
2201
- alignItems: "center",
2202
- justifyContent: "space-between",
2203
- sx: { minWidth: 0 },
2204
- children: [
2205
- /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 0.5, alignItems: "center", sx: { minWidth: 0, flex: 1 }, children: [
2206
- beforeSubtitle ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforeSubtitle }) : null,
2207
- /* @__PURE__ */ jsx13(Typography3, { color: "text.secondary", sx: { lineHeight: "1rem", minWidth: 0 }, children: subtitle })
2208
- ] }),
2209
- /* @__PURE__ */ jsx13(
2210
- IconButton,
2209
+ /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 0.5, sx: { minWidth: 0, alignItems: "center", justifyContent: "space-between" }, children: [
2210
+ /* @__PURE__ */ jsxs7(Stack5, { direction: "row", spacing: 0.5, sx: { minWidth: 0, flex: 1, alignItems: "center" }, children: [
2211
+ beforeSubtitle ? /* @__PURE__ */ jsx13(Box5, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforeSubtitle }) : null,
2212
+ /* @__PURE__ */ jsx13(Typography3, { color: "text.secondary", sx: { lineHeight: "1rem", minWidth: 0 }, children: subtitle })
2213
+ ] }),
2214
+ /* @__PURE__ */ jsx13(
2215
+ IconButton,
2216
+ {
2217
+ "aria-label": isDetailsOpen ? t("hide_details") : t("show_details"),
2218
+ onClick: () => setIsDetailsOpen((current) => !current),
2219
+ size: "small",
2220
+ sx: { mr: -0.5 },
2221
+ children: /* @__PURE__ */ jsx13(
2222
+ ExpandMoreIcon3,
2211
2223
  {
2212
- "aria-label": isDetailsOpen ? t("hide_details") : t("show_details"),
2213
- onClick: () => setIsDetailsOpen((current) => !current),
2214
- size: "small",
2215
- sx: { mr: -0.5 },
2216
- children: /* @__PURE__ */ jsx13(
2217
- ExpandMoreIcon3,
2218
- {
2219
- fontSize: "small",
2220
- sx: {
2221
- transform: isDetailsOpen ? "rotate(180deg)" : "rotate(0deg)",
2222
- transition: "transform 0.2s ease"
2223
- }
2224
- }
2225
- )
2224
+ fontSize: "small",
2225
+ sx: {
2226
+ transform: isDetailsOpen ? "rotate(180deg)" : "rotate(0deg)",
2227
+ transition: "transform 0.2s ease"
2228
+ }
2226
2229
  }
2227
2230
  )
2228
- ]
2229
- }
2230
- ),
2231
+ }
2232
+ )
2233
+ ] }),
2231
2234
  /* @__PURE__ */ jsx13(Collapse, { in: isDetailsOpen, children: /* @__PURE__ */ jsx13(Box5, { sx: { pt: 0.5 }, children: details }) })
2232
2235
  ] }) : null
2233
2236
  ] }),
@@ -2638,7 +2641,7 @@ function DeletePreviewDialog({
2638
2641
  children: [
2639
2642
  /* @__PURE__ */ jsx16(DialogTitle3, { children: title }),
2640
2643
  /* @__PURE__ */ jsxs10(DialogContent3, { dividers: true, children: [
2641
- isLoading ? /* @__PURE__ */ jsxs10(Stack7, { spacing: 1.5, alignItems: "center", justifyContent: "center", sx: { minHeight: 180 }, children: [
2644
+ isLoading ? /* @__PURE__ */ jsxs10(Stack7, { spacing: 1.5, sx: { minHeight: 180, alignItems: "center", justifyContent: "center" }, children: [
2642
2645
  /* @__PURE__ */ jsx16(CircularProgress2, { size: 28, thickness: 4.2, color: "inherit" }),
2643
2646
  /* @__PURE__ */ jsx16(Typography4, { color: "text.secondary", children: t("delete_preview_loading") })
2644
2647
  ] }) : null,
@@ -2796,7 +2799,7 @@ var ListFiltersSidebar = memo3(function ListFiltersSidebar2({
2796
2799
  overflow: "auto"
2797
2800
  },
2798
2801
  children: /* @__PURE__ */ jsxs11(Stack8, { spacing: 1.5, children: [
2799
- /* @__PURE__ */ jsxs11(Stack8, { direction: "row", spacing: 1, alignItems: "center", justifyContent: "space-between", children: [
2802
+ /* @__PURE__ */ jsxs11(Stack8, { direction: "row", spacing: 1, sx: { alignItems: "center", justifyContent: "space-between" }, children: [
2800
2803
  /* @__PURE__ */ jsxs11(Stack8, { spacing: 0.25, children: [
2801
2804
  /* @__PURE__ */ jsx17(Typography5, { sx: { fontSize: 15, fontWeight: 700 }, children: t("filters") }),
2802
2805
  activeFiltersCount > 0 ? /* @__PURE__ */ jsx17(Typography5, { color: "text.secondary", sx: { fontSize: 12 }, children: t("active_filters", { count: activeFiltersCount }) }) : null
@@ -2949,8 +2952,8 @@ function ListFilterField({ client, slug, filter, value, onChange, debounceMs })
2949
2952
  },
2950
2953
  onInputChange: (_, nextInputValue) => setSearchValue(nextInputValue),
2951
2954
  renderInput: (params) => (() => {
2952
- var _a2;
2953
- const { InputProps, inputProps, ...textFieldParams } = params;
2955
+ var _a2, _b2;
2956
+ const { slotProps, ...textFieldParams } = params;
2954
2957
  return /* @__PURE__ */ jsx17(
2955
2958
  TextField2,
2956
2959
  {
@@ -2958,14 +2961,15 @@ function ListFilterField({ client, slug, filter, value, onChange, debounceMs })
2958
2961
  label: filter.label,
2959
2962
  placeholder: (_a2 = filter.placeholder) != null ? _a2 : t("search"),
2960
2963
  slotProps: {
2964
+ inputLabel: slotProps.inputLabel,
2961
2965
  input: {
2962
- ...InputProps,
2966
+ ...slotProps.input,
2963
2967
  endAdornment: /* @__PURE__ */ jsxs11(Fragment2, { children: [
2964
2968
  isLoadingChoices ? /* @__PURE__ */ jsx17(CircularProgress3, { color: "inherit", size: 16 }) : null,
2965
- InputProps.endAdornment
2969
+ (_b2 = slotProps.input) == null ? void 0 : _b2.endAdornment
2966
2970
  ] })
2967
2971
  },
2968
- htmlInput: inputProps
2972
+ htmlInput: slotProps.htmlInput
2969
2973
  }
2970
2974
  }
2971
2975
  );
@@ -3866,7 +3870,7 @@ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3866
3870
  function ModelPageSkeleton() {
3867
3871
  return /* @__PURE__ */ jsxs13(Stack9, { spacing: 1.5, sx: { height: "100%", minHeight: 0 }, children: [
3868
3872
  /* @__PURE__ */ jsx20(MainHeaderSkeleton, { titleWidth: 240, subtitleWidth: "38%" }),
3869
- /* @__PURE__ */ jsx20(Paper6, { sx: { p: 1.5, borderRadius: "10px", flexShrink: 0 }, children: /* @__PURE__ */ jsxs13(Stack9, { direction: { xs: "column", lg: "row" }, spacing: 1.5, alignItems: { lg: "center" }, children: [
3873
+ /* @__PURE__ */ jsx20(Paper6, { sx: { p: 1.5, borderRadius: "10px", flexShrink: 0 }, children: /* @__PURE__ */ jsxs13(Stack9, { direction: { xs: "column", lg: "row" }, spacing: 1.5, sx: { alignItems: { lg: "center" } }, children: [
3870
3874
  /* @__PURE__ */ jsx20(Skeleton3, { variant: "rounded", width: 360, height: 40 }),
3871
3875
  /* @__PURE__ */ jsx20(Skeleton3, { variant: "rounded", width: 128, height: 40 }),
3872
3876
  /* @__PURE__ */ jsx20(Box10, { sx: { flex: 1 } }),
@@ -3979,14 +3983,13 @@ function ModelPage({ client, basePath, slug, router, renderBeforePagination }) {
3979
3983
  borderRadius: "10px",
3980
3984
  flexShrink: 0
3981
3985
  },
3982
- children: /* @__PURE__ */ jsxs14(Stack10, { direction: { xs: "column", lg: "row" }, spacing: 1.5, alignItems: { lg: "center" }, children: [
3986
+ children: /* @__PURE__ */ jsxs14(Stack10, { direction: { xs: "column", lg: "row" }, spacing: 1.5, sx: { alignItems: { lg: "center" } }, children: [
3983
3987
  /* @__PURE__ */ jsxs14(
3984
3988
  Stack10,
3985
3989
  {
3986
3990
  direction: { xs: "column", xl: "row" },
3987
3991
  spacing: 1.5,
3988
- alignItems: { xl: "center" },
3989
- sx: { flex: 1, minWidth: 0 },
3992
+ sx: { flex: 1, minWidth: 0, alignItems: { xl: "center" } },
3990
3993
  children: [
3991
3994
  /* @__PURE__ */ jsx21(
3992
3995
  SearchField,
@@ -3997,7 +4000,7 @@ function ModelPage({ client, basePath, slug, router, renderBeforePagination }) {
3997
4000
  placeholder: t("search")
3998
4001
  }
3999
4002
  ),
4000
- controller.hasSelection ? /* @__PURE__ */ jsxs14(Stack10, { direction: "row", spacing: 1, alignItems: "center", sx: { minWidth: 0, flexWrap: "wrap" }, children: [
4003
+ controller.hasSelection ? /* @__PURE__ */ jsxs14(Stack10, { direction: "row", spacing: 1, sx: { minWidth: 0, flexWrap: "wrap", alignItems: "center" }, children: [
4001
4004
  /* @__PURE__ */ jsx21(
4002
4005
  Button5,
4003
4006
  {
@@ -4025,7 +4028,7 @@ function ModelPage({ client, basePath, slug, router, renderBeforePagination }) {
4025
4028
  ]
4026
4029
  }
4027
4030
  ),
4028
- /* @__PURE__ */ jsxs14(Stack10, { direction: "row", spacing: 1, alignItems: "center", sx: { marginLeft: "auto" }, children: [
4031
+ /* @__PURE__ */ jsxs14(Stack10, { direction: "row", spacing: 1, sx: { marginLeft: "auto", alignItems: "center" }, children: [
4029
4032
  beforePagination ? /* @__PURE__ */ jsx21(Box11, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforePagination }) : null,
4030
4033
  isMobile && controller.hasListFilters ? /* @__PURE__ */ jsx21(Tooltip, { title: t("filters"), children: /* @__PURE__ */ jsx21(
4031
4034
  IconButton3,
@@ -5073,7 +5076,7 @@ function ObjectPage({ client, slug, id, router }) {
5073
5076
  }
5074
5077
 
5075
5078
  // src/components/Shell.tsx
5076
- 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";
5077
5080
  import { Box as Box17, CssBaseline, Drawer, GlobalStyles, Stack as Stack14, useMediaQuery as useMediaQuery4 } from "@mui/material";
5078
5081
  import { ThemeProvider } from "@mui/material/styles";
5079
5082
 
@@ -5244,7 +5247,8 @@ function Main({ children }) {
5244
5247
 
5245
5248
  // src/components/layout/Sidebar.tsx
5246
5249
  import { memo as memo8, useEffect as useEffect13, useRef as useRef7 } from "react";
5247
- 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";
5248
5252
  import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
5249
5253
  function getOffsetTopWithinContainer(element, container) {
5250
5254
  let offsetTop = element.offsetTop;
@@ -5255,7 +5259,14 @@ function getOffsetTopWithinContainer(element, container) {
5255
5259
  }
5256
5260
  return offsetTop;
5257
5261
  }
5258
- 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
+ }) {
5259
5270
  var _a;
5260
5271
  const t = useAdminTranslation();
5261
5272
  const { pathname } = useAdminLocation();
@@ -5338,59 +5349,123 @@ var Sidebar = memo8(function Sidebar2({ models, blocks, basePath }) {
5338
5349
  }
5339
5350
  };
5340
5351
  }, [activeModelSlug, blocks]);
5341
- 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(
5342
5414
  Box16,
5343
5415
  {
5344
- ref: scrollContainerRef,
5345
5416
  sx: {
5346
- height: "100%",
5347
- overflowY: "scroll",
5348
- overflowX: "hidden",
5349
- scrollbarGutter: "stable",
5350
- direction: "rtl",
5351
- ml: 0,
5352
- 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
5353
5428
  },
5354
- children: /* @__PURE__ */ jsxs19(Box16, { sx: { direction: "ltr", pl: 1 }, children: [
5429
+ children: [
5355
5430
  /* @__PURE__ */ jsx27(
5356
- NavLink,
5431
+ Typography8,
5357
5432
  {
5358
- href: basePath,
5359
- style: { textDecoration: "none", display: "block" },
5360
- onClick: () => startPendingNavigation(basePath, "overview"),
5361
- children: /* @__PURE__ */ jsx27(
5362
- ListItemButton3,
5363
- {
5364
- selected: isOverviewActive,
5365
- sx: {
5366
- mb: 2,
5367
- borderRadius: "8px",
5368
- backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.18)" : "rgba(255, 255, 255, 0.035)",
5369
- boxShadow: isOverviewActive ? "inset 0 0 0 1px rgba(255, 255, 255, 0.08)" : "none",
5370
- "&:hover": {
5371
- backgroundColor: isOverviewActive ? "rgba(255, 255, 255, 0.2)" : "rgba(255, 255, 255, 0.055)"
5372
- }
5373
- },
5374
- children: /* @__PURE__ */ jsx27(Typography8, { variant: "subtitle1", sx: { fontWeight: 700 }, children: t("overview") })
5375
- }
5376
- )
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
5377
5444
  }
5378
5445
  ),
5379
- /* @__PURE__ */ jsx27(
5380
- ModelsBlocks,
5446
+ onLogout ? /* @__PURE__ */ jsx27(Tooltip2, { title: "Logout", children: /* @__PURE__ */ jsx27("span", { children: /* @__PURE__ */ jsx27(
5447
+ IconButton5,
5381
5448
  {
5382
- models,
5383
- blocks,
5384
- basePath,
5385
- variant: "sidebar",
5386
- activeModelSlug,
5387
- 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 } })
5388
5463
  }
5389
- )
5390
- ] })
5464
+ ) }) }) : null
5465
+ ]
5391
5466
  }
5392
- ) });
5393
- });
5467
+ );
5468
+ }
5394
5469
 
5395
5470
  // src/components/Shell.tsx
5396
5471
  import { jsx as jsx28, jsxs as jsxs20 } from "react/jsx-runtime";
@@ -5398,8 +5473,19 @@ function normalizeAdminPath(path) {
5398
5473
  const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
5399
5474
  return normalizedPath.replace(/^\/(ru|en)(?=\/|$)/, "") || "/";
5400
5475
  }
5401
- function Shell({ client, models, blocks, basePath, locale, children, theme, router }) {
5402
- 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
+ }) {
5403
5489
  const activeTheme = theme != null ? theme : defaultAdminTheme;
5404
5490
  const resolvedRouter = useAdminRouter(router);
5405
5491
  const location = useAdminLocation(resolvedRouter);
@@ -5408,6 +5494,12 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5408
5494
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState13(false);
5409
5495
  const [pendingPath, setPendingPath] = useState13(null);
5410
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
+ );
5411
5503
  useEffect14(() => {
5412
5504
  if (isDesktopSidebar) {
5413
5505
  setIsMobileSidebarOpen(false);
@@ -5416,6 +5508,40 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5416
5508
  useEffect14(() => {
5417
5509
  setIsMobileSidebarOpen(false);
5418
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]);
5419
5545
  const shellContextValue = useMemo11(() => ({
5420
5546
  isMobile: !isDesktopSidebar,
5421
5547
  openMobileSidebar: () => setIsMobileSidebarOpen(true),
@@ -5510,7 +5636,17 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5510
5636
  pl: 0,
5511
5637
  pr: 1
5512
5638
  },
5513
- 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
+ )
5514
5650
  }
5515
5651
  ),
5516
5652
  /* @__PURE__ */ jsx28(
@@ -5554,11 +5690,31 @@ function Shell({ client, models, blocks, basePath, locale, children, theme, rout
5554
5690
  }
5555
5691
  }
5556
5692
  },
5557
- 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
+ ) })
5558
5704
  }
5559
5705
  )
5560
5706
  ] }) }) }) }) }) });
5561
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
+ }
5562
5718
  export {
5563
5719
  AdminLocaleProvider,
5564
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.4.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",
@@ -38,9 +38,9 @@
38
38
  "peerDependencies": {
39
39
  "@emotion/react": ">=11.14.0 <12.0.0",
40
40
  "@emotion/styled": ">=11.14.0 <12.0.0",
41
- "@mui/icons-material": ">=7.0.0 <8.0.0",
42
- "@mui/material": ">=7.0.0 <8.0.0",
43
- "@mui/x-date-pickers": ">=8.0.0 <9.0.0",
41
+ "@mui/icons-material": ">=9.0.0 <10.0.0",
42
+ "@mui/material": ">=9.0.0 <10.0.0",
43
+ "@mui/x-date-pickers": ">=9.0.0 <10.0.0",
44
44
  "axios": ">=1.0.0 <2.0.0",
45
45
  "dayjs": ">=1.11.0 <2.0.0",
46
46
  "react": ">=19.0.0 <20.0.0",