@nocobase/portal-template-default 1.0.15 → 1.0.17

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 (153) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-iG-ADes-.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DXoSw1tM.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-C_ee4Eht.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-5Uxza9Ms.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-CyqG1QME.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-D-ptYjX_.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-C08mkkiX.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-wj34-b1B.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-DWrhrHL_.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSWsSruv.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-DxHNfq1C.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-D8kXn6ME.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-QY8v95qK.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-D-yqAK-r.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-P4-i8jPU.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-ByQZ-DbP.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-BFVEyPEp.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Btk35Nx-.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-um5hdKdZ.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-CwNxd66J.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DQqRy3a_.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-CQdRcR_p.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-CnQ4GYM7.js → page-context-DAiW_dId.js} +3 -3
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-DN_Vv6OL.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-DTX9GV2I.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-jI1psK0d.js → static.browser-kzzpH0Xt.js} +8 -8
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-DFttLAfo.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-CRGjzXju.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-C9j8wfCD.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/wecom-sign-in-button-Dn_2zDab.js +1 -0
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/components/auth/sign-up-form.tsx +287 -59
  70. package/src/components/auth/types.ts +23 -1
  71. package/src/extensions/nocobase-acl/README.md +28 -14
  72. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  73. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  74. package/src/extensions/nocobase-mail/README.md +74 -0
  75. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  76. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  77. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  78. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  79. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  80. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  81. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  82. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  83. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  84. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  85. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  86. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  87. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  89. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  90. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  91. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  92. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  93. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  94. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  95. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  96. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  97. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  98. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  99. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  100. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  101. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  102. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  103. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  104. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  108. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  109. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  110. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  111. package/src/extensions/nocobase-mail/index.ts +1 -0
  112. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  113. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  114. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  115. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  116. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  117. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  118. package/src/extensions/nocobase-users-example/index.ts +1 -0
  119. package/src/lib/nocobase/acl/menu.ts +17 -3
  120. package/src/lib/nocobase/acl/types.ts +4 -0
  121. package/src/locales/en-US.ts +2 -1
  122. package/src/locales/zh-CN.ts +2 -1
  123. package/src/providers/runtime-config.ts +18 -0
  124. package/src/routes.tsx +10 -0
  125. package/src/vite-env.d.ts +3 -0
  126. package/vite.config.ts +8 -0
  127. package/dist/assets/auth-method-demo-DaOCuvsq.js +0 -1
  128. package/dist/assets/auto-login-provider-DyaPGK2D.js +0 -1
  129. package/dist/assets/auto-redirect-provider-CfwRvYOI.js +0 -1
  130. package/dist/assets/auto-redirect-provider-Cr3jmYts.js +0 -1
  131. package/dist/assets/auto-redirect-provider-QkJQRBbG.js +0 -1
  132. package/dist/assets/calendar-DjDRfD5t.js +0 -1
  133. package/dist/assets/card-DuQBOS6A.js +0 -1
  134. package/dist/assets/createSeriesDataSimply-D0A6SAVQ.js +0 -1
  135. package/dist/assets/demo-BYWHeKUe.js +0 -1
  136. package/dist/assets/demo-CPWoNtx8.js +0 -1
  137. package/dist/assets/demo-CyvJk5zV.js +0 -1
  138. package/dist/assets/demo-D4OiKplc.js +0 -1
  139. package/dist/assets/demo-DBueJg0m.js +0 -1
  140. package/dist/assets/demo-DkIhyiRK.js +0 -1
  141. package/dist/assets/demo-tpTjO0sC.js +0 -1
  142. package/dist/assets/index-BgCbGDMc.js +0 -43
  143. package/dist/assets/index-MB-jfQld.js +0 -686
  144. package/dist/assets/index-NH3aqqQz.css +0 -1
  145. package/dist/assets/index-gFtCXH3o.js +0 -58
  146. package/dist/assets/ldap-sign-in-form--6Gt3yGq.js +0 -1
  147. package/dist/assets/prompt-card-B2RS4nt2.js +0 -1
  148. package/dist/assets/prompt-output-DZHRM2ft.js +0 -1
  149. package/dist/assets/saml-sign-in-button-BHL5a0JV.js +0 -1
  150. package/dist/assets/shortcut-C5x5LQWU.js +0 -47
  151. package/dist/assets/switch-CRL8PmRI.js +0 -1
  152. package/dist/assets/use-dingtalk-sign-in-8WYzJ2jf.js +0 -1
  153. package/dist/assets/wecom-sign-in-button-mIS1zGSU.js +0 -1
