@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,38 +1,78 @@
1
- import {
2
- type HttpError,
3
- useResourceParams,
4
- useTranslate,
5
- } from "@refinedev/core";
1
+ import { type HttpError, useTranslate } from "@refinedev/core";
6
2
  import { useForm } from "@refinedev/react-hook-form";
7
3
  import { useMemo } from "react";
8
- import { useNavigate } from "react-router";
4
+ import { useParams } from "react-router";
9
5
 
10
- import { EditView } from "@/components/resources/views/edit-view";
11
6
  import { Button } from "@/components/ui/button";
12
- import { Card, CardContent } from "@/components/ui/card";
13
7
  import { Form } from "@/components/ui/form";
14
8
  import { useAIForm, type AIFormField } from "@/extensions/nocobase-ai";
9
+ import {
10
+ RouteDrawer,
11
+ RouteDrawerFooter,
12
+ useRefineUnsavedChangesGuard,
13
+ useRouteSurfaceClose,
14
+ } from "@/extensions/nocobase-route-surfaces";
15
15
  import {
16
16
  applyAIUserFormValues,
17
17
  getAIUserFormFields,
18
18
  getAIUserFormValues,
19
19
  } from "./form-context";
20
20
  import { UserFormFields } from "./form-fields";
21
+ import { getUserShowPath, userRoutes } from "./routes";
21
22
  import type { UserFormValues, UserRecord } from "./types";
22
23
 
23
- export const UserEdit = () => {
24
+ export const UserEdit = ({
25
+ returnTo = "list",
26
+ }: {
27
+ returnTo?: "list" | "show";
28
+ }) => {
24
29
  const translate = useTranslate();
25
- const navigate = useNavigate();
26
- const { id } = useResourceParams();
30
+ const { id } = useParams<{ id: string }>();
31
+ const closeTo =
32
+ returnTo === "show" && id ? getUserShowPath(id) : userRoutes.list;
33
+ const { beforeClose, confirmation } = useRefineUnsavedChangesGuard();
34
+
35
+ return (
36
+ <>
37
+ <RouteDrawer
38
+ title={translate(
39
+ "users.drawer.edit.title",
40
+ { ns: "app" },
41
+ "Edit user"
42
+ )}
43
+ description={translate(
44
+ "users.drawer.edit.description",
45
+ { ns: "app" },
46
+ "Update this user's identity and contact information."
47
+ )}
48
+ closeLabel={translate("buttons.close", "Close")}
49
+ closeTo={closeTo}
50
+ beforeClose={beforeClose}
51
+ >
52
+ <UserEditForm id={id} />
53
+ </RouteDrawer>
54
+ {confirmation}
55
+ </>
56
+ );
57
+ };
58
+
59
+ function UserEditForm({ id }: { id?: string }) {
60
+ const translate = useTranslate();
61
+ const close = useRouteSurfaceClose();
27
62
  const {
28
63
  refineCore: { onFinish },
29
64
  ...form
30
65
  } = useForm<UserRecord, HttpError, UserFormValues>({
31
66
  refineCoreProps: {
32
- redirect: "list",
67
+ action: "edit",
68
+ resource: "users",
69
+ id,
70
+ redirect: false,
71
+ onMutationSuccess: () => {
72
+ void close({ skipBeforeClose: true });
73
+ },
33
74
  },
34
75
  });
35
-
36
76
  const aiFields = useMemo<AIFormField[]>(
37
77
  () => getAIUserFormFields(translate),
38
78
  [translate]
@@ -46,47 +86,38 @@ export const UserEdit = () => {
46
86
  });
47
87
 
48
88
  return (
49
- <EditView>
50
- <Card className="max-w-3xl">
51
- <CardContent>
52
- <Form {...form}>
53
- <form
54
- ref={aiFormRef}
55
- onSubmit={form.handleSubmit((values) => onFinish(values))}
56
- className="resource-form"
57
- >
58
- <UserFormFields form={form} translate={translate} />
59
-
60
- <div className="flex flex-wrap gap-2">
61
- <Button
62
- type="submit"
63
- {...form.saveButtonProps}
64
- disabled={form.formState.isSubmitting}
65
- >
66
- {form.formState.isSubmitting
67
- ? translate(
68
- "users.form.edit.submitting",
69
- { ns: "app" },
70
- "Saving..."
71
- )
72
- : translate(
73
- "users.form.edit.submit",
74
- { ns: "app" },
75
- "Save changes"
76
- )}
77
- </Button>
78
- <Button
79
- type="button"
80
- variant="outline"
81
- onClick={() => navigate(-1)}
82
- >
83
- {translate("users.form.cancel", { ns: "app" }, "Cancel")}
84
- </Button>
85
- </div>
86
- </form>
87
- </Form>
88
- </CardContent>
89
- </Card>
90
- </EditView>
89
+ <Form {...form}>
90
+ <form
91
+ ref={aiFormRef}
92
+ onSubmit={form.handleSubmit((values) => onFinish(values))}
93
+ className="flex min-h-0 flex-1 flex-col"
94
+ >
95
+ <div className="resource-form min-h-0 flex-1 overflow-y-auto px-5 py-5">
96
+ <UserFormFields form={form} translate={translate} />
97
+ </div>
98
+ <RouteDrawerFooter className="flex-row justify-end">
99
+ <Button type="button" variant="outline" onClick={() => void close()}>
100
+ {translate("users.form.cancel", { ns: "app" }, "Cancel")}
101
+ </Button>
102
+ <Button
103
+ type="submit"
104
+ {...form.saveButtonProps}
105
+ disabled={form.formState.isSubmitting}
106
+ >
107
+ {form.formState.isSubmitting
108
+ ? translate(
109
+ "users.form.edit.submitting",
110
+ { ns: "app" },
111
+ "Saving..."
112
+ )
113
+ : translate(
114
+ "users.form.edit.submit",
115
+ { ns: "app" },
116
+ "Save changes"
117
+ )}
118
+ </Button>
119
+ </RouteDrawerFooter>
120
+ </form>
121
+ </Form>
91
122
  );
92
- };
123
+ }
@@ -1,4 +1,7 @@
1
1
  export * from "./create";
