@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,187 @@
1
+ import {
2
+ useGetLocale,
3
+ useResourceParams,
4
+ useShow,
5
+ useTranslate,
6
+ } from "@refinedev/core";
7
+
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";
17
+ import { Separator } from "@/components/ui/separator";
18
+ import { useAIPageElementHandle } from "@/extensions/nocobase-ai";
19
+ import { resolveTranslatableText } from "@/lib/i18n";
20
+ import type { UserRecord } from "./types";
21
+
22
+ export const UserShow = () => {
23
+ const translate = useTranslate();
24
+ const getLocale = useGetLocale();
25
+ const locale = getLocale();
26
+ const { id } = useResourceParams();
27
+ const { result: record } = useShow<UserRecord>({
28
+ meta: {
29
+ appends: ["roles"],
30
+ },
31
+ });
32
+
33
+ const displayName =
34
+ record?.nickname ||
35
+ record?.username ||
36
+ record?.email ||
37
+ 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 detailContext = useAIPageElementHandle({
43
+ id: `users-detail-${id ?? "current"}`,
44
+ title: `${translate(
45
+ "users.ai.detail",
46
+ { ns: "app" },
47
+ "User details"
48
+ )}: ${displayName}`,
49
+ kind: "detail",
50
+ getContext: () => ({
51
+ resource: "users",
52
+ record: {
53
+ id: record?.id,
54
+ nickname: record?.nickname,
55
+ username: record?.username,
56
+ email: record?.email,
57
+ phone: record?.phone,
58
+ roles: roleLabels,
59
+ createdAt: record?.createdAt,
60
+ updatedAt: record?.updatedAt,
61
+ },
62
+ }),
63
+ });
64
+
65
+ const formatDate = (value?: string) =>
66
+ value
67
+ ? new Intl.DateTimeFormat(locale, {
68
+ dateStyle: "medium",
69
+ timeStyle: "short",
70
+ }).format(new Date(value))
71
+ : "-";
72
+
73
+ 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(
127
+ "users.detail.noRoles",
128
+ { ns: "app" },
129
+ "No assigned roles"
130
+ )}
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",
154
+ { ns: "app" },
155
+ "Updated at"
156
+ ),
157
+ formatDate(record?.updatedAt),
158
+ ],
159
+ ]}
160
+ />
161
+ </CardContent>
162
+ </Card>
163
+ </ShowView>
164
+ );
165
+ };
166
+
167
+ function DetailSection({
168
+ title,
169
+ items,
170
+ }: {
171
+ title: string;
172
+ items: Array<[label: string, value: string | number]>;
173
+ }) {
174
+ return (
175
+ <section className="space-y-3">
176
+ <h3 className="text-sm font-medium">{title}</h3>
177
+ <dl className="grid gap-4 sm:grid-cols-2">
178
+ {items.map(([label, value]) => (
179
+ <div key={label} className="space-y-1">
180
+ <dt className="text-xs text-muted-foreground">{label}</dt>
181
+ <dd className="text-sm font-medium">{value}</dd>
182
+ </div>
183
+ ))}
184
+ </dl>
185
+ </section>
186
+ );
187
+ }
@@ -0,0 +1,20 @@
1
+ import type { Role } from "@/lib/nocobase/acl";
2
+
3
+ export type UserRecord = {
4
+ id: string | number;
5
+ nickname?: string;
6
+ username?: string;
7
+ email?: string;
8
+ phone?: string;
9
+ roles?: Role[];
10
+ createdAt?: string;
11
+ updatedAt?: string;
12
+ };
13
+
14
+ export type UserFormValues = {
15
+ nickname: string;
16
+ username: string;
17
+ email: string;
18
+ phone: string;
19
+ password?: string;
20
+ };
@@ -36,5 +36,6 @@ export const API_URL = toProxyRelativeUrl(rawApiUrl, proxyTarget);
36
36
  export const API_ORIGIN = getDefaultProxyTarget(rawApiUrl);
37
37
  export const NOCOBASE_TOKEN_KEY = "nocobase-auth-token";
38
38
  export const NOCOBASE_ROLE_KEY = "nocobase-auth-role";
39
+ export const NOCOBASE_LOCALE_KEY = "nocobase-locale";
39
40
  export const NOCOBASE_AUTHENTICATOR =
40
41
  import.meta.env.NOCOBASE_AUTHENTICATOR ?? "basic";
