@nocobase/portal-template-default 1.0.0 → 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 (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,279 @@
1
+ import {
2
+ API_ORIGIN,
3
+ API_URL,
4
+ NOCOBASE_AUTHENTICATOR,
5
+ NOCOBASE_LOCALE_KEY,
6
+ NOCOBASE_ROLE_KEY,
7
+ NOCOBASE_TOKEN_KEY,
8
+ } from "@/providers/constants";
9
+ import { getNocoBaseErrorMessage, NocoBaseHttpError } from "./error";
10
+
11
+ type QueryValue =
12
+ | string
13
+ | number
14
+ | boolean
15
+ | null
16
+ | undefined
17
+ | Array<string | number | boolean>;
18
+
19
+ export type NocoBaseRequestOptions = {
20
+ method?: "GET" | "POST" | "PUT" | "DELETE";
21
+ query?: Record<string, QueryValue>;
22
+ body?: unknown;
23
+ signal?: AbortSignal;
24
+ token?: string;
25
+ role?: string;
26
+ includeRole?: boolean;
27
+ includeAuthenticator?: boolean;
28
+ withAclMeta?: boolean;
29
+ headers?: Record<string, string>;
30
+ accept?: "json" | "stream";
31
+ unwrap?: "data" | "deep-data" | "none";
32
+ };
33
+
34
+ const getBrowserLocale = () =>
35
+ typeof navigator === "undefined" ? undefined : navigator.language;
36
+
37
+ const getClientTimezone = () => {
38
+ const offsetMinutes = -new Date().getTimezoneOffset();
39
+ const sign = offsetMinutes >= 0 ? "+" : "-";
40
+ const absoluteMinutes = Math.abs(offsetMinutes);
41
+ const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, "0");
42
+ const minutes = String(absoluteMinutes % 60).padStart(2, "0");
43
+ return `${sign}${hours}:${minutes}`;
44
+ };
45
+
46
+ const unwrapPayload = (
47
+ payload: unknown,
48
+ mode: NocoBaseRequestOptions["unwrap"]
49
+ ) => {
50
+ if (mode === "none") return payload;
51
+ if (!payload || typeof payload !== "object") return payload;
52
+ const data = (payload as { data?: unknown }).data;
53
+ if (mode === "deep-data" && data && typeof data === "object") {
54
+ return (data as { data?: unknown }).data ?? data;
55
+ }
56
+ return data ?? payload;
57
+ };
58
+
59
+ export class NocoBaseClient {
60
+ private readonly apiOrigin?: string;
61
+
62
+ constructor(
63
+ private readonly apiUrl = API_URL,
64
+ apiOrigin = getUrlOrigin(apiUrl) ?? API_ORIGIN
65
+ ) {
66
+ this.apiOrigin = apiOrigin;
67
+ }
68
+
69
+ getApiUrl() {
70
+ return this.apiUrl;
71
+ }
72
+
73
+ resolveUrl(value: string) {
74
+ if (!value || /^[a-z][a-z\d+.-]*:/i.test(value)) return value;
75
+ const base =
76
+ this.apiOrigin ??
77
+ (typeof window === "undefined" ? undefined : window.location.origin);
78
+ if (!base) return value;
79
+ try {
80
+ return new URL(value, `${base.replace(/\/$/, "")}/`).toString();
81
+ } catch {
82
+ return value;
83
+ }
84
+ }
85
+
86
+ getToken() {
87
+ return (
88
+ (typeof localStorage === "undefined"
89
+ ? undefined
90
+ : localStorage.getItem(NOCOBASE_TOKEN_KEY) ?? undefined) ??
91
+ import.meta.env.NOCOBASE_API_TOKEN
92
+ );
93
+ }
94
+
95
+ setToken(token?: string | null) {
96
+ if (typeof localStorage === "undefined") return;
97
+ if (token) localStorage.setItem(NOCOBASE_TOKEN_KEY, token);
98
+ else localStorage.removeItem(NOCOBASE_TOKEN_KEY);
99
+ }
100
+
101
+ getRole() {
102
+ return typeof localStorage === "undefined"
103
+ ? undefined
104
+ : localStorage.getItem(NOCOBASE_ROLE_KEY) ?? undefined;
105
+ }
106
+
107
+ setRole(role?: string | null) {
108
+ if (typeof localStorage === "undefined") return;
109
+ if (role) localStorage.setItem(NOCOBASE_ROLE_KEY, role);
110
+ else localStorage.removeItem(NOCOBASE_ROLE_KEY);
111
+ }
112
+
113
+ getStoredLocale() {
114
+ return typeof localStorage === "undefined"
115
+ ? undefined
116
+ : localStorage.getItem(NOCOBASE_LOCALE_KEY) ?? undefined;
117
+ }
118
+
119
+ getLocale() {
120
+ return this.getStoredLocale() ?? getBrowserLocale();
121
+ }
122
+
123
+ setLocale(locale?: string | null) {
124
+ if (typeof localStorage === "undefined") return;
125
+ if (locale) localStorage.setItem(NOCOBASE_LOCALE_KEY, locale);
126
+ else localStorage.removeItem(NOCOBASE_LOCALE_KEY);
127
+ }
128
+
129
+ buildUrl(endpoint: string, query?: Record<string, QueryValue>) {
130
+ const base = `${this.apiUrl.replace(/\/$/, "")}/${endpoint.replace(
131
+ /^\//,
132
+ ""
133
+ )}`;
134
+ const url = /^https?:\/\//.test(base)
135
+ ? new URL(base)
136
+ : new URL(base, window.location.origin);
137
+
138
+ for (const [key, value] of Object.entries(query ?? {})) {
139
+ if (value === undefined) continue;
140
+ if (Array.isArray(value)) {
141
+ value.forEach((item) => url.searchParams.append(key, String(item)));
142
+ } else if (value !== null) {
143
+ url.searchParams.set(key, String(value));
144
+ }
145
+ }
146
+ return url;
147
+ }
148
+
149
+ getHeaders({
150
+ token = this.getToken(),
151
+ role = this.getRole(),
152
+ includeRole = true,
153
+ includeAuthenticator = false,
154
+ withAclMeta = true,
155
+ headers,
156
+ accept = "json",
157
+ body,
158
+ }: Pick<
159
+ NocoBaseRequestOptions,
160
+ | "token"
161
+ | "role"
162
+ | "includeRole"
163
+ | "includeAuthenticator"
164
+ | "withAclMeta"
165
+ | "headers"
166
+ | "accept"
167
+ | "body"
168
+ > = {}) {
169
+ const locale = this.getLocale();
170
+ const formData =
171
+ typeof FormData !== "undefined" && body instanceof FormData;
172
+ return {
173
+ Accept: accept === "stream" ? "text/event-stream" : "application/json",
174
+ ...(body !== undefined && !formData
175
+ ? { "Content-Type": "application/json" }
176
+ : {}),
177
+ ...(accept === "stream"
178
+ ? { "Cache-Control": "no-cache", Pragma: "no-cache" }
179
+ : {}),
180
+ ...(includeAuthenticator
181
+ ? { "X-Authenticator": NOCOBASE_AUTHENTICATOR }
182
+ : {}),
183
+ ...(includeRole && role ? { "X-Role": role } : {}),
184
+ ...(withAclMeta ? { "X-With-ACL-Meta": "true" } : {}),
185
+ ...(locale ? { "X-Locale": locale } : {}),
186
+ "X-Timezone": getClientTimezone(),
187
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
188
+ ...headers,
189
+ };
190
+ }
191
+
192
+ async request<T>(
193
+ endpoint: string,
194
+ options: NocoBaseRequestOptions = {}
195
+ ): Promise<T> {
196
+ const method =
197
+ options.method ?? (options.body === undefined ? "GET" : "POST");
198
+ const response = await fetch(this.buildUrl(endpoint, options.query), {
199
+ method,
200
+ headers: this.getHeaders({ ...options, body: options.body }),
201
+ body:
202
+ options.body === undefined
203
+ ? undefined
204
+ : options.body instanceof FormData
205
+ ? options.body
206
+ : JSON.stringify(options.body),
207
+ signal: options.signal,
208
+ });
209
+ this.captureRenewedToken(response);
210
+ const payload = await response.json().catch(() => undefined);
211
+ if (!response.ok) {
212
+ throw new NocoBaseHttpError({
213
+ status: response.status,
214
+ payload,
215
+ message: getNocoBaseErrorMessage(
216
+ payload,
217
+ `NocoBase request failed (${response.status})`
218
+ ),
219
+ });
220
+ }
221
+ return unwrapPayload(payload, options.unwrap ?? "data") as T;
222
+ }
223
+
224
+ action<T>(
225
+ resource: string,
226
+ action: string,
227
+ options: Omit<NocoBaseRequestOptions, "accept"> = {}
228
+ ) {
229
+ const method =
230
+ options.method ?? (["get", "list"].includes(action) ? "GET" : "POST");
231
+ return this.request<T>(`${resource}:${action}`, { ...options, method });
232
+ }
233
+
234
+ async stream(
235
+ endpoint: string,
236
+ options: Omit<NocoBaseRequestOptions, "accept" | "unwrap"> = {}
237
+ ) {
238
+ const response = await fetch(this.buildUrl(endpoint, options.query), {
239
+ method: options.method ?? "POST",
240
+ headers: this.getHeaders({
241
+ ...options,
242
+ accept: "stream",
243
+ body: options.body,
244
+ }),
245
+ body:
246
+ options.body === undefined ? undefined : JSON.stringify(options.body),
247
+ signal: options.signal,
248
+ });
249
+ this.captureRenewedToken(response);
250
+ if (!response.ok || !response.body) {
251
+ const payload = await response.json().catch(() => undefined);
252
+ throw new NocoBaseHttpError({
253
+ status: response.status,
254
+ payload,
255
+ message: getNocoBaseErrorMessage(
256
+ payload,
257
+ `NocoBase stream failed (${response.status})`
258
+ ),
259
+ });
260
+ }
261
+ return response.body;
262
+ }
263
+
264
+ private captureRenewedToken(response: Response) {
265
+ const token = response.headers.get("x-new-token");
266
+ if (token) this.setToken(token);
267
+ }
268
+ }
269
+
270
+ const getUrlOrigin = (value: string) => {
271
+ if (!/^https?:\/\//i.test(value)) return undefined;
272
+ try {
273
+ return new URL(value).origin;
274
+ } catch {
275
+ return undefined;
276
+ }
277
+ };
278
+
279
+ export const nocobaseClient = new NocoBaseClient();
@@ -0,0 +1,32 @@
1
+ export class NocoBaseHttpError extends Error {
2
+ readonly status: number;
3
+ readonly payload: unknown;
4
+
5
+ constructor({
6
+ message,
7
+ status,
8
+ payload,
9
+ }: {
10
+ message: string;
11
+ status: number;
12
+ payload?: unknown;
13
+ }) {
14
+ super(message);
15
+ this.name = "NocoBaseHttpError";
16
+ this.status = status;
17
+ this.payload = payload;
18
+ }
19
+ }
20
+
21
+ export const getNocoBaseErrorMessage = (payload: unknown, fallback: string) => {
22
+ if (!payload || typeof payload !== "object") return fallback;
23
+ const value = payload as {
24
+ message?: string;
25
+ error?: { message?: string };
26
+ errors?: Array<{ message?: string } | string>;
27
+ messages?: Array<{ message?: string } | string>;
28
+ };
29
+ const first = value.errors?.[0] ?? value.messages?.[0];
30
+ if (typeof first === "string") return first;
31
+ return first?.message ?? value.error?.message ?? value.message ?? fallback;
32
+ };
@@ -0,0 +1,82 @@
1
+ export const starter = {
2
+ "buttons.create": "Create",
3
+ "buttons.edit": "Edit",
4
+ "buttons.show": "View",
5
+ "buttons.delete": "Delete",
6
+ "buttons.refresh": "Refresh",
7
+ "buttons.cancel": "Cancel",
8
+ "buttons.confirm": "Are you sure?",
9
+ "views.create.title": "Create {{resource}}",
10
+ "views.create.description":
11
+ "Add a new {{resource}} to your NocoBase workspace.",
12
+ "views.edit.title": "Edit {{resource}}",
13
+ "views.edit.description":
14
+ "Update this {{resource}} while NocoBase keeps the data consistent.",
15
+ "views.show.description": "Review the record and its NocoBase-managed data.",
16
+ "table.empty.title": "No data to display",
17
+ "table.empty.description": "This table is empty for the time being.",
18
+ "table.pagination.total": "{{count}} row(s)",
19
+ "table.pagination.pageSize": "Rows per page",
20
+ "table.pagination.page": "Page {{current}} of {{total}}",
21
+ "table.pagination.first": "Go to first page",
22
+ "table.pagination.previous": "Go to previous page",
23
+ "table.pagination.next": "Go to next page",
24
+ "table.pagination.last": "Go to last page",
25
+ "notifications.success": "Success",
26
+ "notifications.createSuccess": "Successfully created {{resource}}",
27
+ "notifications.editSuccess": "Successfully updated {{resource}}",
28
+ "notifications.deleteSuccess": "Successfully deleted {{resource}}",
29
+ "users.users": "User",
30
+ Admin: "Admin",
31
+ Root: "Root",
32
+ Member: "Member",
33
+ Anonymous: "Anonymous",
34
+ "Full permissions": "Full permissions",
35
+ } as const;
36
+
37
+ export const app = {
38
+ "resources.users": "Users",
39
+ "resources.user": "User",
40
+ "resources.users.description":
41
+ "Manage the people who can sign in and work in this NocoBase application.",
42
+ "users.fields.id": "ID",
43
+ "users.fields.nickname": "Nickname",
44
+ "users.fields.username": "Username",
45
+ "users.fields.email": "Email",
46
+ "users.fields.phone": "Phone",
47
+ "users.fields.password": "Password",
48
+ "users.fields.roles": "Roles",
49
+ "users.fields.createdAt": "Created at",
50
+ "users.fields.updatedAt": "Updated at",
51
+ "users.fields.actions": "Actions",
52
+ "users.actions.view": "View user",
53
+ "users.actions.edit": "Edit user",
54
+ "users.actions.delete": "Delete user",
55
+ "users.form.create.submit": "Create user",
56
+ "users.form.create.submitting": "Creating...",
57
+ "users.form.edit.submit": "Save changes",
58
+ "users.form.edit.submitting": "Saving...",
59
+ "users.form.cancel": "Cancel",
60
+ "users.form.nickname.placeholder": "Enter a display name",
61
+ "users.form.username.placeholder": "Enter a unique username",
62
+ "users.form.email.placeholder": "Enter an email address",
63
+ "users.form.phone.placeholder": "Enter a phone number",
64
+ "users.form.password.placeholder": "Set an initial password",
65
+ "users.form.password.aiNotice":
66
+ "Passwords are never included in AI page context or Form filler.",
67
+ "users.validation.username": "Username is required",
68
+ "users.validation.email": "Email is required",
69
+ "users.validation.emailFormat": "Enter a valid email address",
70
+ "users.validation.phone": "Phone is required",
71
+ "users.validation.password": "Password is required",
72
+ "users.detail.identity": "Identity",
73
+ "users.detail.contact": "Contact",
74
+ "users.detail.access": "Access",
75
+ "users.detail.timestamps": "Timestamps",
76
+ "users.detail.noRoles": "No assigned roles",
77
+ "users.detail.unnamed": "Unnamed user",
78
+ "users.ai.table": "Users table",
79
+ "users.ai.detail": "User details",
80
+ "users.ai.createForm": "Create user form",
81
+ "users.ai.editForm": "Edit user form",
82
+ } as const;
@@ -0,0 +1,13 @@
1
+ import { registerTranslationResources } from "@/lib/i18n";
2
+ import { app as enUSApp, starter as enUSStarter } from "./en-US";
3
+ import { app as zhCNApp, starter as zhCNStarter } from "./zh-CN";
4
+
5
+ registerTranslationResources("starter", {
6
+ "en-US": enUSStarter,
7
+ "zh-CN": zhCNStarter,
8
+ });
9
+
10
+ registerTranslationResources("app", {
11
+ "en-US": enUSApp,
12
+ "zh-CN": zhCNApp,
13
+ });
@@ -0,0 +1,79 @@
1
+ export const starter = {
2
+ "buttons.create": "新建",
3
+ "buttons.edit": "编辑",
4
+ "buttons.show": "查看",
5
+ "buttons.delete": "删除",
6
+ "buttons.refresh": "刷新",
7
+ "buttons.cancel": "取消",
8
+ "buttons.confirm": "确定要执行此操作吗?",
9
+ "views.create.title": "新建{{resource}}",
10
+ "views.create.description": "在 NocoBase 工作区中添加新的{{resource}}。",
11
+ "views.edit.title": "编辑{{resource}}",
12
+ "views.edit.description": "更新此{{resource}},数据一致性由 NocoBase 保障。",
13
+ "views.show.description": "查看记录及其由 NocoBase 管理的数据。",
14
+ "table.empty.title": "暂无数据",
15
+ "table.empty.description": "当前表格中没有可显示的数据。",
16
+ "table.pagination.total": "共 {{count}} 条",
17
+ "table.pagination.pageSize": "每页行数",
18
+ "table.pagination.page": "第 {{current}} 页,共 {{total}} 页",
19
+ "table.pagination.first": "前往第一页",
20
+ "table.pagination.previous": "前往上一页",
21
+ "table.pagination.next": "前往下一页",
22
+ "table.pagination.last": "前往最后一页",
23
+ "notifications.success": "操作成功",
24
+ "notifications.createSuccess": "已成功创建{{resource}}",
25
+ "notifications.editSuccess": "已成功更新{{resource}}",
26
+ "notifications.deleteSuccess": "已成功删除{{resource}}",
27
+ "users.users": "用户",
28
+ Admin: "管理员",
29
+ Root: "超级管理员",
30
+ Member: "成员",
31
+ Anonymous: "匿名用户",
32
+ "Full permissions": "全部权限",
33
+ } as const;
34
+
35
+ export const app = {
36
+ "resources.users": "用户",
37
+ "resources.user": "用户",
38
+ "resources.users.description": "管理可以登录并使用此 NocoBase 应用的用户。",
39
+ "users.fields.id": "ID",
40
+ "users.fields.nickname": "昵称",
41
+ "users.fields.username": "用户名",
42
+ "users.fields.email": "邮箱",
43
+ "users.fields.phone": "手机号",
44
+ "users.fields.password": "密码",
45
+ "users.fields.roles": "角色",
46
+ "users.fields.createdAt": "创建时间",
47
+ "users.fields.updatedAt": "更新时间",
48
+ "users.fields.actions": "操作",
49
+ "users.actions.view": "查看用户",
50
+ "users.actions.edit": "编辑用户",
51
+ "users.actions.delete": "删除用户",
52
+ "users.form.create.submit": "创建用户",
53
+ "users.form.create.submitting": "正在创建...",
54
+ "users.form.edit.submit": "保存修改",
55
+ "users.form.edit.submitting": "正在保存...",
56
+ "users.form.cancel": "取消",
57
+ "users.form.nickname.placeholder": "输入显示名称",
58
+ "users.form.username.placeholder": "输入唯一用户名",
59
+ "users.form.email.placeholder": "输入邮箱地址",
60
+ "users.form.phone.placeholder": "输入手机号",
61
+ "users.form.password.placeholder": "设置初始密码",
62
+ "users.form.password.aiNotice":
63
+ "密码不会包含在 AI 页面上下文中,Form filler 也无法填写密码。",
64
+ "users.validation.username": "请输入用户名",
65
+ "users.validation.email": "请输入邮箱",
66
+ "users.validation.emailFormat": "请输入有效的邮箱地址",
67
+ "users.validation.phone": "请输入手机号",
68
+ "users.validation.password": "请输入密码",
69
+ "users.detail.identity": "身份信息",
70
+ "users.detail.contact": "联系方式",
71
+ "users.detail.access": "访问权限",
72
+ "users.detail.timestamps": "时间信息",
73
+ "users.detail.noRoles": "尚未分配角色",
74
+ "users.detail.unnamed": "未命名用户",
75
+ "users.ai.table": "用户表格",
76
+ "users.ai.detail": "用户详情",
77
+ "users.ai.createForm": "新建用户表单",
78
+ "users.ai.editForm": "编辑用户表单",
79
+ } as const;
@@ -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
+ };