@nocobase/portal-template-default 1.0.2 → 1.0.4

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.
Files changed (62) hide show
  1. package/README.MD +10 -5
  2. package/package.json +1 -1
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/registry/nocobase-route-surfaces/demo/lazy-route.tsx +11 -0
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/src/App.tsx +65 -35
  36. package/src/app/extension.ts +1 -2
  37. package/src/app/extensions.tsx +0 -14
  38. package/src/components/app-shell/breadcrumb.tsx +67 -2
  39. package/src/components/app-shell/sidebar.tsx +2 -2
  40. package/src/components/resources/views/list-view.tsx +3 -2
  41. package/src/components/ui/sheet.tsx +22 -21
  42. package/src/lib/nocobase/client.ts +6 -1
  43. package/src/locales/en-US.ts +81 -0
  44. package/src/locales/zh-CN.ts +77 -0
  45. package/src/pages/users/create.tsx +76 -53
  46. package/src/pages/users/edit.tsx +87 -56
  47. package/src/pages/users/index.ts +3 -0
  48. package/src/pages/users/layout.tsx +15 -0
  49. package/src/pages/users/list.tsx +19 -21
  50. package/src/pages/users/role-badges.tsx +43 -0
  51. package/src/pages/users/role-detail.tsx +272 -0
  52. package/src/pages/users/role-utils.ts +6 -0
  53. package/src/pages/users/routes.ts +24 -0
  54. package/src/pages/users/show.tsx +195 -104
  55. package/src/pages/users/types.ts +13 -0
  56. package/src/providers/i18n/index.ts +3 -0
  57. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  58. package/src/providers/system-settings/context.ts +26 -0
  59. package/src/providers/system-settings/index.ts +2 -0
  60. package/src/providers/system-settings/provider.tsx +94 -0
  61. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  62. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -0,0 +1,272 @@
