@nocobase/portal-template-default 1.0.3 → 1.0.5

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 (170) hide show
  1. package/README.MD +10 -5
  2. package/package.json +17 -17
  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/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
  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/scripts/registry.mjs +0 -2
  36. package/src/App.tsx +65 -35
  37. package/src/app/extension.ts +1 -2
  38. package/src/app/extensions.tsx +0 -14
  39. package/src/components/app-shell/breadcrumb.tsx +67 -2
  40. package/src/components/app-shell/sidebar.tsx +2 -2
  41. package/src/components/resources/views/list-view.tsx +3 -2
  42. package/src/components/ui/sheet.tsx +22 -21
  43. package/src/lib/nocobase/client.ts +6 -1
  44. package/src/locales/en-US.ts +81 -0
  45. package/src/locales/zh-CN.ts +77 -0
  46. package/src/pages/users/create.tsx +76 -53
  47. package/src/pages/users/edit.tsx +87 -56
  48. package/src/pages/users/index.ts +3 -0
  49. package/src/pages/users/layout.tsx +15 -0
  50. package/src/pages/users/list.tsx +19 -21
  51. package/src/pages/users/role-badges.tsx +43 -0
  52. package/src/pages/users/role-detail.tsx +272 -0
  53. package/src/pages/users/role-utils.ts +6 -0
  54. package/src/pages/users/routes.ts +24 -0
  55. package/src/pages/users/show.tsx +195 -104
  56. package/src/pages/users/types.ts +13 -0
  57. package/src/providers/i18n/index.ts +3 -0
  58. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  59. package/src/providers/system-settings/context.ts +26 -0
  60. package/src/providers/system-settings/index.ts +2 -0
  61. package/src/providers/system-settings/provider.tsx +94 -0
  62. package/src/extensions/nocobase-acl/README.md +0 -19
  63. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
  64. package/src/extensions/nocobase-acl/components/index.ts +0 -3
  65. package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
  66. package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
  67. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
  68. package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
  69. package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
  70. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
  71. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
  72. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
  73. package/src/extensions/nocobase-acl/extension.tsx +0 -71
  74. package/src/extensions/nocobase-acl/index.ts +0 -1
  75. package/src/extensions/nocobase-ai/README.md +0 -70
  76. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
  77. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
  78. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
  79. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
  80. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
  81. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
  82. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
  83. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
  84. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
  85. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
  86. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
  87. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
  88. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
  89. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
  90. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
  91. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
  92. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
  93. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
  94. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
  95. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
  96. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
  97. package/src/extensions/nocobase-ai/components/index.ts +0 -59
  98. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
  99. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
  100. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  101. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
  102. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
  103. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
  104. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  105. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
  106. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
  107. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
  108. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
  109. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
  110. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
  111. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
  112. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
  113. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
  114. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
  115. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
  116. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
  117. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
  118. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
  119. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
  120. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
  121. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
  122. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
  123. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
  124. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
  125. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
  126. package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
  127. package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
  128. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
  129. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
  130. package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
  131. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
  132. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
  133. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
  134. package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
  135. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
  136. package/src/extensions/nocobase-ai/extension.tsx +0 -158
  137. package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
  138. package/src/extensions/nocobase-ai/index.ts +0 -3
  139. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
  140. package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
  141. package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
  142. package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
  143. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
  144. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
  145. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
  146. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
  147. package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
  148. package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
  149. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
  150. package/src/extensions/nocobase-ai/providers/index.ts +0 -61
  151. package/src/extensions/nocobase-ai/providers/model.ts +0 -32
  152. package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
  153. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
  154. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
  155. package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
  156. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
  157. package/src/extensions/nocobase-ai/providers/types.ts +0 -183
  158. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
  159. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
  160. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
  161. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
  162. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
  163. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
  164. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
  165. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
  166. package/src/extensions/nocobase-ai/services/index.ts +0 -7
  167. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
  168. package/src/extensions/nocobase-ai/services/types.ts +0 -82
  169. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  170. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -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";
@@ -3,6 +3,7 @@ import { createInstance, type TOptions } from "i18next";
3
3
  import {
4
4
  getTranslationResources,
5
5
  registerTranslationResources,
6
+ setTranslationResolver,
6
7
  subscribeTranslationResources,
7
8
  type TranslationOptions,
8
9
  } from "@/lib/i18n";
@@ -14,15 +15,23 @@ import {
14
15
  getLocaleLabel,
15
16
  registerLocale,
16
17
  resolveSupportedLocale,
18
+ setEnabledLocales,
17
19
  } from "./locale-store";
18
- import enUS from "./locales/en-US";
19
- import zhCN from "./locales/zh-CN";
20
20
 