@@ -1,27 +1,201 @@
1
1
  "use client";
2
2
 
3
+ import { AlertCircle, KeyRound } from "lucide-react";
3
4
  import { useState } from "react";
4
5
  import { useLink, useNotification, useRegister } from "@refinedev/core";
5
6
  import { useSearchParams } from "react-router";
6
7
 
7
8
  import { AuthLayout } from "@/components/auth/auth-layout";
8
9
  import { InputPassword } from "@/components/auth/input-password";
10
+ import type {
11
+ Authenticator,
12
+ AuthenticatorSignUpField,
13
+ } from "@/components/auth/types";
14
+ import { usePublicAuthenticators } from "@/components/auth/use-public-authenticators";
15
+ import {
16
+ Alert,
17
+ AlertDescription,
18
+ AlertTitle,
19
+ } from "@/components/ui/alert";
9
20
  import { Button } from "@/components/ui/button";
21
+ import { Checkbox } from "@/components/ui/checkbox";
22
+ import {
23
+ Empty,
24
+ EmptyDescription,
25
+ EmptyHeader,
26
+ EmptyMedia,
27
+ EmptyTitle,
28
+ } from "@/components/ui/empty";
10
29
  import { Input } from "@/components/ui/input";
11
30
  import { Label } from "@/components/ui/label";
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from "@/components/ui/select";
38
+ import { Spinner } from "@/components/ui/spinner";
39
+ import { Textarea } from "@/components/ui/textarea";
40
+ import { resolveTranslatableText } from "@/lib/i18n";
12
41
 