1
+ import { useGetLocale, useOne, useTranslate } from "@refinedev/core";
2
+ import { useParams } from "react-router";
3
+
4
+ import { LoadingState } from "@/components/app-shell/loading-state";
5
+ import { Alert, AlertDescription } from "@/components/ui/alert";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Separator } from "@/components/ui/separator";
8
+ import { useAIPageElementHandle } from "@/extensions/nocobase-ai";
9
+ import { RouteDrawer } from "@/extensions/nocobase-route-surfaces";
10
+ import type { Role } from "@/lib/nocobase/acl";
11
+ import { resolveTranslatableText } from "@/lib/i18n";
12
+ import { resolveRoleLabel } from "./role-utils";
13
+ import { getUserShowPath, userRoutes } from "./routes";
14
+ import type { RoleRecord } from "./types";
15
+
16
+ export function RoleDetailRoute({
17
+ returnTo,
18
+ }: {
19
+ returnTo: "list" | "show";
20
+ }) {
21
+ const { id, roleName } = useParams<{
22
+ id?: string;
23
+ roleName: string;
24
+ }>();
25
+
26
+ if (!roleName) return null;
27
+
28
+ const closeTarget =
29
+ returnTo === "show" && id
30
+ ? getUserShowPath(id)
31
+ : userRoutes.list;
32
+
33
+ return (
34
+ <RoleDetailDrawer
35
+ role={{ name: roleName }}
36
+ closeTo={closeTarget}
37
+ />
38
+ );
39
+ }
40
+
41
+ export function RoleDetailDrawer({
42
+ role,
43
+ closeTo,
44
+ }: {
45
+ role?: Role;
46
+ closeTo: string;
47
+ }) {
48
+ const translate = useTranslate();
49
+ const getLocale = useGetLocale();
50
+ const locale = getLocale();
51
+ const { result, query } = useOne<RoleRecord>({
52
+ resource: "roles",
53
+ id: role?.name,
54
+ errorNotification: false,
55
+ queryOptions: {
56
+ enabled: Boolean(role?.name),
57
+ retry: false,
58
+ },
59
+ });
60
+
61
+ const record = result ?? role ?? { name: "" };
62
+ const title = resolveRoleLabel(record);
63
+ const description = resolveTranslatableText(result?.description, {
64
+ ns: "starter",
65
+ });
66
+ const actions = result?.strategy?.actions ?? [];
67
+ const detailContext = useAIPageElementHandle({
68
+ id: `role-detail-${record.name || "current"}`,
69
+ title: `${translate(
70
+ "roles.ai.detail",
71
+ { ns: "app" },
72
+ "Role details"
73
+ )}: ${title}`,
74
+ kind: "detail",
75
+ getContext: () => ({
76
+ resource: "roles",
77
+ record: {
78
+ name: record.name,
79
+ title,
80
+ description,
81
+ default: result?.default,
82
+ hidden: result?.hidden,
83
+ allowConfigure: result?.allowConfigure,
84
+ actions,
85
+ createdAt: result?.createdAt,
86
+ updatedAt: result?.updatedAt,
87
+ },
88
+ }),
89
+ });
90
+
91
+ const formatBoolean = (value?: boolean) => {
92
+ if (typeof value !== "boolean") return "-";
93
+ return value
94
+ ? translate("common.yes", { ns: "app" }, "Yes")
95
+ : translate("common.no", { ns: "app" }, "No");
96
+ };
97
+ const formatDate = (value?: string) =>
98
+ value
99
+ ? new Intl.DateTimeFormat(locale, {
100
+ dateStyle: "medium",
101
+ timeStyle: "short",
102
+ }).format(new Date(value))
103
+ : "-";
104
+
105
+ if (!role) return null;
106
+
107
+ return (
108
+ <RouteDrawer
109
+ title={title}
110
+ description={translate(
111
+ "roles.drawer.description",
112
+ { ns: "app" },
113
+ "Review the role associated with this user."
114
+ )}
115
+ closeLabel={translate("buttons.close", "Close")}
116
+ closeTo={closeTo}
117
+ >
118
+ <div
119
+ ref={detailContext.ref}
120
+ className="min-h-0 flex-1 overflow-y-auto px-5 py-5"
121
+ >
122
+ {query.isLoading ? (
123
+ <LoadingState className="min-h-48" />
124
+ ) : (
125
+ <div className="space-y-5">
126
+ {query.isError ? (
127
+ <Alert>
128
+ <AlertDescription>
129
+ {translate(
130
+ "roles.detail.unavailable",
131
+ { ns: "app" },
132
+ "Additional role details are unavailable. Showing the relation data included with the user."
133
+ )}
134
+ </AlertDescription>
135
+ </Alert>
136
+ ) : null}
137
+
138
+ <RoleDetailSection
139
+ title={translate(
140
+ "roles.detail.identity",
141
+ { ns: "app" },
142
+ "Role identity"
143
+ )}
144
+ items={[
145
+ [
146
+ translate("roles.fields.name", { ns: "app" }, "Name"),
147
+ role.name,
148
+ ],
149
+ [
150
+ translate("roles.fields.title", { ns: "app" }, "Title"),
151
+ title,
152
+ ],
153
+ [
154
+ translate(
155
+ "roles.fields.description",
156
+ { ns: "app" },
157
+ "Description"
158
+ ),
159
+ description || "-",
160
+ ],
161
+ ]}
162
+ />
163
+
164
+ <Separator />
165
+
166
+ <RoleDetailSection
167
+ title={translate(
168
+ "roles.detail.settings",
169
+ { ns: "app" },
170
+ "Settings"
171
+ )}
172
+ items={[
173
+ [
174
+ translate(
175
+ "roles.fields.default",
176
+ { ns: "app" },
177
+ "Default role"
178
+ ),
179
+ formatBoolean(result?.default),
180
+ ],
181
+ [
182
+ translate("roles.fields.hidden", { ns: "app" }, "Hidden"),
183
+ formatBoolean(result?.hidden),
184
+ ],
185
+ [
186
+ translate(
187
+ "roles.fields.allowConfigure",
188
+ { ns: "app" },
189
+ "Can configure"
190
+ ),
191
+ formatBoolean(result?.allowConfigure),
192
+ ],
193
+ ]}
194
+ />
195
+
196
+ {actions.length ? (
197
+ <>
198
+ <Separator />
199
+ <section className="space-y-3">
200
+ <h3 className="text-sm font-medium">
201
+ {translate(
202
+ "roles.detail.actions",
203
+ { ns: "app" },
204
+ "Allowed actions"
205
+ )}
206
+ </h3>
207
+ <div className="flex flex-wrap gap-1">
208
+ {actions.map((action) => (
209
+ <Badge key={action} variant="outline">
210
+ {action}
211
+ </Badge>
212
+ ))}
213
+ </div>
214
+ </section>
215
+ </>
216
+ ) : null}
217
+
218
+ <Separator />
219
+
220
+ <RoleDetailSection
221
+ title={translate(
222
+ "roles.detail.timestamps",
223
+ { ns: "app" },
224
+ "Timestamps"
225
+ )}
226
+ items={[
227
+ [
228
+ translate(
229
+ "roles.fields.createdAt",
230
+ { ns: "app" },
231
+ "Created at"
232
+ ),
233
+ formatDate(result?.createdAt),
234
+ ],
235
+ [
236
+ translate(
237
+ "roles.fields.updatedAt",
238
+ { ns: "app" },
239
+ "Updated at"
240
+ ),
241
+ formatDate(result?.updatedAt),
242
+ ],
243
+ ]}
244
+ />
245
+ </div>
246
+ )}
247
+ </div>
248
+ </RouteDrawer>
249
+ );
250
+ }
251
+
252
+ function RoleDetailSection({
253
+ title,
254
+ items,
255
+ }: {
256
+ title: string;
257
+ items: Array<[label: string, value: string | number]>;
258
+ }) {
259
+ return (
260
+ <section className="space-y-3">
261
+ <h3 className="text-sm font-medium">{title}</h3>
262
+ <dl className="grid gap-4 sm:grid-cols-2">
263
+ {items.map(([label, value]) => (
264
+ <div key={label} className="space-y-1">
265
+ <dt className="text-xs text-muted-foreground">{label}</dt>
266
+ <dd className="text-sm font-medium break-words">{value}</dd>
267
+ </div>
268
+ ))}
269
+ </dl>
270
+ </section>
271
+ );
272
+ }
@@ -0,0 +1,6 @@
1
+ import type { Role } from "@/lib/nocobase/acl";
2
+ import { resolveTranslatableText } from "@/lib/i18n";
3
+
4
+ export function resolveRoleLabel(role: Role) {
5
+ return resolveTranslatableText(role.title || role.name, { ns: "starter" });
6
+ }
@@ -0,0 +1,24 @@
1
+ export const userRoutes = {
2
+ list: "/users",
3
+ create: "/users/create",
4
+ edit: "/users/edit/:id",
5
+ show: "/users/show/:id",
6
+ role: "/users/roles/:roleName",
7
+ nestedEdit: "/users/show/:id/edit",
8
+ nestedRole: "/users/show/:id/roles/:roleName",
9
+ } as const;
10
+
11
+ export const getUserEditPath = (id: string | number) =>
12
+ `/users/edit/${encodeURIComponent(id)}`;
13
+
14
+ export const getUserShowPath = (id: string | number) =>
15
+ `/users/show/${encodeURIComponent(id)}`;
16
+
17
+ export const getRolePath = (roleName: string) =>
18
+ `/users/roles/${encodeURIComponent(roleName)}`;
19
+
20
+ export const getUserRolePath = (
21
+ id: string | number,
22
+ roleName: string
23
+ ) =>
24
+ `/users/show/${encodeURIComponent(id)}/roles/${encodeURIComponent(roleName)}`;
@@ -1,30 +1,34 @@
1
1
  import {
2
2
  useGetLocale,
3
- useResourceParams,
4
3
  useShow,
5
4
  useTranslate,
6
5
  } from "@refinedev/core";
