@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.
- package/README.MD +90 -1
- package/components.json +3 -1
- package/package.json +21 -5
- package/registry/nocobase-acl/README.md +19 -0
- package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/registry/nocobase-acl/components/index.ts +3 -0
- package/registry/nocobase-acl/components/role-options.ts +36 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/registry/nocobase-acl/demo/components.tsx +231 -0
- package/registry/nocobase-acl/demo/index.tsx +505 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/registry/nocobase-acl/extension.tsx +71 -0
- package/registry/nocobase-acl/index.ts +1 -0
- package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
- package/registry/nocobase-ai/README.md +70 -0
- package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/registry/nocobase-ai/components/index.ts +59 -0
- package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/registry/nocobase-ai/demo/floating.tsx +134 -0
- package/registry/nocobase-ai/demo/index.tsx +346 -0
- package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/registry/nocobase-ai/demo/page-context.tsx +884 -0
- package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/registry/nocobase-ai/extension.tsx +158 -0
- package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
- package/registry/nocobase-ai/index.ts +3 -0
- package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/registry/nocobase-ai/providers/avatars.ts +817 -0
- package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
- package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
- package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
- package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
- package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/registry/nocobase-ai/providers/index.ts +61 -0
- package/registry/nocobase-ai/providers/model.ts +32 -0
- package/registry/nocobase-ai/providers/page-context.tsx +136 -0
- package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
- package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/registry/nocobase-ai/providers/types.ts +183 -0
- package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/registry/nocobase-ai/services/index.ts +7 -0
- package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/registry/nocobase-ai/services/types.ts +82 -0
- package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
- package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
- package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +163 -0
- package/scripts/registry.mjs +164 -0
- package/src/App.css +95 -0
- package/src/App.tsx +87 -69
- package/src/app/extension.ts +11 -0
- package/src/app/extensions.tsx +49 -0
- package/src/components/access-control/access-denied.tsx +30 -0
- package/src/components/access-control/acl-bootstrap.tsx +9 -0
- package/src/components/access-control/acl-gate.tsx +36 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
- package/src/components/access-control/resource-access-guard.tsx +21 -0
- package/src/components/access-control/use-acl-runtime.ts +28 -0
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +22 -4
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +103 -45
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/acl/action.ts +147 -0
- package/src/lib/nocobase/acl/data-source.ts +24 -0
- package/src/lib/nocobase/acl/index.ts +6 -0
- package/src/lib/nocobase/acl/menu.ts +41 -0
- package/src/lib/nocobase/acl/record-permissions.ts +111 -0
- package/src/lib/nocobase/acl/store.ts +182 -0
- package/src/lib/nocobase/acl/types.ts +96 -0
- package/src/lib/nocobase/client.ts +279 -0
- package/src/lib/nocobase/error.ts +32 -0
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +3 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /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
|
+
};
|