@nocobase/portal-template-default 1.0.1 → 1.0.2

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 (78) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +3 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/index.tsx +2 -3
  56. package/src/lib/i18n.ts +161 -0
  57. package/src/lib/nocobase/client.ts +19 -2
  58. package/src/locales/en-US.ts +82 -0
  59. package/src/locales/index.ts +13 -0
  60. package/src/locales/zh-CN.ts +79 -0
  61. package/src/pages/users/create.tsx +98 -0
  62. package/src/pages/users/edit.tsx +92 -0
  63. package/src/pages/users/form-context.ts +57 -0
  64. package/src/pages/users/form-fields.tsx +211 -0
  65. package/src/pages/users/list.tsx +205 -0
  66. package/src/pages/users/show.tsx +187 -0
  67. package/src/pages/users/types.ts +20 -0
  68. package/src/providers/constants.ts +1 -0
  69. package/src/pages/blog-posts/create.tsx +0 -177
  70. package/src/pages/blog-posts/edit.tsx +0 -197
  71. package/src/pages/blog-posts/list.tsx +0 -131
  72. package/src/pages/blog-posts/show.tsx +0 -81
  73. package/src/pages/categories/create.tsx +0 -73
  74. package/src/pages/categories/edit.tsx +0 -73
  75. package/src/pages/categories/index.ts +0 -4
  76. package/src/pages/categories/list.tsx +0 -86
  77. package/src/pages/categories/show.tsx +0 -37
  78. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,98 @@