6
+ import { useNavigate, useOutlet, useParams } from "react-router";
7
+ import { Pencil, RotateCw } from "lucide-react";
7
8
 
8
- import { ShowView } from "@/components/resources/views/show-view";
9
- import { Badge } from "@/components/ui/badge";
10
- import {
11
- Card,
12
- CardContent,
13
- CardDescription,
14
- CardHeader,
15
- CardTitle,
16
- } from "@/components/ui/card";
9
+ import { LoadingState } from "@/components/app-shell/loading-state";
10
+ import { EditButton } from "@/components/resources/buttons/edit";
11
+ import { RefreshButton } from "@/components/resources/buttons/refresh";
12
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
17
13
  import { Separator } from "@/components/ui/separator";
14
+ import { Skeleton } from "@/components/ui/skeleton";
15
+ import { RouteDrawer } from "@/extensions/nocobase-route-surfaces";
18
16
  import { useAIPageElementHandle } from "@/extensions/nocobase-ai";
19
- import { resolveTranslatableText } from "@/lib/i18n";
17
+ import { RoleBadges } from "./role-badges";
18
+ import { resolveRoleLabel } from "./role-utils";
19
+ import { getUserRolePath, userRoutes } from "./routes";
20
20
  import type { UserRecord } from "./types";
21
21
 