21
21
  export type LocaleResources = Record<
22
22
  string,
23
23
  Record<string, string | number | boolean>
24
24
  >;
25
25
 
26
+ export type LocaleSystemSettings = {
27
+ appLang?: string | null;
28
+ enabledLanguages?: string[] | null;
29
+ };
30
+
31
+ type LocalePersistence = (locale: string) => void | Promise<void>;
32
+
33
+ let localePersistence: LocalePersistence | undefined;
34
+
26
35
  export const i18n = createInstance();
27
36
 
28
37
  void i18n.init({
@@ -36,10 +45,6 @@ void i18n.init({
36
45
  interpolation: {
37
46
  escapeValue: false,
38
47
  },
39
- resources: {
40
- "en-US": enUS,
41
- "zh-CN": zhCN,
42
- },
43
48
  });
44
49
 
45
50
  function addLocaleResources(namespace: string, resources: LocaleResources) {
@@ -56,7 +61,6 @@ function addLocaleResources(namespace: string, resources: LocaleResources) {
56
61
  getTranslationResources().forEach(([namespace, resources]) =>
57
62
  addLocaleResources(namespace, resources)
58
63
  );
59
-
60
64
  subscribeTranslationResources(addLocaleResources);
61
65
 
62
66
  export function getCurrentLocale() {
@@ -94,18 +98,60 @@ export function applyDocumentLocale(locale = getCurrentLocale()) {
94
98
  document.documentElement.dir = direction;
95
99
  }
96
100
 
101
+ function resolveSystemLocale(settings?: LocaleSystemSettings) {
102
+ const enabledLanguages = Array.isArray(settings?.enabledLanguages)
103
+ ? settings.enabledLanguages.filter(Boolean)
104
+ : [];
105
+ const storedLocale = nocobaseClient.getStoredLocale();
106
+
107
+ if (
108
+ storedLocale &&
109
+ (!enabledLanguages.length || enabledLanguages.includes(storedLocale))
110
+ ) {
111
+ return resolveSupportedLocale(storedLocale);
112
+ }
113
+
114
+ const defaultLocale =
115
+ settings?.appLang || enabledLanguages[0] || DEFAULT_LOCALE;
116
+ return resolveSupportedLocale(defaultLocale);
117
+ }
118
+
119
+ export async function applySystemLocale(settings?: LocaleSystemSettings) {
120
+ const enabledLanguages = Array.isArray(settings?.enabledLanguages)
121
+ ? settings.enabledLanguages.filter(Boolean)
122
+ : [];
123
+ if (enabledLanguages.length) setEnabledLocales(enabledLanguages);
124
+
125
+ const storedLocale = nocobaseClient.getStoredLocale();
126
+ if (
127
+ storedLocale &&
128
+ enabledLanguages.length &&
129
+ !enabledLanguages.includes(storedLocale)
130
+ ) {
131
+ nocobaseClient.setLocale(null);
132
+ }
133
+
134
+ const locale = resolveSystemLocale(settings);
135
+ nocobaseClient.setRuntimeLocale(locale);
136
+ await i18n.changeLanguage(locale);
137
+ applyDocumentLocale(locale);
138
+ return locale;
139
+ }
140
+
141
+ export function setLocalePersistence(persistence?: LocalePersistence) {
142
+ localePersistence = persistence;
143
+ return () => {
144
+ if (localePersistence === persistence) localePersistence = undefined;
145
+ };
146
+ }
147
+
97
148
  export async function changeLocale(locale: string) {
98
149
  const nextLocale = resolveSupportedLocale(locale);
99
150
 
100
- if (nocobaseClient.getToken()) {
101
- try {
102
- await nocobaseClient.action("users", "updateLang", {
103
- method: "POST",
104
- body: { appLang: nextLocale },
105
- });
106
- } catch (error) {
107
- console.warn("Unable to persist the NocoBase language preference", error);
108
- }
151
+ try {
152
+ await localePersistence?.(nextLocale);
153
+ } catch (error) {
154
+ console.warn("Unable to persist the language preference", error);
109
155
  }
110
156
 
111
157
  nocobaseClient.setLocale(nextLocale);
@@ -115,4 +161,5 @@ export async function changeLocale(locale: string) {
115
161
  if (typeof window !== "undefined") window.location.reload();
116
162
  }
117
163
 
164
+ setTranslationResolver(translate);
118
165
  applyDocumentLocale();
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ export type SystemSettings = Record<string, unknown> & {
4
+ appLang?: string | null;
5
+ enabledLanguages?: string[] | null;
6
+ };
7
+
8
+ export type SystemSettingsContextValue = {
9
+ settings?: SystemSettings;
10
+ error?: Error;
11
+ loading: boolean;
12
+ refresh: () => Promise<SystemSettings | undefined>;
13
+ };
14
+
15
+ export const SystemSettingsContext =
16
+ createContext<SystemSettingsContextValue | null>(null);
17
+
18
+ export function useSystemSettings() {
19
+ const value = useContext(SystemSettingsContext);
20
+ if (!value) {
21
+ throw new Error(
22
+ "useSystemSettings must be used inside SystemSettingsProvider"
23
+ );
24
+ }
25
+ return value;
26
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./context";
2
+ export * from "./provider";
@@ -0,0 +1,94 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useState,
6
+ type PropsWithChildren,
7
+ } from "react";
8
+
9
+ import { LoadingState } from "@/components/app-shell/loading-state";
10
+ import { nocobaseClient } from "@/lib/nocobase/client";
11
+ import { applySystemLocale } from "../i18n";
12
+ import {
13
+ SystemSettingsContext,
14
+ type SystemSettings,
15
+ type SystemSettingsContextValue,
16
+ } from "./context";
17
+
18
+ let cachedSettings: SystemSettings | undefined;
19
+ let settingsRequest: Promise<SystemSettings> | undefined;
20
+
21
+ function requestSystemSettings(force = false) {
22
+ if (!force && cachedSettings) return Promise.resolve(cachedSettings);
23
+ if (!force && settingsRequest) return settingsRequest;
24
+
25
+ const request = nocobaseClient
26
+ .action<SystemSettings>("systemSettings", "get", {
27
+ method: "GET",
28
+ includeRole: false,
29
+ withAclMeta: false,
30
+ })
31
+ .then((settings) => {
32
+ cachedSettings = settings;
33
+ return settings;
34
+ });
35
+
36
+ settingsRequest = request;
37
+ const clearRequest = () => {
38
+ if (settingsRequest === request) settingsRequest = undefined;
39
+ };
40
+ void request.then(clearRequest, clearRequest);
41
+ return request;
42
+ }
43
+
44
+ export function SystemSettingsProvider({ children }: PropsWithChildren) {
45
+ const [settings, setSettings] = useState(cachedSettings);
46
+ const [error, setError] = useState<Error>();
47
+ const [ready, setReady] = useState(false);
48
+ const [loading, setLoading] = useState(true);
49
+
50
+ const load = useCallback(async (force = false) => {
51
+ setLoading(true);
52
+ try {
53
+ const nextSettings = await requestSystemSettings(force);
54
+ await applySystemLocale(nextSettings);
55
+ setSettings(nextSettings);
56
+ setError(undefined);
57
+ return nextSettings;
58
+ } catch (reason) {
59
+ const nextError =
60
+ reason instanceof Error
61
+ ? reason
62
+ : new Error("Unable to load system settings");
63
+ console.warn("Unable to load NocoBase system settings", reason);
64
+ setError(nextError);
65
+ await applySystemLocale();
66
+ return undefined;
67
+ } finally {
68
+ setLoading(false);
69
+ setReady(true);
70
+ }
71
+ }, []);
72
+
73
+ useEffect(() => {
74
+ void load();
75
+ }, [load]);
76
+
77
+ const value = useMemo<SystemSettingsContextValue>(
78
+ () => ({
79
+ settings,
80
+ error,
81
+ loading,
82
+ refresh: () => load(true),
83
+ }),
84
+ [error, load, loading, settings]
85
+ );
86
+
87
+ if (!ready) return <LoadingState className="min-h-svh" />;
88
+
89
+ return (
90
+ <SystemSettingsContext.Provider value={value}>
91
+ {children}
92
+ </SystemSettingsContext.Provider>
93
+ );
94
+ }
@@ -1,19 +0,0 @@
1
- # NocoBase ACL UI
2
-
3
- Reusable ACL composition components for the NocoBase Refine starter.
4
-
5
- After installation, import the components from `@/extensions/nocobase-acl`.
6
-
7
- - `AclPage` combines page-level permissions.
8
- - `AclRegion` hides or replaces individual data regions.
9
- - `AclField` applies NocoBase field whitelists.
10
- - `RoleSwitcher` can be placed in any application surface. It supports
11
- normal roles, Anonymous when enabled, and Full permissions through the
12
- `__union__` role when the system role mode allows it.
13
-
14
- Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
15
- the `collection`, `snippet`, and `route` ACL metadata variants when the resource
16
- should participate in NocoBase permission checks.
17
-
18
- The underlying `accessControlProvider`, `roles:check` cache, record permissions,
19
- and Refine integration are provided by the compatible Starter.