2
2
  export * from "./edit";
3
+ export * from "./layout";
3
4
  export * from "./list";
5
+ export * from "./role-detail";
6
+ export * from "./routes";
4
7
  export * from "./show";
@@ -0,0 +1,15 @@
1
+ import { Outlet } from "react-router";
2
+
3
+ import { ResourceAccessGuard } from "@/components/access-control/resource-access-guard";
4
+ import { UserList } from "./list";
5
+
6
+ export function UserResourceLayout() {
7
+ return (
8
+ <>
9
+ <ResourceAccessGuard resource="users" action="list">
10
+ <UserList />
11
+ </ResourceAccessGuard>
12
+ <Outlet />
13
+ </>
14
+ );
15
+ }
@@ -2,22 +2,21 @@ import { useGetLocale, useTranslate } from "@refinedev/core";
2
2
  import { useTable } from "@refinedev/react-table";
3
3
  import { createColumnHelper } from "@tanstack/react-table";
4
4
  import { Eye, Pencil, Trash2 } from "lucide-react";
5
- import { useMemo } from "react";
5
+ import { useCallback, useMemo } from "react";
6
+ import { useNavigate } from "react-router";
6
7
 
7
8
  import { DataTable } from "@/components/data-table/data-table";
8
9
  import { DeleteButton } from "@/components/resources/buttons/delete";
9
10
  import { EditButton } from "@/components/resources/buttons/edit";
10
11
  import { ShowButton } from "@/components/resources/buttons/show";
11
12
  import { ListView } from "@/components/resources/views/list-view";
12
- import { Badge } from "@/components/ui/badge";
13
13
  import { useAIPageElementHandle } from "@/extensions/nocobase-ai";
14
- import { resolveTranslatableText } from "@/lib/i18n";
15
14
  import type { Role } from "@/lib/nocobase/acl";
15
+ import { RoleBadges } from "./role-badges";
16
+ import { resolveRoleLabel } from "./role-utils";
17
+ import { getRolePath } from "./routes";
16
18
  import type { UserRecord } from "./types";
17
19
 
18
- const getRoleLabel = (role: Role) =>
19
- resolveTranslatableText(role.title || role.name, { ns: "starter" });
20
-
21
20
  const isRootUser = (record: UserRecord) =>
22
21
  record.roles?.some((role) => role.name === "root") ?? false;
23
22
 