22
22
  export const UserShow = () => {
23
23
  const translate = useTranslate();
24
24
  const getLocale = useGetLocale();
25
25
  const locale = getLocale();
26
- const { id } = useResourceParams();
27
- const { result: record } = useShow<UserRecord>({
26
+ const navigate = useNavigate();
27
+ const { id } = useParams<{ id: string }>();
28
+ const nestedDrawer = useOutlet();
29
+ const { result: record, query } = useShow<UserRecord>({
30
+ resource: "users",
31
+ id,
28
32
  meta: {
29
33
  appends: ["roles"],
30
34
  },
@@ -35,10 +39,7 @@ export const UserShow = () => {
35
39
  record?.username ||
36
40
  record?.email ||
37
41
  translate("users.detail.unnamed", { ns: "app" }, "Unnamed user");
38
- const roleLabels =
39
- record?.roles?.map((role) =>
40
- resolveTranslatableText(role.title || role.name, { ns: "starter" })
41
- ) ?? [];
42
+ const roles = record?.roles ?? [];
42
43
  const detailContext = useAIPageElementHandle({
43
44
  id: `users-detail-${id ?? "current"}`,
44
45
  title: `${translate(
@@ -55,7 +56,10 @@ export const UserShow = () => {
55
56
  username: record?.username,
56
57
  email: record?.email,
57
58
  phone: record?.phone,
58
- roles: roleLabels,
59
+ roles: roles.map((role) => ({
60
+ name: role.name,
61
+ title: resolveRoleLabel(role),
62
+ })),
59
63
  createdAt: record?.createdAt,
60
64
  updatedAt: record?.updatedAt,
61
65
  },
@@ -71,96 +75,183 @@ export const UserShow = () => {
71
75
  : "-";
72
76
 
73
77
  return (
74
- <ShowView title={displayName}>
75
- <Card ref={detailContext.ref} className="max-w-4xl">
76
- <CardHeader>
77
- <CardTitle>{displayName}</CardTitle>
78
- <CardDescription>ID: {record?.id ?? "-"}</CardDescription>
79
- </CardHeader>
80
- <CardContent className="space-y-5">
81
- <DetailSection
82
- title={translate(
83
- "users.detail.identity",
84
- { ns: "app" },
85
- "Identity"
86
- )}
87
- items={[
88
- [
89
- translate("users.fields.nickname", { ns: "app" }, "Nickname"),
90
- record?.nickname || "-",
91
- ],
92
- [
93
- translate("users.fields.username", { ns: "app" }, "Username"),
94
- record?.username || "-",
95
- ],
96
- ]}
97
- />
98
- <Separator />
99
- <DetailSection
100
- title={translate("users.detail.contact", { ns: "app" }, "Contact")}
101
- items={[
102
- [
103
- translate("users.fields.email", { ns: "app" }, "Email"),
104
- record?.email || "-",
105
- ],
106
- [
107
- translate("users.fields.phone", { ns: "app" }, "Phone"),
108
- record?.phone || "-",
109
- ],
110
- ]}
111
- />
112
- <Separator />
113
- <section className="space-y-3">
114
- <h3 className="text-sm font-medium">
115
- {translate("users.detail.access", { ns: "app" }, "Access")}
116
- </h3>
117
- <div className="flex flex-wrap gap-2">
118
- {roleLabels.length ? (
119
- roleLabels.map((role, index) => (
120
- <Badge key={`${role}-${index}`} variant="secondary">
121
- {role}
122
- </Badge>
123
- ))
124
- ) : (
125
- <span className="text-sm text-muted-foreground">
126
- {translate(
78
+ <RouteDrawer
79
+ title={
80
+ query.isLoading && !record ? (
81
+ <Skeleton className="h-6 w-40" />
82
+ ) : (
83
+ displayName
84
+ )
85
+ }
86
+ description={translate(
87
+ "users.drawer.show.description",
88
+ { ns: "app" },
89
+ "Review this user's identity, contact information, and roles."
90
+ )}
91
+ closeLabel={translate("buttons.close", "Close")}
92
+ closeTo={userRoutes.list}
93
+ nested={nestedDrawer}
94
+ actions={
95
+ record ? (
96
+ <>
97
+ <RefreshButton
98
+ resource="users"
99
+ recordItemId={record.id}
100
+ variant="outline"
101
+ size="icon-sm"
102
+ aria-label={translate("buttons.refresh", "Refresh")}
103
+ title={translate("buttons.refresh", "Refresh")}
104
+ >
105
+ <RotateCw />
106
+ </RefreshButton>
107
+ <EditButton
108
+ resource="users"
109
+ recordItemId={record.id}
110
+ variant="outline"
111
+ size="icon-sm"
112
+ aria-label={translate(
113
+ "users.actions.edit",
114
+ { ns: "app" },
115
+ "Edit user"
116
+ )}
117
+ title={translate(
118
+ "users.actions.edit",
119
+ { ns: "app" },
120
+ "Edit user"
121
+ )}
122
+ onClick={() => navigate("edit")}
123
+ >
124
+ <Pencil />
125
+ </EditButton>
126
+ </>
127
+ ) : null
128
+ }
129
+ >
130
+ <div
131
+ ref={detailContext.ref}
132
+ className="min-h-0 flex-1 overflow-y-auto px-5 py-5"
133
+ >
134
+ {query.isLoading ? (
135
+ <LoadingState className="min-h-64" />
136
+ ) : query.isError ? (
137
+ <Alert variant="destructive">
138
+ <AlertTitle>
139
+ {translate(
140
+ "users.detail.loadError.title",
141
+ { ns: "app" },
142
+ "Unable to load user"
143
+ )}
144
+ </AlertTitle>
145
+ <AlertDescription>
146
+ {translate(
147
+ "users.detail.loadError.description",
148
+ { ns: "app" },
149
+ "The user may no longer exist, or you may not have permission to view it."
150
+ )}
151
+ </AlertDescription>
152
+ </Alert>
153
+ ) : (
154
+ <div className="space-y-5">
155
+ <DetailSection
156
+ title={translate(
157
+ "users.detail.identity",
158
+ { ns: "app" },
159
+ "Identity"
160
+ )}
161
+ items={[
162
+ [
163
+ translate("users.fields.id", { ns: "app" }, "ID"),
164
+ record?.id ?? "-",
165
+ ],
166
+ [
167
+ translate(
168
+ "users.fields.nickname",
169
+ { ns: "app" },
170
+ "Nickname"
171
+ ),
172
+ record?.nickname || "-",
173
+ ],
174
+ [
175
+ translate(
176
+ "users.fields.username",
177
+ { ns: "app" },
178
+ "Username"
179
+ ),
180
+ record?.username || "-",
181
+ ],
182
+ ]}
183
+ />
184
+
185
+ <Separator />
186
+
187
+ <DetailSection
188
+ title={translate(
189
+ "users.detail.contact",
190
+ { ns: "app" },
191
+ "Contact"
192
+ )}
193
+ items={[
194
+ [
195
+ translate("users.fields.email", { ns: "app" }, "Email"),
196
+ record?.email || "-",
197
+ ],
198
+ [
199
+ translate("users.fields.phone", { ns: "app" }, "Phone"),
200
+ record?.phone || "-",
201
+ ],
202
+ ]}
203
+ />
204
+
205
+ <Separator />
206
+
207
+ <section className="space-y-3">
208
+ <h3 className="text-sm font-medium">
209
+ {translate("users.detail.access", { ns: "app" }, "Access")}
210
+ </h3>
211
+ <RoleBadges
212
+ roles={roles}
213
+ onSelect={(role) =>
214
+ id ? navigate(getUserRolePath(id, role.name)) : undefined
215
+ }
216
+ empty={translate(
127
217
  "users.detail.noRoles",
128
218
  { ns: "app" },
129
219
  "No assigned roles"
130
220
  )}
131
- </span>
132
- )}
133
- </div>
134
- </section>
135
- <Separator />
136
- <DetailSection
137
- title={translate(
138
- "users.detail.timestamps",
139
- { ns: "app" },
140
- "Timestamps"
141
- )}
142
- items={[
143
- [
144
- translate(
145
- "users.fields.createdAt",
146
- { ns: "app" },
147
- "Created at"
148
- ),
149
- formatDate(record?.createdAt),
150
- ],
151
- [
152
- translate(
153
- "users.fields.updatedAt",
221
+ />
222
+ </section>
223
+
224
+ <Separator />
225
+
226
+ <DetailSection
227
+ title={translate(
228
+ "users.detail.timestamps",
154
229
  { ns: "app" },
155
- "Updated at"
156
- ),
157
- formatDate(record?.updatedAt),
158
- ],
159
- ]}
160
- />
161
- </CardContent>
162
- </Card>
163
- </ShowView>
230
+ "Timestamps"
231
+ )}
232
+ items={[
233
+ [
234
+ translate(
235
+ "users.fields.createdAt",
236
+ { ns: "app" },
237
+ "Created at"
238
+ ),
239
+ formatDate(record?.createdAt),
240
+ ],
241
+ [
242
+ translate(
243
+ "users.fields.updatedAt",
244
+ { ns: "app" },
245
+ "Updated at"
246
+ ),
247
+ formatDate(record?.updatedAt),
248
+ ],
249
+ ]}
250
+ />
251
+ </div>
252
+ )}
253
+ </div>
254
+ </RouteDrawer>
164
255
  );
165
256
  };
166
257
 
@@ -178,7 +269,7 @@ function DetailSection({
178
269
  {items.map(([label, value]) => (
179
270
  <div key={label} className="space-y-1">
180
271
  <dt className="text-xs text-muted-foreground">{label}</dt>
181
- <dd className="text-sm font-medium">{value}</dd>
272
+ <dd className="text-sm font-medium break-words">{value}</dd>
182
273
  </div>
183
274
  ))}
184
275
  </dl>
@@ -18,3 +18,16 @@ export type UserFormValues = {
18
18
  phone: string;
19
19
  password?: string;
20
20
  };
21
+
22
+ export type RoleRecord = Role & {
23
+ id?: string | number;
24
+ description?: string;
25
+ default?: boolean;
26
+ hidden?: boolean;
27
+ allowConfigure?: boolean;
28
+ strategy?: {
29
+ actions?: string[];
30
+ };
31
+ createdAt?: string;
32
+ updatedAt?: string;
33
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./i18n-provider";
2
+ export * from "./locale-store";
3
+ export * from "./runtime";