@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
@@ -1,11 +1,62 @@
1
1
  export const starter = {
2
+ "shell.workspace": "AI 应用工作区",
3
+ "shell.footer.freedom": "AI 自由构建。",
4
+ "shell.footer.reliability": "NocoBase 保障可靠运行。",
5
+ "auth.signOut": "退出登录",
6
+ "auth.signingOut": "正在退出...",
7
+ "buttons.apply": "应用",
8
+ "buttons.clear": "清除",
9
+ "buttons.undo": "撤销",
10
+ "pages.error.title": "页面不存在",
11
+ "pages.error.description": "你访问的页面不存在。",
12
+ "pages.error.backHome": "返回首页",
13
+ "table.filter.text.placeholder": "输入筛选内容...",
14
+ "table.filter.numeric.placeholder": "输入筛选数值...",
15
+ "table.filter.combobox.placeholder": "请选择...",
16
+ "table.filter.combobox.search": "搜索...",
17
+ "table.filter.combobox.noResults": "没有找到结果。",
18
+ "table.filter.operator.placeholder": "搜索操作符...",
19
+ "table.filter.operator.noResults": "没有找到操作符。",
20
+ "table.filter.operator.eq": "等于",
21
+ "table.filter.operator.ne": "不等于",
22
+ "table.filter.operator.lt": "小于",
23
+ "table.filter.operator.gt": "大于",
24
+ "table.filter.operator.lte": "小于或等于",
25
+ "table.filter.operator.gte": "大于或等于",
26
+ "table.filter.operator.in": "包含于数组",
27
+ "table.filter.operator.nin": "不包含于数组",
28
+ "table.filter.operator.ina": "包含于数组(区分大小写)",
29
+ "table.filter.operator.nina": "不包含于数组(区分大小写)",
30
+ "table.filter.operator.contains": "包含",
31
+ "table.filter.operator.ncontains": "不包含",
32
+ "table.filter.operator.containss": "包含(区分大小写)",
33
+ "table.filter.operator.ncontainss": "不包含(区分大小写)",
34
+ "table.filter.operator.between": "介于",
35
+ "table.filter.operator.nbetween": "不介于",
36
+ "table.filter.operator.null": "为空",
37
+ "table.filter.operator.nnull": "不为空",
38
+ "table.filter.operator.startswith": "开头是",
39
+ "table.filter.operator.nstartswith": "开头不是",
40
+ "table.filter.operator.startswiths": "开头是(区分大小写)",
41
+ "table.filter.operator.nstartswiths": "开头不是(区分大小写)",
42
+ "table.filter.operator.endswith": "结尾是",
43
+ "table.filter.operator.nendswith": "结尾不是",
44
+ "table.filter.operator.endswiths": "结尾是(区分大小写)",
45
+ "table.filter.operator.nendswiths": "结尾不是(区分大小写)",
46
+ "table.filter.operator.eqs": "等于(区分大小写)",
47
+ "table.filter.operator.nes": "不等于(区分大小写)",
2
48
  "buttons.create": "新建",
3
49
  "buttons.edit": "编辑",
4
50
  "buttons.show": "查看",
5
51
  "buttons.delete": "删除",
6
52
  "buttons.refresh": "刷新",
7
53
  "buttons.cancel": "取消",
54
+ "buttons.close": "关闭",
8
55
  "buttons.confirm": "确定要执行此操作吗?",
56
+ "unsavedChanges.title": "放弃未保存的修改?",
57
+ "unsavedChanges.description": "当前修改尚未保存,离开后这些修改将会丢失。",
58
+ "unsavedChanges.stay": "继续编辑",
59
+ "unsavedChanges.leave": "放弃修改",
9
60
  "views.create.title": "新建{{resource}}",
10
61
  "views.create.description": "在 NocoBase 工作区中添加新的{{resource}}。",
11
62
  "views.edit.title": "编辑{{resource}}",
@@ -49,6 +100,11 @@ export const app = {
49
100
  "users.actions.view": "查看用户",
50
101
  "users.actions.edit": "编辑用户",
51
102
  "users.actions.delete": "删除用户",
103
+ "users.drawer.create.title": "新建用户",
104
+ "users.drawer.create.description": "添加可以登录此 NocoBase 应用的用户。",
105
+ "users.drawer.edit.title": "编辑用户",
106
+ "users.drawer.edit.description": "更新用户的身份和联系方式。",
107
+ "users.drawer.show.description": "查看用户的身份、联系方式和关联角色。",
52
108
  "users.form.create.submit": "创建用户",
53
109
  "users.form.create.submitting": "正在创建...",
54
110
  "users.form.edit.submit": "保存修改",
@@ -72,8 +128,29 @@ export const app = {
72
128
  "users.detail.timestamps": "时间信息",
73
129
  "users.detail.noRoles": "尚未分配角色",
74
130
  "users.detail.unnamed": "未命名用户",
131
+ "users.detail.loadError.title": "无法加载用户",
132
+ "users.detail.loadError.description":
133
+ "该用户可能已不存在,或者你没有权限查看此用户。",
75
134
  "users.ai.table": "用户表格",
76
135
  "users.ai.detail": "用户详情",
77
136
  "users.ai.createForm": "新建用户表单",
78
137
  "users.ai.editForm": "编辑用户表单",
138
+ "common.yes": "是",
139
+ "common.no": "否",
140
+ "roles.drawer.description": "查看与当前用户关联的角色信息。",
141
+ "roles.fields.name": "名称",
142
+ "roles.fields.title": "标题",
143
+ "roles.fields.description": "描述",
144
+ "roles.fields.default": "默认角色",
145
+ "roles.fields.hidden": "隐藏",
146
+ "roles.fields.allowConfigure": "允许配置",
147
+ "roles.fields.createdAt": "创建时间",
148
+ "roles.fields.updatedAt": "更新时间",
149
+ "roles.detail.identity": "角色标识",
150
+ "roles.detail.settings": "角色设置",
151
+ "roles.detail.actions": "允许的操作",
152
+ "roles.detail.timestamps": "时间信息",
153
+ "roles.detail.unavailable":
154
+ "无法加载更多角色详情,当前展示用户关联数据中已有的信息。",
155
+ "roles.ai.detail": "角色详情",
79
156
  } as const;
@@ -1,30 +1,67 @@
1
1
  import { type HttpError, useTranslate } from "@refinedev/core";
2
2
  import { useForm } from "@refinedev/react-hook-form";
3
3
  import { useMemo } from "react";
4
- import { useNavigate } from "react-router";
5
4
 
6
- import { CreateView } from "@/components/resources/views/create-view";
7
5
  import { Button } from "@/components/ui/button";
8
- import { Card, CardContent } from "@/components/ui/card";
9
6
  import { Form } from "@/components/ui/form";
10
7
  import { useAIForm, type AIFormField } from "@/extensions/nocobase-ai";
8
+ import {
9
+ RouteDrawer,
10
+ RouteDrawerFooter,
11
+ useRefineUnsavedChangesGuard,
12
+ useRouteSurfaceClose,
13
+ } from "@/extensions/nocobase-route-surfaces";
11
14
  import {
12
15
  applyAIUserFormValues,
13
16
  getAIUserFormFields,
14
17
  getAIUserFormValues,
15
18
  } from "./form-context";
16
19
  import { UserFormFields } from "./form-fields";
20
+ import { userRoutes } from "./routes";
17
21
  import type { UserFormValues, UserRecord } from "./types";
18
22
 
19
23
  export const UserCreate = () => {
20
24
  const translate = useTranslate();
21
- const navigate = useNavigate();
25
+ const { beforeClose, confirmation } = useRefineUnsavedChangesGuard();
26
+
27
+ return (
28
+ <>
29
+ <RouteDrawer
30
+ title={translate(
31
+ "users.drawer.create.title",
32
+ { ns: "app" },
33
+ "Create user"
34
+ )}
35
+ description={translate(
36
+ "users.drawer.create.description",
37
+ { ns: "app" },
38
+ "Add a user who can sign in to this NocoBase application."
39
+ )}
40
+ closeLabel={translate("buttons.close", "Close")}
41
+ closeTo={userRoutes.list}
42
+ beforeClose={beforeClose}
43
+ >
44
+ <UserCreateForm />
45
+ </RouteDrawer>
46
+ {confirmation}
47
+ </>
48
+ );
49
+ };
50
+
51
+ function UserCreateForm() {
52
+ const translate = useTranslate();
53
+ const close = useRouteSurfaceClose();
22
54
  const {
23
55
  refineCore: { onFinish },
24
56
  ...form
25
57
  } = useForm<UserRecord, HttpError, UserFormValues>({
26
58
  refineCoreProps: {
27
- redirect: "list",
59
+ resource: "users",
60
+ action: "create",
61
+ redirect: false,
62
+ onMutationSuccess: () => {
63
+ void close({ skipBeforeClose: true });
64
+ },
28
65
  },
29
66
  defaultValues: {
30
67
  nickname: "",
@@ -34,7 +71,6 @@ export const UserCreate = () => {
34
71
  password: "",
35
72
  },
36
73
  });
37
-
38
74
  const aiFields = useMemo<AIFormField[]>(
39
75
  () => getAIUserFormFields(translate),
40
76
  [translate]
@@ -48,51 +84,38 @@ export const UserCreate = () => {
48
84
  });
49
85
 
50
86
  return (
51
- <CreateView>
52
- <Card className="max-w-3xl">
53
- <CardContent>
54
- <Form {...form}>
55
- <form
56
- ref={aiFormRef}
57
- onSubmit={form.handleSubmit((values) => onFinish(values))}
58
- className="resource-form"
59
- >
60
- <UserFormFields
61
- form={form}
62
- includePassword
63
- translate={translate}
64
- />
65
-
66
- <div className="flex flex-wrap gap-2">
67
- <Button
68
- type="submit"
69
- {...form.saveButtonProps}
70
- disabled={form.formState.isSubmitting}
71
- >
72
- {form.formState.isSubmitting
73
- ? translate(
74
- "users.form.create.submitting",
75
- { ns: "app" },
76
- "Creating..."
77
- )
78
- : translate(
79
- "users.form.create.submit",
80
- { ns: "app" },
81
- "Create user"
82
- )}
83
- </Button>
84
- <Button
85
- type="button"
86
- variant="outline"
87
- onClick={() => navigate(-1)}
88
- >
89
- {translate("users.form.cancel", { ns: "app" }, "Cancel")}
90
- </Button>
91
- </div>
92
- </form>
93
- </Form>
94
- </CardContent>
95
- </Card>
96
- </CreateView>
87
+ <Form {...form}>
88
+ <form
89
+ ref={aiFormRef}
90
+ onSubmit={form.handleSubmit((values) => onFinish(values))}
91
+ className="flex min-h-0 flex-1 flex-col"
92
+ >
93
+ <div className="resource-form min-h-0 flex-1 overflow-y-auto px-5 py-5">
94
+ <UserFormFields form={form} includePassword translate={translate} />
95
+ </div>
96
+ <RouteDrawerFooter className="flex-row justify-end">
97
+ <Button type="button" variant="outline" onClick={() => void close()}>
98
+ {translate("users.form.cancel", { ns: "app" }, "Cancel")}
99
+ </Button>
100
+ <Button
101
+ type="submit"
102
+ {...form.saveButtonProps}
103
+ disabled={form.formState.isSubmitting}
104
+ >
105
+ {form.formState.isSubmitting
106
+ ? translate(
107
+ "users.form.create.submitting",
108
+ { ns: "app" },
109
+ "Creating..."
110
+ )
111
+ : translate(
112
+ "users.form.create.submit",
113
+ { ns: "app" },
114
+ "Create user"
115
+ )}
116
+ </Button>
117
+ </RouteDrawerFooter>
118
+ </form>
119
+ </Form>
97
120
  );
98
- };
121
+ }
@@ -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
+ }