@@ -25,6 +24,11 @@ export const UserList = () => {
25
24
  const translate = useTranslate();
26
25
  const getLocale = useGetLocale();
27
26
  const locale = getLocale();
27
+ const navigate = useNavigate();
28
+ const showRole = useCallback(
29
+ (role: Role) => navigate(getRolePath(role.name)),
30
+ [navigate]
31
+ );
28
32
 
29
33
  const columns = useMemo(() => {
30
34
  const columnHelper = createColumnHelper<UserRecord>();
@@ -61,17 +65,7 @@ export const UserList = () => {
61
65
  enableSorting: false,
62
66
  cell: ({ getValue }) => {
63
67
  const roles = getValue() ?? [];
64
- return roles.length ? (
65
- <div className="flex flex-wrap gap-1">
66
- {roles.map((role) => (
67
- <Badge key={role.name} variant="secondary">
68
- {getRoleLabel(role)}
69
- </Badge>
70
- ))}
71
- </div>
72
- ) : (
73
- "-"
74
- );
68
+ return <RoleBadges roles={roles} onSelect={showRole} empty="-" />;
75
69
  },
76
70
  }),
77
71
  columnHelper.accessor("createdAt", {
@@ -97,6 +91,7 @@ export const UserList = () => {
97
91
  cell: ({ row }) => (
98
92
  <div className="flex items-center gap-1">
99
93
  <EditButton
94
+ resource="users"
100
95
  recordItemId={row.original.id}
101
96
  variant="ghost"
102
97
  size="icon"
@@ -114,6 +109,7 @@ export const UserList = () => {
114
109
  <Pencil />
115
110
  </EditButton>
116
111
  <ShowButton
112
+ resource="users"
117
113
  recordItemId={row.original.id}
118
114
  variant="ghost"
119
115
  size="icon"
@@ -132,6 +128,7 @@ export const UserList = () => {
132
128
  </ShowButton>
133
129
  {isRootUser(row.original) ? null : (
134
130
  <DeleteButton
131
+ resource="users"
135
132
  recordItemId={row.original.id}
136
133
  variant="ghost"
137
134
  size="icon"
@@ -156,12 +153,13 @@ export const UserList = () => {
156
153
  size: 144,
157
154
  }),
158
155
  ];
159
- }, [locale, translate]);
156
+ }, [locale, showRole, translate]);
160
157
 
161
158
  const table = useTable<UserRecord>({
162
159
  columns,
163
160
  refineCoreProps: {
164
- syncWithLocation: true,
161
+ resource: "users",
162
+ syncWithLocation: false,
165
163
  meta: {
166
164
  appends: ["roles"],
167
165
  },
@@ -188,7 +186,7 @@ export const UserList = () => {
188
186
  phone: record.phone,
189
187
  roles: record.roles?.map((role) => ({
190
188
  name: role.name,
191
- title: getRoleLabel(role),
189
+ title: resolveRoleLabel(role),
192
190
  })),
193
191
  createdAt: record.createdAt,
194
192
  })),
@@ -196,7 +194,7 @@ export const UserList = () => {
196
194
  });
197
195
 
198
196
  return (
199
- <ListView>
197
+ <ListView resource="users">
200
198
  <div ref={tableContext.ref}>
201
199
  <DataTable table={table} />
202
200
  </div>
@@ -0,0 +1,43 @@
1
+ import type { Role } from "@/lib/nocobase/acl";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { ChevronRight } from "lucide-react";
4
+ import { resolveRoleLabel } from "./role-utils";
5
+
6
+ export function RoleBadges({
7
+ roles,
8
+ onSelect,
9
+ empty,
10
+ }: {
11
+ roles: Role[];
12
+ onSelect: (role: Role) => void;
13
+ empty: string;
14
+ }) {
15
+ if (!roles.length) {
16
+ return <span className="text-sm text-muted-foreground">{empty}</span>;
17
+ }
18
+
19
+ return (
20
+ <div className="flex flex-wrap gap-1">
21
+ {roles.map((role) => {
22
+ const label = resolveRoleLabel(role);
23
+ return (
24
+ <button
25
+ key={role.name}
26
+ type="button"
27
+ className="rounded-full outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
28
+ onClick={() => onSelect(role)}
29
+ title={label}
30
+ >
31
+ <Badge
32
+ variant="secondary"
33
+ className="cursor-pointer transition-colors hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_8%)]"
34
+ >
35
+ {label}
36
+ <ChevronRight data-icon="inline-end" />
37
+ </Badge>
38
+ </button>
39
+ );
40
+ })}
41
+ </div>
42
+ );
43
+ }
@@ -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)}`;