@@ -1,177 +0,0 @@
1
- import { useForm } from "@refinedev/react-hook-form";
2
- import { useSelect } from "@refinedev/core";
3
- import { useNavigate } from "react-router";
4
- import { Textarea } from "@/components/ui/textarea";
5
-
6
- import { CreateView } from "@/components/resources/views/create-view";
7
- import { Button } from "@/components/ui/button";
8
- import { Input } from "@/components/ui/input";
9
- import {
10
- Select,
11
- SelectContent,
12
- SelectItem,
13
- SelectTrigger,
14
- SelectValue,
15
- } from "@/components/ui/select";
16
- import {
17
- Form,
18
- FormControl,
19
- FormField,
20
- FormItem,
21
- FormLabel,
22
- FormMessage,
23
- } from "@/components/ui/form";
24
-
25
- export const BlogPostCreate = () => {
26
- const navigate = useNavigate();
27
-
28
- const {
29
- refineCore: { onFinish },
30
- ...form
31
- } = useForm({
32
- refineCoreProps: {},
33
- });
34
-
35
- const { options: categoryOptions } = useSelect({
36
- resource: "categories",
37
- optionLabel: "title",
38
- optionValue: "id",
39
- pagination: {
40
- currentPage: 1,
41
- pageSize: 100,
42
- },
43
- });
44
-
45
- function onSubmit(values: Record<string, string>) {
46
- onFinish(values);
47
- }
48
-
49
- return (
50
- <CreateView>
51
- <Form {...form}>
52
- <form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
53
- <FormField
54
- control={form.control}
55
- name="title"
56
- rules={{ required: "Title is required" }}
57
- render={({ field }) => (
58
- <FormItem>
59
- <FormLabel>Title</FormLabel>
60
- <FormControl
61
- render={<Input
62
- {...field}
63
- value={field.value || ""}
64
- placeholder="Enter title"
65
- />}
66
- />
67
- <FormMessage />
68
- </FormItem>
69
- )}
70
- />
71
-
72
- <FormField
73
- control={form.control}
74
- name="content"
75
- rules={{ required: "Content is required" }}
76
- render={({ field }) => (
77
- <FormItem>
78
- <FormLabel>Content</FormLabel>
79
- <FormControl
80
- render={<Textarea
81
- {...field}
82
- value={field.value || ""}
83
- placeholder="Enter content"
84
- rows={10}
85
- />}
86
- />
87
- <FormMessage />
88
- </FormItem>
89
- )}
90
- />
91
-
92
- <FormField
93
- control={form.control}
94
- name="categoryId"
95
- rules={{ required: "Category is required" }}
96
- render={({ field }) => (
97
- <FormItem>
98
- <FormLabel>Category</FormLabel>
99
- <Select
100
- items={categoryOptions?.map((option) => ({
101
- label: option.label,
102
- value: option.value.toString(),
103
- }))}
104
- onValueChange={field.onChange}
105
- value={field.value?.toString() || ""}
106
- >
107
- <FormControl
108
- render={<SelectTrigger>
109
- <SelectValue placeholder="Select a category" />
110
- </SelectTrigger>}
111
- />
112
- <SelectContent>
113
- {categoryOptions?.map((option) => (
114
- <SelectItem key={option.value} value={option.value.toString()}>
115
- {option.label}
116
- </SelectItem>
117
- ))}
118
- </SelectContent>
119
- </Select>
120
- <FormMessage />
121
- </FormItem>
122
- )}
123
- />
124
-
125
- <FormField
126
- control={form.control}
127
- name="status"
128
- rules={{ required: "Status is required" }}
129
- render={({ field }) => (
130
- <FormItem>
131
- <FormLabel>Status</FormLabel>
132
- <Select
133
- items={{
134
- draft: "Draft",
135
- published: "Published",
136
- rejected: "Rejected",
137
- }}
138
- onValueChange={field.onChange}
139
- defaultValue={"draft"}
140
- >
141
- <FormControl
142
- render={<SelectTrigger>
143
- <SelectValue placeholder="Select status" />
144
- </SelectTrigger>}
145
- />
146
- <SelectContent>
147
- <SelectItem value="draft">Draft</SelectItem>
148
- <SelectItem value="published">Published</SelectItem>
149
- <SelectItem value="rejected">Rejected</SelectItem>
150
- </SelectContent>
151
- </Select>
152
- <FormMessage />
153
- </FormItem>
154
- )}
155
- />
156
-
157
- <div className="flex gap-2">
158
- <Button
159
- type="submit"
160
- {...form.saveButtonProps}
161
- disabled={form.formState.isSubmitting}
162
- >
163
- {form.formState.isSubmitting ? "Creating..." : "Create"}
164
- </Button>
165
- <Button
166
- type="button"
167
- variant="outline"
168
- onClick={() => navigate(-1)}
169
- >
170
- Cancel
171
- </Button>
172
- </div>
173
- </form>
174
- </Form>
175
- </CreateView>
176
- );
177
- };
@@ -1,197 +0,0 @@
1
- import { useForm } from "@refinedev/react-hook-form";
2
- import { useSelect } from "@refinedev/core";
3
- import { useEffect } from "react";
4
- import { useNavigate } from "react-router";
5
- import { Textarea } from "@/components/ui/textarea";
6
-
7
- import { EditView } from "@/components/resources/views/edit-view";
8
- import { Button } from "@/components/ui/button";
9
- import { Input } from "@/components/ui/input";
10
- import {
11
- Select,
12
- SelectContent,
13
- SelectItem,
14
- SelectTrigger,
15
- SelectValue,
16
- } from "@/components/ui/select";
17
- import {
18
- Form,
19
- FormControl,
20
- FormField,
21
- FormItem,
22
- FormLabel,
23
- FormMessage,
24
- } from "@/components/ui/form";
25
-
26
- export const BlogPostEdit = () => {
27
- const navigate = useNavigate();
28
-
29
- const {
30
- refineCore: { onFinish, query },
31
- ...form
32
- } = useForm({
33
- refineCoreProps: {
34
- meta: {
35
- appends: ["category"],
36
- },
37
- },
38
- });
39
-
40
- const blogPostsData = query?.data?.data;
41
- const defaultCategoryId =
42
- blogPostsData?.categoryId ?? blogPostsData?.category?.id;
43
-
44
- useEffect(() => {
45
- if (defaultCategoryId != null && !form.getValues("categoryId")) {
46
- form.setValue("categoryId", defaultCategoryId.toString(), {
47
- shouldDirty: false,
48
- });
49
- }
50
- }, [defaultCategoryId, form]);
51
-
52
- const { options: categoryOptions } = useSelect({
53
- resource: "categories",
54
- defaultValue: defaultCategoryId?.toString(),
55
- optionLabel: "title",
56
- optionValue: "id",
57
- pagination: {
58
- currentPage: 1,
59
- pageSize: 100,
60
- },
61
- });
62
-
63
- function onSubmit(values: Record<string, string>) {
64
- onFinish(values);
65
- }
66
-
67
- return (
68
- <EditView>
69
- <Form {...form}>
70
- <form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
71
- <FormField
72
- control={form.control}
73
- name="title"
74
- rules={{ required: "Title is required" }}
75
- render={({ field }) => (
76
- <FormItem>
77
- <FormLabel>Title</FormLabel>
78
- <FormControl
79
- render={<Input
80
- {...field}
81
- value={field.value || ""}
82
- placeholder="Enter title"
83
- />}
84
- />
85
- <FormMessage />
86
- </FormItem>
87
- )}
88
- />
89
-
90
- <FormField
91
- control={form.control}
92
- name="content"
93
- rules={{ required: "Content is required" }}
94
- render={({ field }) => (
95
- <FormItem>
96
- <FormLabel>Content</FormLabel>
97
- <FormControl
98
- render={<Textarea
99
- {...field}
100
- value={field.value || ""}
101
- placeholder="Enter content"
102
- rows={10}
103
- />}
104
- />
105
- <FormMessage />
106
- </FormItem>
107
- )}
108
- />
109
-
110
- <FormField
111
- control={form.control}
112
- name="categoryId"
113
- rules={{ required: "Category is required" }}
114
- render={({ field }) => {
115
- const selectedCategoryId = field.value ?? defaultCategoryId;
116
-
117
- return <FormItem>
118
- <FormLabel>Category</FormLabel>
119
- <Select
120
- items={categoryOptions?.map((option) => ({
121
- label: option.label,
122
- value: option.value.toString(),
123
- }))}
124
- onValueChange={field.onChange}
125
- value={selectedCategoryId?.toString() || ""}
126
- >
127
- <FormControl
128
- render={<SelectTrigger>
129
- <SelectValue placeholder="Select a category" />
130
- </SelectTrigger>}
131
- />
132
- <SelectContent>
133
- {categoryOptions?.map((option) => (
134
- <SelectItem key={option.value} value={option.value.toString()}>
135
- {option.label}
136
- </SelectItem>
137
- ))}
138
- </SelectContent>
139
- </Select>
140
- <FormMessage />
141
- </FormItem>;
142
- }}
143
- />
144
-
145
- <FormField
146
- control={form.control}
147
- name="status"
148
- rules={{ required: "Status is required" }}
149
- render={({ field }) => (
150
- <FormItem>
151
- <FormLabel>Status</FormLabel>
152
- <Select
153
- items={{
154
- draft: "Draft",
155
- published: "Published",
156
- rejected: "Rejected",
157
- }}
158
- onValueChange={field.onChange}
159
- value={field.value || ""}
160
- >
161
- <FormControl
162
- render={<SelectTrigger>
163
- <SelectValue placeholder="Select status" />
164
- </SelectTrigger>}
165
- />
166
- <SelectContent>
167
- <SelectItem value="draft">Draft</SelectItem>
168
- <SelectItem value="published">Published</SelectItem>
169
- <SelectItem value="rejected">Rejected</SelectItem>
170
- </SelectContent>
171
- </Select>
172
- <FormMessage />
173
- </FormItem>
174
- )}
175
- />
176
-
177
- <div className="flex gap-2">
178
- <Button
179
- type="submit"
180
- {...form.saveButtonProps}
181
- disabled={form.formState.isSubmitting}
182
- >
183
- {form.formState.isSubmitting ? "Updating..." : "Update"}
184
- </Button>
185
- <Button
186
- type="button"
187
- variant="outline"
188
- onClick={() => navigate(-1)}
189
- >
190
- Cancel
191
- </Button>
192
- </div>
193
- </form>
194
- </Form>
195
- </EditView>
196
- );
197
- };