13
- export const SignUpForm = () => {
14
- const [email, setEmail] = useState("");
42
+ type SignUpFieldValue = string | number | boolean;
43
+ type SignUpValues = Record<string, SignUpFieldValue>;
44
+
45
+ type SignUpVariables = SignUpValues & {
46
+ authenticator: string;
47
+ password: string;
48
+ confirm_password: string;
49
+ };
50
+
51
+ function getFieldLabel(field: AuthenticatorSignUpField) {
52
+ return (
53
+ resolveTranslatableText(field.uiSchema?.title, {
54
+ ns: "lm-collections",
55
+ }) || field.field
56
+ );
57
+ }
58
+
59
+ function getFieldId(authenticatorName: string, fieldName: string) {
60
+ return `${authenticatorName}-${fieldName}`.replace(/[^a-zA-Z0-9_-]/g, "-");
61
+ }
62
+
63
+ function DynamicSignUpField({
64
+ authenticatorName,
65
+ field,
66
+ value,
67
+ onChange,
68
+ }: {
69
+ authenticatorName: string;
70
+ field: AuthenticatorSignUpField;
71
+ value: SignUpFieldValue | undefined;
72
+ onChange: (value: SignUpFieldValue) => void;
73
+ }) {
74
+ const id = getFieldId(authenticatorName, field.field);
75
+ const label = getFieldLabel(field);
76
+ const component = field.uiSchema?.["x-component"];
77
+ const options = field.uiSchema?.enum;
78
+
79
+ if (options?.length) {
80
+ const selectedValue =
81
+ typeof value === "undefined" ? undefined : String(value);
82
+
83
+ return (
84
+ <div className="space-y-2">
85
+ <Label htmlFor={id}>{label}</Label>
86
+ <Select
87
+ value={selectedValue}
88
+ onValueChange={(nextValue) => {
89
+ if (nextValue === null) return;
90
+ const option = options.find(
91
+ (item) => String(item.value) === nextValue
92
+ );
93
+ onChange(option?.value ?? nextValue);
94
+ }}
95
+ >
96
+ <SelectTrigger id={id} className="w-full" aria-required={field.required}>
97
+ <SelectValue />
98
+ </SelectTrigger>
99
+ <SelectContent>
100
+ {options.map((option) => (
101
+ <SelectItem
102
+ key={`${typeof option.value}:${String(option.value)}`}
103
+ value={String(option.value)}
104
+ >
105
+ {resolveTranslatableText(option.label) || String(option.value)}
106
+ </SelectItem>
107
+ ))}
108
+ </SelectContent>
109
+ </Select>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ if (component === "Checkbox") {
115
+ return (
116
+ <div className="flex items-center gap-2">
117
+ <Checkbox
118
+ id={id}
119
+ name={field.field}
120
+ checked={value === true}
121
+ onCheckedChange={(checked) => onChange(checked)}
122
+ aria-required={field.required}
123
+ />
124
+ <Label htmlFor={id}>{label}</Label>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ if (component === "Input.TextArea" || component === "TextArea") {
130
+ return (
131
+ <div className="space-y-2">
132
+ <Label htmlFor={id}>{label}</Label>
133
+ <Textarea
134
+ id={id}
135
+ name={field.field}
136
+ value={typeof value === "string" ? value : ""}
137
+ onChange={(event) => onChange(event.target.value)}
138
+ required={field.required}
139
+ rows={4}
140
+ />
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <div className="space-y-2">
147
+ <Label htmlFor={id}>{label}</Label>
148
+ <Input
149
+ id={id}
150
+ name={field.field}
151
+ type={field.field === "email" ? "email" : "text"}
152
+ value={typeof value === "string" ? value : ""}
153
+ onChange={(event) => onChange(event.target.value)}
154
+ autoComplete={
155
+ field.field === "email"
156
+ ? "email"
157
+ : field.field === "username"
158
+ ? "username"
159
+ : undefined
160
+ }
161
+ required={field.required}
162
+ />
163
+ </div>
164
+ );
165
+ }
166
+
167
+ function BasicSignUpForm({ authenticator }: { authenticator: Authenticator }) {
168
+ const [values, setValues] = useState<SignUpValues>({});
15
169
  const [password, setPassword] = useState("");
16
170
  const [confirmPassword, setConfirmPassword] = useState("");
17
171
  const { open } = useNotification();
18
172
  const Link = useLink();
19
- const [searchParams] = useSearchParams();
20
- const { mutate: register, isPending } = useRegister();
173
+ const { mutate: register, isPending } = useRegister<SignUpVariables>();
174
+ const fields = (authenticator.options?.signupForm ?? []).filter(
175
+ (field) =>
176
+ field?.show &&
177
+ field.field !== "password" &&
178
+ field.field !== "confirm_password"
179
+ );
180
+
181
+ const updateValue = (field: string, value: SignUpFieldValue) => {
182
+ setValues((current) => ({ ...current, [field]: value }));
183
+ };
21
184
 
22
185
  const handleSignUp = (event: React.FormEvent<HTMLFormElement>) => {
23
186
  event.preventDefault();
24
187
 
188
+ const missingField = fields.find(
189
+ (field) => field.required && !values[field.field]
190
+ );
191
+ if (missingField) {
192
+ open?.({
193
+ type: "error",
194
+ message: `Please enter ${getFieldLabel(missingField)}`,
195
+ });
196
+ return;
197
+ }
198
+
25
199
  if (password !== confirmPassword) {
26
200
  open?.({
27
201
  type: "error",
@@ -32,71 +206,125 @@ export const SignUpForm = () => {
32
206
  }
33
207
 
34
208
  register({
35
- email,
209
+ ...values,
36
210
  password,
37
- authenticator: searchParams.get("name") ?? undefined,
211
+ confirm_password: confirmPassword,
212
+ authenticator: authenticator.name,
38
213
  });
39
214
  };
40
215
 
216
+ return (
217
+ <form onSubmit={handleSignUp} className="space-y-5">
218
+ {fields.map((field) => (
219
+ <DynamicSignUpField
220
+ key={field.field}
221
+ authenticatorName={authenticator.name}
222
+ field={field}
223
+ value={values[field.field]}
224
+ onChange={(value) => updateValue(field.field, value)}
225
+ />
226
+ ))}
227
+
228
+ <div className="space-y-2">
229
+ <Label htmlFor={`${authenticator.name}-password`}>Password</Label>
230
+ <InputPassword
231
+ id={`${authenticator.name}-password`}
232
+ value={password}
233
+ onChange={(event) => setPassword(event.target.value)}
234
+ autoComplete="new-password"
235
+ required
236
+ />
237
+ </div>
238
+
239
+ <div className="space-y-2">
240
+ <Label htmlFor={`${authenticator.name}-confirm-password`}>
241
+ Confirm password
242
+ </Label>
243
+ <InputPassword
244
+ id={`${authenticator.name}-confirm-password`}
245
+ value={confirmPassword}
246
+ onChange={(event) => setConfirmPassword(event.target.value)}
247
+ autoComplete="new-password"
248
+ required
249
+ />
250
+ </div>
251
+
252
+ <Button type="submit" className="w-full" disabled={isPending}>
253
+ {isPending ? "Creating account…" : "Sign up"}
254
+ </Button>
255
+
256
+ <p className="text-center text-sm text-muted-foreground">
257
+ Have an account?{" "}
258
+ <Link
259
+ to="/login"
260
+ className="font-semibold text-foreground underline underline-offset-4"
261
+ >
262
+ Sign in
263
+ </Link>
264
+ </p>
265
+ </form>
266
+ );
267
+ }
268
+
269
+ export const SignUpForm = () => {
270
+ const [searchParams] = useSearchParams();
271
+ const { data: authenticators = [], error, isPending } =
272
+ usePublicAuthenticators();
273
+ const authenticatorName = searchParams.get("name");
274
+ const authenticator = authenticators.find(
275
+ (item) => item.name === authenticatorName
276
+ );
277
+
41
278
  return (
42
279
  <AuthLayout
43
280
  title="Create your account"
44
281
  description="Create your NocoBase account."
45
282
  >
46
- <form onSubmit={handleSignUp} className="space-y-5">
47
- <div className="space-y-2">
48
- <Label htmlFor="email">Email</Label>
49
- <Input
50
- id="email"
51
- type="email"
52
- value={email}
53
- onChange={(event) => setEmail(event.target.value)}
54
- autoComplete="email"
55
- autoFocus
56
- required
57
- />
283
+ {isPending ? (
284
+ <div className="flex min-h-64 items-center justify-center">
285
+ <Spinner className="size-6 text-muted-foreground" />
58
286
  </div>
59
-
60
- <div className="space-y-2">
61
- <Label htmlFor="password">Password</Label>
62
- <InputPassword
63
- id="password"
64
- value={password}
65
- onChange={(event) => setPassword(event.target.value)}
66
- autoComplete="new-password"
67
- required
68
- />
69
- </div>
70
-
71
- <div className="space-y-2">
72
- <Label htmlFor="confirmPassword">Confirm password</Label>
73
- <InputPassword
74
- id="confirmPassword"
75
- value={confirmPassword}
76
- onChange={(event) => setConfirmPassword(event.target.value)}
77
- autoComplete="new-password"
78
- required
79
- />
80
- </div>
81
-
82
- <Button
83
- type="submit"
84
- className="w-full"
85
- disabled={isPending}
86
- >
87
- {isPending ? "Creating account…" : "Sign up"}
88
- </Button>
89
-
90
- <p className="text-center text-sm text-muted-foreground">
91
- Have an account?{" "}
92
- <Link
93
- to="/login"
94
- className="font-semibold text-foreground underline underline-offset-4"
95
- >
96
- Sign in
97
- </Link>
98
- </p>
99
- </form>
287
+ ) : error ? (
288
+ <Alert variant="destructive">
289
+ <AlertCircle />
290
+ <AlertTitle>Unable to load the sign-up method</AlertTitle>
291
+ <AlertDescription>
292
+ {import.meta.env.DEV && error instanceof Error
293
+ ? error.message
294
+ : "Please try again or contact your administrator."}
295
+ </AlertDescription>
296
+ </Alert>
297
+ ) : !authenticator || authenticator.authType !== "Email/Password" ? (
298
+ <Empty className="min-h-64 border">
299
+ <EmptyHeader>
300
+ <EmptyMedia variant="icon">
301
+ <KeyRound />
302
+ </EmptyMedia>
303
+ <EmptyTitle>No sign-up method available</EmptyTitle>
304
+ <EmptyDescription>
305
+ This sign-up link is invalid or the authentication method does
306
+ not support account registration.
307
+ </EmptyDescription>
308
+ </EmptyHeader>
309
+ </Empty>
310
+ ) : authenticator.options?.allowSignUp !== true ? (
311
+ <Empty className="min-h-64 border">
312
+ <EmptyHeader>
313
+ <EmptyMedia variant="icon">
314
+ <KeyRound />
315
+ </EmptyMedia>
316
+ <EmptyTitle>Account registration is disabled</EmptyTitle>
317
+ <EmptyDescription>
318
+ Contact your administrator if you need an account.
319
+ </EmptyDescription>
320
+ </EmptyHeader>
321
+ </Empty>
322
+ ) : (
323
+ <BasicSignUpForm
324
+ key={authenticator.name}
325
+ authenticator={authenticator}
326
+ />
327
+ )}
100
328
  </AuthLayout>
101
329
  );
102
330
  };
@@ -4,12 +4,34 @@ import type {
4
4
  ReactNode,
5
5
  } from "react";
6
6
 
7
+ export type AuthenticatorSignUpFieldOption = {
8
+ label?: unknown;
9
+ value: string | number | boolean;
10
+ };
11
+
12
+ export type AuthenticatorSignUpField = {
13
+ field: string;
14
+ required?: boolean;
15
+ show?: boolean;
16
+ uiSchema?: {
17
+ enum?: AuthenticatorSignUpFieldOption[];
18
+ title?: unknown;
19
+ "x-component"?: string;
20
+ };
21
+ };
22
+
23
+ export type AuthenticatorPublicOptions = Record<string, unknown> & {
24
+ allowSignUp?: boolean;
25
+ enableResetPassword?: boolean;
26
+ signupForm?: AuthenticatorSignUpField[];
27
+ };
28
+
7
29
  export type Authenticator = {
8
30
  name: string;
9
31
  authType: string;
10
32
  authTypeTitle?: string;
11
33
  title?: string;
12
- options?: Record<string, unknown>;
34
+ options?: AuthenticatorPublicOptions;
13
35
  sort?: number;
14
36
  };
15
37
 
@@ -12,11 +12,17 @@ After installation, import the components from `@/extensions/nocobase-acl`.
12
12
  normal roles, Anonymous when enabled, and Full permissions through the
13
13
  `__union__` role when the system role mode allows it.
14
14
 
15
- Route resource metadata and `AclPage` accept a `roles` constraint with `anyOf`,
16
- `allOf`, and `noneOf`. Role constraints use the roles in the current effective
17
- ACL context. In union mode this is the set of roles participating in the union;
18
- in single-role mode it is only the selected role. Regions, fields, and actions
19
- continue to use resource permissions.
15
+ Application route definitions and `AclPage` accept a `roles` constraint with
16
+ `anyOf`, `allOf`, and `noneOf`. Put route-level constraints in `access.roles`
17
+ through `defineAppRoutes`; the Starter applies the same constraint to menu
18
+ visibility and direct URL access. Role constraints use the roles in the current
19
+ effective ACL context. In union mode this is the set of roles participating in
20
+ the union; in single-role mode it is only the selected role. Regions, fields,
21
+ and actions continue to use resource permissions.
22
+
23
+ Nested routes inherit their parents' route access. The generated navigation
24
+ item evaluates every constraint in that route chain, matching the nested route
25
+ guards used for direct URL access.
20
26
 
21
27
  Use the Starter's `useGetRoles()` hook when application UI needs the same
22
28
  effective role names. Its result follows the familiar Refine query shape with
@@ -35,15 +41,23 @@ function CurrentAccessContext() {
35
41
  ```
36
42
 
37
43
  ```tsx
38
- const adminRoute = {
39
- name: "administration",
40
- meta: {
41
- acl: {
42
- type: "authenticated",
44
+ import { defineAppRoutes } from "@/app/route-runtime";
45
+
46
+ export const appRoutes = defineAppRoutes([
47
+ {
48
+ name: "administration",
49
+ path: "/administration",
50
+ element: <AdministrationPage />,
51
+ resource: {
52
+ meta: {
53
+ label: "Administration",
54
+ },
55
+ },
56
+ access: {
43
57
  roles: { anyOf: ["admin"] },
44
58
  },
45
59
  },
46
- };
60
+ ]);
47
61
 
48
62
  function AuditLogsRoute() {
49
63
  return (
@@ -56,9 +70,9 @@ function AuditLogsRoute() {
56
70
  }
57
71
  ```
58
72
 
59
- Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
60
- the `collection`, `snippet`, and `route` ACL metadata variants when the resource
61
- should participate in NocoBase permission checks.
73
+ The route runtime marks generated non-collection resources as authenticated.
74
+ Use the `collection`, `snippet`, and `route` ACL metadata variants when a
75
+ resource should also participate in NocoBase permission checks.
62
76
 
63
77
  The underlying `accessControlProvider`, `roles:check` cache, record permissions,
64
78
  and Refine integration are provided by the compatible Starter.
@@ -9,7 +9,13 @@ import {
9
9
  UserRoundCog,
10
10
  UsersRound,
11
11
  } from "lucide-react";
12
+ import type { TreeMenuItem } from "@refinedev/core";
12
13
 
14
+ import {
15
+ buildRouteResources,
16
+ defineAppRoutes,
17
+ RouteAccessGuard,
18
+ } from "@/app/route-runtime";
13
19
  import { Badge } from "@/components/ui/badge";
14
20
  import { Button } from "@/components/ui/button";
15
21
  import { CanAccess } from "@/components/access-control/can-access";
@@ -21,7 +27,11 @@ import {
21
27
  CardTitle,
22
28
  } from "@/components/ui/card";
23
29
  import { Input } from "@/components/ui/input";
24
- import type { RoleConstraint } from "@/lib/nocobase/acl";
30
+ import {
31
+ filterMenuItemsByAcl,
32
+ type RoleConstraint,
33
+ useAclState,
34
+ } from "@/lib/nocobase/acl";
25
35
  import { AclField, AclPage, AclRegion } from "../components/acl-boundary";
26
36
  import { AclPreviewProvider } from "./acl-preview-provider";
27
37
  import { AclBoundaryApi } from "./boundary-api";
@@ -58,6 +68,53 @@ const usersRouteRoles = {
58
68
  anyOf: ["admin"],
59
69
  } satisfies RoleConstraint;
60
70
 
71
+ const navigationDemoRoutes = defineAppRoutes([
72
+ {
73
+ name: "dashboard",
74
+ path: "/dashboard",
75
+ resource: {
76
+ meta: {
77
+ label: "Dashboard",
78
+ icon: <LayoutPanelTop />,
79
+ },
80
+ },
81
+ },
82
+ {
83
+ name: "users",
84
+ path: "/users",
85
+ resource: {
86
+ meta: {
87
+ label: "Users",
88
+ icon: <UsersRound />,
89
+ },
90
+ },
91
+ access: {
92
+ roles: usersRouteRoles,
93
+ },
94
+ },
95
+ {
96
+ name: "roles",
97
+ path: "/roles",
98
+ resource: {
99
+ meta: {
100
+ label: "Roles",
101
+ icon: <UserRoundCog />,
102
+ },
103
+ },
104
+ },
105
+ ]);
106
+
107
+ const navigationDemoResources = buildRouteResources(navigationDemoRoutes);
108
+ const navigationDemoMenuItems: TreeMenuItem[] = navigationDemoResources.map(
109
+ (resource) => ({
110
+ key: resource.name,
111
+ name: resource.name,
112
+ route: typeof resource.list === "string" ? resource.list : undefined,
113
+ meta: resource.meta,
114
+ children: [],
115
+ })
116
+ );
117
+
61
118
  const allowedRegionPermissions = {
62
119
  "users:list": {},
63
120
  "departments:list": {},
@@ -127,12 +184,12 @@ export function AclPatternsPage() {
127
184
  defaultScene: "User and role administration",
128
185
  defaultTarget:
129
186
  "Allow administrators to open Users, but remove it from navigation and block its direct URL for other roles.",
130
- requirements: `- Put role constraints on the route resource through meta.acl.roles using anyOf, allOf, or noneOf.
131
- - Use meta.acl.type = "authenticated" when the route itself is role-controlled and its inner regions perform collection checks.
132
- - Let the Starter filter inaccessible sidebar items and choose the first accessible default route.
133
- - Wrap list, create, show, and edit routes with the Starter CanAccess and provide an AccessDenied fallback.
134
- - Mark non-collection utility pages as authenticated, snippet, route, or acl: false instead of treating them as collections.
135
- - If a parent route is denied but an accessible child remains, navigation must continue to the child route.`,
187
+ requirements: `- Define application-owned navigation once through defineAppRoutes in src/routes.tsx.
188
+ - Put role constraints in the route access.roles field using anyOf, allOf, or noneOf.
189
+ - Let the route runtime generate the Refine resource, filter the sidebar item, and wrap the direct route with CanAccess.
190
+ - Do not repeat the same role list in Resource meta or a manually written route guard.
191
+ - Keep resource/action, record, region, and field permissions in their dedicated ACL boundaries.
192
+ - Let the Starter choose the first accessible route after inaccessible leaves and empty groups are removed.`,
136
193
  }}
137
194
  >
138
195
  <OutcomeComparison
@@ -343,21 +400,36 @@ function OutcomeComparison({
343
400
  }
344
401
 
345
402
  function NavigationPreview() {
403
+ const acl = useAclState();
404
+ const menuItems =
405
+ acl.status === "ready"
406
+ ? filterMenuItemsByAcl(navigationDemoMenuItems, acl.permissions)
407
+ : [];
408
+ const usersVisible = menuItems.some((item) => item.name === "users");
409
+ const usersRoute = navigationDemoRoutes.find(
410
+ (route) => route.name === "users"
411
+ );
412
+
346
413
  return (
347
414
  <div className="grid min-h-48 grid-cols-[150px_minmax(0,1fr)] overflow-hidden rounded-xl border bg-background">
348
415
  <aside className="border-r bg-muted/35 p-3">
349
416
  <div className="mb-3 text-xs font-semibold text-muted-foreground">
350
417
  Workspace
351
418
  </div>
352
- <NavItem icon={<LayoutPanelTop />} label="Dashboard" />
353
- <CanAccess roles={usersRouteRoles}>
354
- <NavItem icon={<UsersRound />} label="Users" active />
355
- </CanAccess>
356
- <NavItem icon={<UserRoundCog />} label="Roles" />
419
+ {menuItems.map((item) => (
420
+ <NavItem
421
+ key={item.name}
422
+ icon={item.meta?.icon}
423
+ label={String(item.meta?.label ?? item.name)}
424
+ active={
425
+ usersVisible ? item.name === "users" : item.name === "dashboard"
426
+ }
427
+ />
428
+ ))}
357
429
  </aside>
358
430
  <div className="flex items-center justify-center p-5 text-center">
359
- <CanAccess
360
- roles={usersRouteRoles}
431
+ <RouteAccessGuard
432
+ access={usersRoute?.access}
361
433
  fallback={
362
434
  <div>
363
435
  <LayoutPanelTop className="mx-auto size-7 text-muted-foreground" />
@@ -375,7 +447,7 @@ function NavigationPreview() {
375
447
  Route is available
376
448
  </div>
377
449
  </div>
378
- </CanAccess>
450
+ </RouteAccessGuard>
379
451
  </div>
380
452
  </div>
381
453
  );
@@ -45,6 +45,7 @@ Implementation contract
45
45
  - Use the Starter's built-in NocoBase accessControlProvider and ACL store.
46
46
  - NocoBase roles:check is the source of truth. Do not create a second permission store. Use configured role names only when the requirement explicitly includes a role constraint.
47
47
  - Import the Starter CanAccess from @/components/access-control/can-access. Use it with AclPage, AclRegion, and AclField for permission presentation. Do not copy their checks into local booleans or role comparisons.
48
+ - For route-level role access, define the route once with defineAppRoutes in src/routes.tsx and put the constraint in access.roles. Let the route runtime control both its menu item and direct URL.
48
49
  - Use useGetRoles only when UI needs to display or pass through the current effective ACL roles; it is not a replacement permission evaluator.
49
50
  - Keep backend ACL enforcement in place; frontend checks only control presentation and navigation.
50
51
  - Import reusable components from the installed local entry point at @/extensions/nocobase-acl when page composition is needed.