1
+ import { type HttpError, useTranslate } from "@refinedev/core";
2
+ import { useForm } from "@refinedev/react-hook-form";
3
+ import { useMemo } from "react";
4
+ import { useNavigate } from "react-router";
5
+
6
+ import { CreateView } from "@/components/resources/views/create-view";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Card, CardContent } from "@/components/ui/card";
9
+ import { Form } from "@/components/ui/form";
10
+ import { useAIForm, type AIFormField } from "@/extensions/nocobase-ai";
11
+ import {
12
+ applyAIUserFormValues,
13
+ getAIUserFormFields,
14
+ getAIUserFormValues,
15
+ } from "./form-context";
16
+ import { UserFormFields } from "./form-fields";
17
+ import type { UserFormValues, UserRecord } from "./types";
18
+
19
+ export const UserCreate = () => {
20
+ const translate = useTranslate();
21
+ const navigate = useNavigate();
22
+ const {
23
+ refineCore: { onFinish },
24
+ ...form
25
+ } = useForm<UserRecord, HttpError, UserFormValues>({
26
+ refineCoreProps: {
27
+ redirect: "list",
28
+ },
29
+ defaultValues: {
30
+ nickname: "",
31
+ username: "",
32
+ email: "",
33
+ phone: "",
34
+ password: "",
35
+ },
36
+ });
37
+
38
+ const aiFields = useMemo<AIFormField[]>(
39
+ () => getAIUserFormFields(translate),
40
+ [translate]
41
+ );
42
+ const aiFormRef = useAIForm({
43
+ id: "users-create-form",
44
+ title: translate("users.ai.createForm", { ns: "app" }, "Create user form"),
45
+ fields: aiFields,
46
+ getValues: () => getAIUserFormValues(form.getValues()),
47
+ setValues: (values) => applyAIUserFormValues(form, values),
48
+ });
49
+
50
+ 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>
97
+ );
98
+ };
@@ -0,0 +1,92 @@
1
+ import {
2
+ type HttpError,
3
+ useResourceParams,
4
+ useTranslate,
5
+ } from "@refinedev/core";
6
+ import { useForm } from "@refinedev/react-hook-form";
7
+ import { useMemo } from "react";
8
+ import { useNavigate } from "react-router";
9
+
10
+ import { EditView } from "@/components/resources/views/edit-view";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Card, CardContent } from "@/components/ui/card";
13
+ import { Form } from "@/components/ui/form";
14
+ import { useAIForm, type AIFormField } from "@/extensions/nocobase-ai";
15
+ import {
16
+ applyAIUserFormValues,
17
+ getAIUserFormFields,
18
+ getAIUserFormValues,
19
+ } from "./form-context";
20
+ import { UserFormFields } from "./form-fields";
21
+ import type { UserFormValues, UserRecord } from "./types";
22
+
23
+ export const UserEdit = () => {
24
+ const translate = useTranslate();
25
+ const navigate = useNavigate();
26
+ const { id } = useResourceParams();
27
+ const {
28
+ refineCore: { onFinish },
29
+ ...form
30
+ } = useForm<UserRecord, HttpError, UserFormValues>({
31
+ refineCoreProps: {
32
+ redirect: "list",
33
+ },
34
+ });
35
+
36
+ const aiFields = useMemo<AIFormField[]>(
37
+ () => getAIUserFormFields(translate),
38
+ [translate]
39
+ );
40
+ const aiFormRef = useAIForm({
41
+ id: `users-edit-form-${id ?? "current"}`,
42
+ title: translate("users.ai.editForm", { ns: "app" }, "Edit user form"),
43
+ fields: aiFields,
44
+ getValues: () => getAIUserFormValues(form.getValues()),
45
+ setValues: (values) => applyAIUserFormValues(form, values),
46
+ });
47
+
48
+ 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>
91
+ );
92
+ };
@@ -0,0 +1,57 @@
1
+ import type { useTranslate } from "@refinedev/core";
2
+ import type { UseFormReturn } from "react-hook-form";
3
+
4
+ import { applyReactHookFormValues } from "@/extensions/nocobase-ai/adapters/react-hook-form";
5
+ import type { UserFormValues } from "./types";
6
+
7
+ type Translate = ReturnType<typeof useTranslate>;
8
+
9
+ const AI_EDITABLE_FIELDS = ["nickname", "username", "email", "phone"] as const;
10
+
11
+ export function getAIUserFormValues(values: UserFormValues) {
12
+ return Object.fromEntries(
13
+ AI_EDITABLE_FIELDS.map((name) => [name, values[name] ?? ""])
14
+ );
15
+ }
16
+
17
+ export function applyAIUserFormValues(
18
+ form: Pick<UseFormReturn<UserFormValues>, "setValue">,
19
+ values: Record<string, unknown>
20
+ ) {
21
+ applyReactHookFormValues(
22
+ form,
23
+ Object.fromEntries(
24
+ Object.entries(values).filter(([name]) =>
25
+ AI_EDITABLE_FIELDS.includes(name as (typeof AI_EDITABLE_FIELDS)[number])
26
+ )
27
+ )
28
+ );
29
+ }
30
+
31
+ export function getAIUserFormFields(translate: Translate) {
32
+ return [
33
+ {
34
+ name: "nickname",
35
+ title: translate("users.fields.nickname", { ns: "app" }, "Nickname"),
36
+ type: "string",
37
+ },
38
+ {
39
+ name: "username",
40
+ title: translate("users.fields.username", { ns: "app" }, "Username"),
41
+ type: "string",
42
+ required: true,
43
+ },
44
+ {
45
+ name: "email",
46
+ title: translate("users.fields.email", { ns: "app" }, "Email"),
47
+ type: "email",
48
+ required: true,
49
+ },
50
+ {
51
+ name: "phone",
52
+ title: translate("users.fields.phone", { ns: "app" }, "Phone"),
53
+ type: "string",
54
+ required: true,
55
+ },
56
+ ];
57
+ }
@@ -0,0 +1,211 @@
1
+ import { type useTranslate } from "@refinedev/core";
2
+ import type { UseFormReturn } from "react-hook-form";
3
+
4
+ import {
5
+ FormControl,
6
+ FormDescription,
7
+ FormField,
8
+ FormItem,
9
+ FormLabel,
10
+ FormMessage,
11
+ } from "@/components/ui/form";
12
+ import { Input } from "@/components/ui/input";
13
+ import type { UserFormValues } from "./types";
14
+
15
+ type Translate = ReturnType<typeof useTranslate>;
16
+
17
+ export function UserFormFields({
18
+ form,
19
+ includePassword,
20
+ translate,
21
+ }: {
22
+ form: UseFormReturn<UserFormValues>;
23
+ includePassword?: boolean;
24
+ translate: Translate;
25
+ }) {
26
+ return (
27
+ <>
28
+ <FormField
29
+ control={form.control}
30
+ name="nickname"
31
+ render={({ field }) => (
32
+ <FormItem>
33
+ <FormLabel>
34
+ {translate("users.fields.nickname", { ns: "app" }, "Nickname")}
35
+ </FormLabel>
36
+ <FormControl
37
+ render={
38
+ <Input
39
+ {...field}
40
+ value={field.value ?? ""}
41
+ placeholder={translate(
42
+ "users.form.nickname.placeholder",
43
+ { ns: "app" },
44
+ "Enter a display name"
45
+ )}
46
+ />
47
+ }
48
+ />
49
+ <FormMessage />
50
+ </FormItem>
51
+ )}
52
+ />
53
+
54
+ <FormField
55
+ control={form.control}
56
+ name="username"
57
+ rules={{
58
+ required: translate(
59
+ "users.validation.username",
60
+ { ns: "app" },
61
+ "Username is required"
62
+ ),
63
+ }}
64
+ render={({ field }) => (
65
+ <FormItem>
66
+ <FormLabel>
67
+ {translate("users.fields.username", { ns: "app" }, "Username")}
68
+ </FormLabel>
69
+ <FormControl
70
+ render={
71
+ <Input
72
+ {...field}
73
+ value={field.value ?? ""}
74
+ autoComplete="username"
75
+ placeholder={translate(
76
+ "users.form.username.placeholder",
77
+ { ns: "app" },
78
+ "Enter a unique username"
79
+ )}
80
+ />
81
+ }
82
+ />
83
+ <FormMessage />
84
+ </FormItem>
85
+ )}
86
+ />
87
+
88
+ <FormField
89
+ control={form.control}
90
+ name="email"
91
+ rules={{
92
+ required: translate(
93
+ "users.validation.email",
94
+ { ns: "app" },
95
+ "Email is required"
96
+ ),
97
+ pattern: {
98
+ value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
99
+ message: translate(
100
+ "users.validation.emailFormat",
101
+ { ns: "app" },
102
+ "Enter a valid email address"
103
+ ),
104
+ },
105
+ }}
106
+ render={({ field }) => (
107
+ <FormItem>
108
+ <FormLabel>
109
+ {translate("users.fields.email", { ns: "app" }, "Email")}
110
+ </FormLabel>
111
+ <FormControl
112
+ render={
113
+ <Input
114
+ {...field}
115
+ value={field.value ?? ""}
116
+ type="email"
117
+ autoComplete="email"
118
+ placeholder={translate(
119
+ "users.form.email.placeholder",
120
+ { ns: "app" },
121
+ "Enter an email address"
122
+ )}
123
+ />
124
+ }
125
+ />
126
+ <FormMessage />
127
+ </FormItem>
128
+ )}
129
+ />
130
+
131
+ <FormField
132
+ control={form.control}
133
+ name="phone"
134
+ rules={{
135
+ required: translate(
136
+ "users.validation.phone",
137
+ { ns: "app" },
138
+ "Phone is required"
139
+ ),
140
+ }}
141
+ render={({ field }) => (
142
+ <FormItem>
143
+ <FormLabel>
144
+ {translate("users.fields.phone", { ns: "app" }, "Phone")}
145
+ </FormLabel>
146
+ <FormControl
147
+ render={
148
+ <Input
149
+ {...field}
150
+ value={field.value ?? ""}
151
+ type="tel"
152
+ autoComplete="tel"
153
+ placeholder={translate(
154
+ "users.form.phone.placeholder",
155
+ { ns: "app" },
156
+ "Enter a phone number"
157
+ )}
158
+ />
159
+ }
160
+ />
161
+ <FormMessage />
162
+ </FormItem>
163
+ )}
164
+ />
165
+
166
+ {includePassword ? (
167
+ <FormField
168
+ control={form.control}
169
+ name="password"
170
+ rules={{
171
+ required: translate(
172
+ "users.validation.password",
173
+ { ns: "app" },
174
+ "Password is required"
175
+ ),
176
+ }}
177
+ render={({ field }) => (
178
+ <FormItem>
179
+ <FormLabel>
180
+ {translate("users.fields.password", { ns: "app" }, "Password")}
181
+ </FormLabel>
182
+ <FormControl
183
+ render={
184
+ <Input
185
+ {...field}
186
+ value={field.value ?? ""}
187
+ type="password"
188
+ autoComplete="new-password"
189
+ placeholder={translate(
190
+ "users.form.password.placeholder",
191
+ { ns: "app" },
192
+ "Set an initial password"
193
+ )}
194
+ />
195
+ }
196
+ />
197
+ <FormDescription>
198
+ {translate(
199
+ "users.form.password.aiNotice",
200
+ { ns: "app" },
201
+ "Passwords are never included in AI page context or Form filler."
202
+ )}
203
+ </FormDescription>
204
+ <FormMessage />
205
+ </FormItem>
206
+ )}
207
+ />
208
+ ) : null}
209
+ </>
210
+ );
211
+ }
@@ -0,0 +1,205 @@
1
+ import { useGetLocale, useTranslate } from "@refinedev/core";
2
+ import { useTable } from "@refinedev/react-table";
3
+ import { createColumnHelper } from "@tanstack/react-table";
4
+ import { Eye, Pencil, Trash2 } from "lucide-react";
5
+ import { useMemo } from "react";
6
+
7
+ import { DataTable } from "@/components/data-table/data-table";
8
+ import { DeleteButton } from "@/components/resources/buttons/delete";
9
+ import { EditButton } from "@/components/resources/buttons/edit";
10
+ import { ShowButton } from "@/components/resources/buttons/show";
11
+ import { ListView } from "@/components/resources/views/list-view";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { useAIPageElementHandle } from "@/extensions/nocobase-ai";
14
+ import { resolveTranslatableText } from "@/lib/i18n";
15
+ import type { Role } from "@/lib/nocobase/acl";
16
+ import type { UserRecord } from "./types";
17
+
18
+ const getRoleLabel = (role: Role) =>
19
+ resolveTranslatableText(role.title || role.name, { ns: "starter" });
20
+
21
+ const isRootUser = (record: UserRecord) =>
22
+ record.roles?.some((role) => role.name === "root") ?? false;
23
+
24
+ export const UserList = () => {
25
+ const translate = useTranslate();
26
+ const getLocale = useGetLocale();
27
+ const locale = getLocale();
28
+
29
+ const columns = useMemo(() => {
30
+ const columnHelper = createColumnHelper<UserRecord>();
31
+
32
+ return [
33
+ columnHelper.accessor("nickname", {
34
+ id: "nickname",
35
+ header: translate("users.fields.nickname", { ns: "app" }, "Nickname"),
36
+ enableSorting: true,
37
+ cell: ({ row, getValue }) =>
38
+ getValue() || row.original.username || row.original.email || "-",
39
+ }),
40
+ columnHelper.accessor("username", {
41
+ id: "username",
42
+ header: translate("users.fields.username", { ns: "app" }, "Username"),
43
+ enableSorting: true,
44
+ cell: ({ getValue }) => getValue() || "-",
45
+ }),
46
+ columnHelper.accessor("email", {
47
+ id: "email",
48
+ header: translate("users.fields.email", { ns: "app" }, "Email"),
49
+ enableSorting: true,
50
+ cell: ({ getValue }) => getValue() || "-",
51
+ }),
52
+ columnHelper.accessor("phone", {
53
+ id: "phone",
54
+ header: translate("users.fields.phone", { ns: "app" }, "Phone"),
55
+ enableSorting: true,
56
+ cell: ({ getValue }) => getValue() || "-",
57
+ }),
58
+ columnHelper.accessor("roles", {
59
+ id: "roles",
60
+ header: translate("users.fields.roles", { ns: "app" }, "Roles"),
61
+ enableSorting: false,
62
+ cell: ({ getValue }) => {
63
+ 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
+ );
75
+ },
76
+ }),
77
+ columnHelper.accessor("createdAt", {
78
+ id: "createdAt",
79
+ header: translate(
80
+ "users.fields.createdAt",
81
+ { ns: "app" },
82
+ "Created at"
83
+ ),
84
+ enableSorting: true,
85
+ cell: ({ getValue }) => {
86
+ const value = getValue();
87
+ return value
88
+ ? new Intl.DateTimeFormat(locale, {
89
+ dateStyle: "medium",
90
+ }).format(new Date(value))
91
+ : "-";
92
+ },
93
+ }),
94
+ columnHelper.display({
95
+ id: "actions",
96
+ header: translate("users.fields.actions", { ns: "app" }, "Actions"),
97
+ cell: ({ row }) => (
98
+ <div className="flex items-center gap-1">
99
+ <EditButton
100
+ recordItemId={row.original.id}
101
+ variant="ghost"
102
+ size="icon"
103
+ aria-label={translate(
104
+ "users.actions.edit",
105
+ { ns: "app" },
106
+ "Edit user"
107
+ )}
108
+ title={translate(
109
+ "users.actions.edit",
110
+ { ns: "app" },
111
+ "Edit user"
112
+ )}
113
+ >
114
+ <Pencil />
115
+ </EditButton>
116
+ <ShowButton
117
+ recordItemId={row.original.id}
118
+ variant="ghost"
119
+ size="icon"
120
+ aria-label={translate(
121
+ "users.actions.view",
122
+ { ns: "app" },
123
+ "View user"
124
+ )}
125
+ title={translate(
126
+ "users.actions.view",
127
+ { ns: "app" },
128
+ "View user"
129
+ )}
130
+ >
131
+ <Eye />
132
+ </ShowButton>
133
+ {isRootUser(row.original) ? null : (
134
+ <DeleteButton
135
+ recordItemId={row.original.id}
136
+ variant="ghost"
137
+ size="icon"
138
+ className="text-destructive hover:text-destructive"
139
+ aria-label={translate(
140
+ "users.actions.delete",
141
+ { ns: "app" },
142
+ "Delete user"
143
+ )}
144
+ title={translate(
145
+ "users.actions.delete",
146
+ { ns: "app" },
147
+ "Delete user"
148
+ )}
149
+ >
150
+ <Trash2 />
151
+ </DeleteButton>
152
+ )}
153
+ </div>
154
+ ),
155
+ enableSorting: false,
156
+ size: 144,
157
+ }),
158
+ ];
159
+ }, [locale, translate]);
160
+
161
+ const table = useTable<UserRecord>({
162
+ columns,
163
+ refineCoreProps: {
164
+ syncWithLocation: true,
165
+ meta: {
166
+ appends: ["roles"],
167
+ },
168
+ sorters: {
169
+ initial: [{ field: "createdAt", order: "desc" }],
170
+ },
171
+ },
172
+ });
173
+
174
+ const tableContext = useAIPageElementHandle({
175
+ id: "users-table",
176
+ title: translate("users.ai.table", { ns: "app" }, "Users table"),
177
+ kind: "table",
178
+ getContext: () => ({
179
+ resource: "users",
180
+ page: table.refineCore.currentPage,
181
+ pageSize: table.refineCore.pageSize,
182
+ total: table.refineCore.tableQuery.data?.total ?? 0,
183
+ rows: (table.refineCore.tableQuery.data?.data ?? []).map((record) => ({
184
+ id: record.id,
185
+ nickname: record.nickname,
186
+ username: record.username,
187
+ email: record.email,
188
+ phone: record.phone,
189
+ roles: record.roles?.map((role) => ({
190
+ name: role.name,
191
+ title: getRoleLabel(role),
192
+ })),
193
+ createdAt: record.createdAt,
194
+ })),
195
+ }),
196
+ });
197
+
198
+ return (
199
+ <ListView>
200
+ <div ref={tableContext.ref}>
201
+ <DataTable table={table} />
202
+ </div>
203
+ </ListView>
204
+ );
205
+ };