@nocobase/portal-template-default 1.0.8 → 1.0.10

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 (262) hide show
  1. package/.env.example +5 -0
  2. package/.github/workflows/publish.yml +106 -0
  3. package/.gitignore +33 -0
  4. package/AGENTS.md +19 -0
  5. package/README.MD +61 -180
  6. package/package.json +3 -5
  7. package/scripts/build-html.mjs +12 -0
  8. package/src/App.css +2 -0
  9. package/src/App.tsx +35 -186
  10. package/src/app/development.tsx +154 -0
  11. package/src/app/extension.ts +5 -0
  12. package/src/app/extensions.tsx +18 -4
  13. package/src/app/routes.tsx +65 -0
  14. package/src/components/access-control/acl-bootstrap.tsx +11 -3
  15. package/src/components/access-control/acl-gate.tsx +7 -6
  16. package/src/components/access-control/can-access.tsx +17 -0
  17. package/src/components/access-control/navigate-to-accessible-resource.tsx +8 -3
  18. package/src/components/app-shell/sidebar.tsx +21 -5
  19. package/src/components/auth/auto-redirect-provider.tsx +2 -1
  20. package/src/extensions/nocobase-acl/README.md +74 -0
  21. package/{registry → src/extensions}/nocobase-acl/components/acl-boundary.tsx +22 -25
  22. package/{registry → src/extensions}/nocobase-acl/components/role-switcher.tsx +26 -23
  23. package/src/extensions/nocobase-acl/demo/acl-preview-provider.tsx +81 -0
  24. package/{registry → src/extensions}/nocobase-acl/demo/boundary-api.tsx +58 -0
  25. package/{registry → src/extensions}/nocobase-acl/demo/index.tsx +243 -105
  26. package/{registry → src/extensions}/nocobase-acl/demo/prompt-generator.tsx +3 -1
  27. package/src/extensions/nocobase-acl/extension.tsx +73 -0
  28. package/{registry → src/extensions}/nocobase-ai/README.md +21 -5
  29. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-composer.tsx +3 -1
  30. package/{registry → src/extensions}/nocobase-ai/components/chat/chat-messages.tsx +38 -3
  31. package/{registry → src/extensions}/nocobase-ai/components/chat/tool-call-card.tsx +1 -9
  32. package/{registry → src/extensions}/nocobase-ai/components/index.ts +5 -0
  33. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +17 -0
  34. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +35 -0
  35. package/src/extensions/nocobase-ai/components/surfaces/chat-surface.tsx +108 -0
  36. package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +2 -2
  37. package/{registry → src/extensions}/nocobase-ai/demo/configuration-gate.tsx +4 -1
  38. package/{registry → src/extensions}/nocobase-ai/demo/container-showcase.tsx +2 -2
  39. package/{registry → src/extensions}/nocobase-ai/demo/floating.tsx +45 -15
  40. package/{registry → src/extensions}/nocobase-ai/demo/index.tsx +116 -74
  41. package/{registry → src/extensions}/nocobase-ai/demo/prompt-generator.tsx +3 -3
  42. package/{registry → src/extensions}/nocobase-ai/demo/tool-cards.tsx +2 -3
  43. package/src/extensions/nocobase-ai/extension.tsx +160 -0
  44. package/{registry → src/extensions}/nocobase-ai/global-ai-chat.tsx +12 -28
  45. package/{registry → src/extensions}/nocobase-ai/providers/ai-provider.tsx +14 -1
  46. package/{registry → src/extensions}/nocobase-ai/providers/chat-context.tsx +1 -0
  47. package/{registry → src/extensions}/nocobase-ai/providers/chat-provider.tsx +29 -8
  48. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-attachments.ts +6 -1
  49. package/{registry → src/extensions}/nocobase-ai/providers/use-chat-work-context.ts +4 -1
  50. package/{registry → src/extensions}/nocobase-route-surfaces/README.md +2 -1
  51. package/{registry → src/extensions}/nocobase-route-surfaces/demo/index.tsx +33 -7
  52. package/{registry → src/extensions}/nocobase-route-surfaces/demo/scenarios.ts +20 -14
  53. package/{registry → src/extensions}/nocobase-route-surfaces/extension.tsx +55 -52
  54. package/src/{pages/users → extensions/nocobase-users-example}/create.tsx +4 -4
  55. package/src/{pages/users → extensions/nocobase-users-example}/edit.tsx +5 -9
  56. package/src/extensions/nocobase-users-example/extension.tsx +109 -0
  57. package/src/{pages/users → extensions/nocobase-users-example}/form-context.ts +11 -9
  58. package/src/extensions/nocobase-users-example/layout.tsx +16 -0
  59. package/src/{pages/users → extensions/nocobase-users-example}/list.tsx +1 -1
  60. package/src/extensions/nocobase-users-example/locales/en-US.ts +77 -0
  61. package/src/extensions/nocobase-users-example/locales/index.ts +8 -0
  62. package/src/extensions/nocobase-users-example/locales/zh-CN.ts +73 -0
  63. package/src/extensions/nocobase-users-example/optional-ai.ts +67 -0
  64. package/src/{pages/users → extensions/nocobase-users-example}/role-detail.tsx +4 -15
  65. package/src/{pages/users → extensions/nocobase-users-example}/routes.ts +1 -4
  66. package/src/extensions/nocobase-users-example/show.tsx +266 -0
  67. package/src/lib/nocobase/acl/context.ts +32 -0
  68. package/src/lib/nocobase/acl/evaluator.ts +186 -0
  69. package/src/lib/nocobase/acl/hooks.ts +40 -0
  70. package/src/lib/nocobase/acl/index.ts +4 -1
  71. package/src/lib/nocobase/acl/menu.ts +6 -6
  72. package/src/lib/nocobase/acl/record-permissions.ts +37 -7
  73. package/src/lib/nocobase/acl/store-provider.tsx +14 -0
  74. package/src/lib/nocobase/acl/store.ts +92 -86
  75. package/src/lib/nocobase/acl/types.ts +52 -32
  76. package/src/lib/nocobase/auth-session.ts +128 -0
  77. package/src/lib/nocobase/client.ts +48 -50
  78. package/src/locales/en-US.ts +0 -78
  79. package/src/locales/index.ts +2 -7
  80. package/src/locales/zh-CN.ts +0 -74
  81. package/src/providers/access-control.ts +43 -8
  82. package/src/providers/auth-callback.ts +1 -0
  83. package/src/providers/auth.ts +24 -26
  84. package/src/providers/constants.ts +0 -4
  85. package/src/providers/data.ts +14 -26
  86. package/src/providers/runtime-config.ts +40 -0
  87. package/src/vite-env.d.ts +9 -0
  88. package/tests/auth-session-regression.mjs +101 -0
  89. package/tsconfig.json +2 -1
  90. package/vite.config.ts +9 -1
  91. package/registry/nocobase-acl/README.md +0 -19
  92. package/registry/nocobase-acl/extension.tsx +0 -71
  93. package/registry/nocobase-acl/tests/acl-regression.mjs +0 -314
  94. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  95. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  96. package/registry/nocobase-ai/extension.tsx +0 -158
  97. package/registry/nocobase-ai/tests/business-report-regression.mjs +0 -70
  98. package/registry/nocobase-ai/tests/form-filler-regression.mjs +0 -129
  99. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +0 -128
  100. package/registry/nocobase-ai/tests/page-context-regression.mjs +0 -48
  101. package/registry/nocobase-ai/tests/stream-regression.mjs +0 -207
  102. package/registry/nocobase-auth-cas/README.md +0 -6
  103. package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +0 -41
  104. package/registry/nocobase-auth-cas/demo.tsx +0 -20
  105. package/registry/nocobase-auth-cas/extension.tsx +0 -46
  106. package/registry/nocobase-auth-cas/index.ts +0 -1
  107. package/registry/nocobase-auth-cas/use-cas-sign-in.ts +0 -29
  108. package/registry/nocobase-auth-dingtalk/README.md +0 -7
  109. package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +0 -154
  110. package/registry/nocobase-auth-dingtalk/demo.tsx +0 -23
  111. package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +0 -45
  112. package/registry/nocobase-auth-dingtalk/extension.tsx +0 -55
  113. package/registry/nocobase-auth-dingtalk/index.ts +0 -4
  114. package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +0 -64
  115. package/registry/nocobase-auth-ldap/README.md +0 -6
  116. package/registry/nocobase-auth-ldap/demo.tsx +0 -21
  117. package/registry/nocobase-auth-ldap/extension.tsx +0 -46
  118. package/registry/nocobase-auth-ldap/index.ts +0 -1
  119. package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +0 -69
  120. package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +0 -5
  121. package/registry/nocobase-auth-oidc/README.md +0 -11
  122. package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +0 -13
  123. package/registry/nocobase-auth-oidc/demo.tsx +0 -20
  124. package/registry/nocobase-auth-oidc/extension.tsx +0 -51
  125. package/registry/nocobase-auth-oidc/index.ts +0 -1
  126. package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +0 -44
  127. package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +0 -58
  128. package/registry/nocobase-auth-saml/README.md +0 -6
  129. package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +0 -13
  130. package/registry/nocobase-auth-saml/demo.tsx +0 -20
  131. package/registry/nocobase-auth-saml/extension.tsx +0 -51
  132. package/registry/nocobase-auth-saml/index.ts +0 -1
  133. package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +0 -43
  134. package/registry/nocobase-auth-saml/use-saml-sign-in.ts +0 -51
  135. package/registry/nocobase-auth-sms/README.md +0 -7
  136. package/registry/nocobase-auth-sms/demo.tsx +0 -25
  137. package/registry/nocobase-auth-sms/extension.tsx +0 -46
  138. package/registry/nocobase-auth-sms/index.ts +0 -1
  139. package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +0 -107
  140. package/registry/nocobase-auth-sms/use-sms-sign-in.ts +0 -88
  141. package/registry/nocobase-auth-wecom/README.md +0 -6
  142. package/registry/nocobase-auth-wecom/auto-login-provider.tsx +0 -21
  143. package/registry/nocobase-auth-wecom/demo.tsx +0 -23
  144. package/registry/nocobase-auth-wecom/extension.tsx +0 -49
  145. package/registry/nocobase-auth-wecom/index.ts +0 -1
  146. package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +0 -62
  147. package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +0 -70
  148. package/registry/nocobase-i18n/README.md +0 -44
  149. package/registry/nocobase-i18n/components/index.ts +0 -1
  150. package/registry/nocobase-i18n/components/language-switcher.tsx +0 -138
  151. package/registry/nocobase-i18n/demo/index.tsx +0 -209
  152. package/registry/nocobase-i18n/demo/prompt-generator.tsx +0 -132
  153. package/registry/nocobase-i18n/extension.tsx +0 -45
  154. package/registry/nocobase-i18n/index.ts +0 -7
  155. package/registry/nocobase-i18n/locales/en-US.ts +0 -39
  156. package/registry/nocobase-i18n/locales/index.ts +0 -8
  157. package/registry/nocobase-i18n/locales/zh-CN.ts +0 -35
  158. package/registry/nocobase-i18n/provider.tsx +0 -44
  159. package/registry/nocobase-i18n/server-resources.ts +0 -166
  160. package/registry/nocobase-i18n/tests/i18n-regression.mjs +0 -51
  161. package/registry.config.json +0 -302
  162. package/scripts/registry.mjs +0 -164
  163. package/src/components/access-control/resource-access-guard.tsx +0 -21
  164. package/src/components/access-control/use-acl-runtime.ts +0 -28
  165. package/src/lib/nocobase/acl/action.ts +0 -147
  166. package/src/pages/users/layout.tsx +0 -15
  167. package/src/pages/users/show.tsx +0 -278
  168. /package/{registry → src/extensions}/nocobase-acl/components/index.ts +0 -0
  169. /package/{registry → src/extensions}/nocobase-acl/components/role-options.ts +0 -0
  170. /package/{registry → src/extensions}/nocobase-acl/demo/components.tsx +0 -0
  171. /package/{registry → src/extensions}/nocobase-acl/demo/lazy-route.tsx +0 -0
  172. /package/{registry → src/extensions}/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -0
  173. /package/{registry → src/extensions}/nocobase-acl/demo/scenario-section.tsx +0 -0
  174. /package/{registry → src/extensions}/nocobase-acl/index.ts +0 -0
  175. /package/{registry → src/extensions}/nocobase-ai/adapters/react-hook-form.ts +0 -0
  176. /package/{registry → src/extensions}/nocobase-ai/assets/nocobase-ai-chat.svg +0 -0
  177. /package/{registry → src/extensions}/nocobase-ai/components/ai-root-provider.tsx +0 -0
  178. /package/{registry → src/extensions}/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -0
  179. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-attachment.tsx +0 -0
  180. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-compact.tsx +0 -0
  181. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-empty-state.tsx +0 -0
  182. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-header.tsx +0 -0
  183. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -0
  184. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-message.tsx +0 -0
  185. /package/{registry → src/extensions}/nocobase-ai/components/chat/chat-window.tsx +0 -0
  186. /package/{registry → src/extensions}/nocobase-ai/components/chat/conversation-list.tsx +0 -0
  187. /package/{registry → src/extensions}/nocobase-ai/components/chat/markdown-message.tsx +0 -0
  188. /package/{registry → src/extensions}/nocobase-ai/components/chat/model-select-options.tsx +0 -0
  189. /package/{registry → src/extensions}/nocobase-ai/components/chat/reasoning-panel.tsx +0 -0
  190. /package/{registry → src/extensions}/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -0
  191. /package/{registry → src/extensions}/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -0
  192. /package/{registry → src/extensions}/nocobase-ai/components/chat/work-context-chip.tsx +0 -0
  193. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/ai-form.tsx +0 -0
  194. /package/{registry → src/extensions}/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -0
  195. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-inline.tsx +0 -0
  196. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-page.tsx +0 -0
  197. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -0
  198. /package/{registry → src/extensions}/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -0
  199. /package/{registry → src/extensions}/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -0
  200. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-dialog.tsx +0 -0
  201. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-renderer.tsx +0 -0
  202. /package/{registry → src/extensions}/nocobase-ai/components/tools/business-report-utils.tsx +0 -0
  203. /package/{registry → src/extensions}/nocobase-ai/components/tools/chart-renderer.tsx +0 -0
  204. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-preview.tsx +0 -0
  205. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -0
  206. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -0
  207. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -0
  208. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -0
  209. /package/{registry → src/extensions}/nocobase-ai/components/tools/echarts-runtime.ts +0 -0
  210. /package/{registry → src/extensions}/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -0
  211. /package/{registry → src/extensions}/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -0
  212. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -0
  213. /package/{registry → src/extensions}/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -0
  214. /package/{registry → src/extensions}/nocobase-ai/components/tools/workflow-renderer.tsx +0 -0
  215. /package/{registry → src/extensions}/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -0
  216. /package/{registry → src/extensions}/nocobase-ai/demo/interaction-showcase.tsx +0 -0
  217. /package/{registry → src/extensions}/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -0
  218. /package/{registry → src/extensions}/nocobase-ai/demo/page-context.tsx +0 -0
  219. /package/{registry → src/extensions}/nocobase-ai/demo/page-element-showcase.tsx +0 -0
  220. /package/{registry → src/extensions}/nocobase-ai/demo/prompt-card.tsx +0 -0
  221. /package/{registry → src/extensions}/nocobase-ai/demo/shortcut.tsx +0 -0
  222. /package/{registry → src/extensions}/nocobase-ai/index.ts +0 -0
  223. /package/{registry → src/extensions}/nocobase-ai/providers/avatars.ts +0 -0
  224. /package/{registry → src/extensions}/nocobase-ai/providers/chat-controller.ts +0 -0
  225. /package/{registry → src/extensions}/nocobase-ai/providers/chat-message-utils.ts +0 -0
  226. /package/{registry → src/extensions}/nocobase-ai/providers/chat-reducer.ts +0 -0
  227. /package/{registry → src/extensions}/nocobase-ai/providers/chat-task-utils.ts +0 -0
  228. /package/{registry → src/extensions}/nocobase-ai/providers/chat-transport.ts +0 -0
  229. /package/{registry → src/extensions}/nocobase-ai/providers/form-registry.tsx +0 -0
  230. /package/{registry → src/extensions}/nocobase-ai/providers/frontend-tool-registry.tsx +0 -0
  231. /package/{registry → src/extensions}/nocobase-ai/providers/index.ts +0 -0
  232. /package/{registry → src/extensions}/nocobase-ai/providers/model.ts +0 -0
  233. /package/{registry → src/extensions}/nocobase-ai/providers/page-context.tsx +0 -0
  234. /package/{registry → src/extensions}/nocobase-ai/providers/stream-coalescer.ts +0 -0
  235. /package/{registry → src/extensions}/nocobase-ai/providers/stream-event-utils.ts +0 -0
  236. /package/{registry → src/extensions}/nocobase-ai/providers/stream-parser.ts +0 -0
  237. /package/{registry → src/extensions}/nocobase-ai/providers/sub-agent-stream.ts +0 -0
  238. /package/{registry → src/extensions}/nocobase-ai/providers/types.ts +0 -0
  239. /package/{registry → src/extensions}/nocobase-ai/providers/ui-message-stream.ts +0 -0
  240. /package/{registry → src/extensions}/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -0
  241. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-message-actions.ts +0 -0
  242. /package/{registry → src/extensions}/nocobase-ai/providers/use-chat-runtime.ts +0 -0
  243. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-catalog.ts +0 -0
  244. /package/{registry → src/extensions}/nocobase-ai/providers/use-conversation-history.ts +0 -0
  245. /package/{registry → src/extensions}/nocobase-ai/services/index.ts +0 -0
  246. /package/{registry → src/extensions}/nocobase-ai/services/nocobase-ai-service.ts +0 -0
  247. /package/{registry → src/extensions}/nocobase-ai/services/types.ts +0 -0
  248. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/lazy-route.tsx +0 -0
  249. /package/{registry → src/extensions}/nocobase-route-surfaces/demo/prompt-generator.tsx +0 -0
  250. /package/{registry → src/extensions}/nocobase-route-surfaces/index.ts +0 -0
  251. /package/{registry → src/extensions}/nocobase-route-surfaces/route-dialog.tsx +0 -0
  252. /package/{registry → src/extensions}/nocobase-route-surfaces/route-drawer.tsx +0 -0
  253. /package/{registry → src/extensions}/nocobase-route-surfaces/route-page.tsx +0 -0
  254. /package/{registry → src/extensions}/nocobase-route-surfaces/route-surface-context.ts +0 -0
  255. /package/{registry → src/extensions}/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +0 -0
  256. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-close.ts +0 -0
  257. /package/{registry → src/extensions}/nocobase-route-surfaces/use-route-surface-state.ts +0 -0
  258. /package/src/{pages/users → extensions/nocobase-users-example}/form-fields.tsx +0 -0
  259. /package/src/{pages/users → extensions/nocobase-users-example}/index.ts +0 -0
  260. /package/src/{pages/users → extensions/nocobase-users-example}/role-badges.tsx +0 -0
  261. /package/src/{pages/users → extensions/nocobase-users-example}/role-utils.ts +0 -0
  262. /package/src/{pages/users → extensions/nocobase-users-example}/types.ts +0 -0
@@ -3,6 +3,7 @@ import { useLocation } from "react-router";
3
3
 
4
4
  import { LoadingState } from "@/components/app-shell/loading-state";
5
5
  import { nocobaseClient } from "@/lib/nocobase/client";
6
+ import { resolvePortalUrl } from "@/providers/runtime-config";
6
7
 
7
8
  type AutoRedirectResponse = {
8
9
  success?: boolean;
@@ -32,7 +33,7 @@ export function AuthAutoRedirectProvider({
32
33
  }: AuthAutoRedirectProviderProps) {
33
34
  const { pathname, search } = useLocation();
34
35
  const attemptedRef = useRef(false);
35
- const redirect = `${pathname}${search}`;
36
+ const redirect = resolvePortalUrl(`${pathname}${search}`);
36
37
  const shouldCheck =
37
38
  enabled &&
38
39
  !nocobaseClient.getToken() &&
@@ -0,0 +1,74 @@
1
+ # NocoBase ACL UI
2
+
3
+ Reusable ACL composition components for the NocoBase Refine starter.
4
+
5
+ After installation, import the components from `@/extensions/nocobase-acl`.
6
+
7
+ - The Starter's `CanAccess` supports role, resource, record, and field checks.
8
+ - `AclPage` combines page-level permissions.
9
+ - `AclRegion` hides or replaces individual data regions.
10
+ - `AclField` applies NocoBase field whitelists.
11
+ - `RoleSwitcher` can be placed in any application surface. It supports
12
+ normal roles, Anonymous when enabled, and Full permissions through the
13
+ `__union__` role when the system role mode allows it.
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.
20
+
21
+ Use the Starter's `useGetRoles()` hook when application UI needs the same
22
+ effective role names. Its result follows the familiar Refine query shape with
23
+ `data`, `isLoading`, `isPending`, `isError`, and `error`. `data` is the active
24
+ ACL role set, not every role assigned to the signed-in user.
25
+
26
+ ```tsx
27
+ import { useGetRoles } from "@/lib/nocobase/acl";
28
+
29
+ function CurrentAccessContext() {
30
+ const { data: roles, isLoading } = useGetRoles();
31
+
32
+ if (isLoading) return null;
33
+ return <span>Effective roles: {roles?.join(", ")}</span>;
34
+ }
35
+ ```
36
+
37
+ ```tsx
38
+ const adminRoute = {
39
+ name: "administration",
40
+ meta: {
41
+ acl: {
42
+ type: "authenticated",
43
+ roles: { anyOf: ["admin"] },
44
+ },
45
+ },
46
+ };
47
+
48
+ function AuditLogsRoute() {
49
+ return (
50
+ <AclPage roles={{ anyOf: ["admin", "auditor"] }}>
51
+ <AclRegion resource="auditLogs" action="list">
52
+ <AuditLogTable />
53
+ </AclRegion>
54
+ </AclPage>
55
+ );
56
+ }
57
+ ```
58
+
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.
62
+
63
+ The underlying `accessControlProvider`, `roles:check` cache, record permissions,
64
+ and Refine integration are provided by the compatible Starter.
65
+
66
+ The Demo uses the real `CanAccess`, `AclPage`, `AclRegion`, and `AclField`
67
+ components. A local `AclPreviewProvider` supplies a static in-memory ACL store
68
+ so both allowed and restricted outcomes remain visible without changing the
69
+ current user's real roles. It does not replace Refine's access-control context.
70
+ Applications use the Starter's root `AclStoreProvider` and production `aclStore`.
71
+
72
+ Global ACL permissions remain fixed for one page session. Switching roles saves
73
+ the selected role and reloads the page so navigation, routes, data access, and
74
+ Refine permission queries all start from the same `roles:check` result.
@@ -2,10 +2,8 @@ import type { BaseKey } from "@refinedev/core";
2
2
  import type { PropsWithChildren, ReactNode } from "react";
3
3
 
4
4
  import { AccessDenied } from "@/components/access-control/access-denied";
5
- import {
6
- canAccessWithSnapshot,
7
- useAclSnapshot,
8
- } from "@/lib/nocobase/acl";
5
+ import { useAclEvaluator } from "@/lib/nocobase/acl";
6
+ import type { RoleConstraint } from "@/lib/nocobase/acl";
9
7
 
10
8
  export type AclPermission = {
11
9
  resource: string;
@@ -18,6 +16,7 @@ export type AclPermission = {
18
16
  export type AclPageProps = PropsWithChildren<{
19
17
  anyOf?: AclPermission[];
20
18
  allOf?: AclPermission[];
19
+ roles?: RoleConstraint;
21
20
  fallback?: ReactNode;
22
21
  }>;
23
22
 
@@ -35,31 +34,29 @@ export type AclFieldProps = PropsWithChildren<{
35
34
  fallback?: ReactNode;
36
35
  }>;
37
36
 
38
- const canAccess = (
39
- snapshot: ReturnType<typeof useAclSnapshot>,
40
- permission: AclPermission
41
- ) =>
42
- canAccessWithSnapshot(snapshot, {
43
- resource: permission.resource,
44
- action: permission.action,
45
- params: {
46
- id: permission.id,
47
- field: permission.field,
48
- dataSourceKey: permission.dataSourceKey,
49
- },
50
- });
37
+ const toAccessRequest = (permission: AclPermission) => ({
38
+ resource: permission.resource,
39
+ action: permission.action,
40
+ id: permission.id,
41
+ field: permission.field,
42
+ dataSourceKey: permission.dataSourceKey,
43
+ });
51
44
 
52
45
  export function AclPage({
53
46
  children,
54
47
  anyOf,
55
48
  allOf,
49
+ roles,
56
50
  fallback = <AccessDenied />,
57
51
  }: AclPageProps) {
58
- const snapshot = useAclSnapshot();
59
- const anyAllowed = !anyOf?.length || anyOf.some((item) => canAccess(snapshot, item));
60
- const allAllowed = !allOf?.length || allOf.every((item) => canAccess(snapshot, item));
52
+ const canAccess = useAclEvaluator();
53
+ const anyAllowed =
54
+ !anyOf?.length || anyOf.some((item) => canAccess(toAccessRequest(item)));
55
+ const allAllowed =
56
+ !allOf?.length || allOf.every((item) => canAccess(toAccessRequest(item)));
57
+ const roleAllowed = canAccess({ roles });
61
58
 
62
- return anyAllowed && allAllowed ? children : fallback;
59
+ return roleAllowed && anyAllowed && allAllowed ? children : fallback;
63
60
  }
64
61
 
65
62
  export function AclRegion({
@@ -70,8 +67,8 @@ export function AclRegion({
70
67
  dataSourceKey,
71
68
  fallback = "hidden",
72
69
  }: AclRegionProps) {
73
- const snapshot = useAclSnapshot();
74
- const allowed = canAccess(snapshot, {
70
+ const canAccess = useAclEvaluator();
71
+ const allowed = canAccess({
75
72
  resource,
76
73
  action,
77
74
  id,
@@ -94,8 +91,8 @@ export function AclField({
94
91
  dataSourceKey,
95
92
  fallback = null,
96
93
  }: AclFieldProps) {
97
- const snapshot = useAclSnapshot();
98
- return canAccess(snapshot, {
94
+ const canAccess = useAclEvaluator();
95
+ return canAccess({
99
96
  resource,
100
97
  action,
101
98
  field,
@@ -13,7 +13,7 @@ import {
13
13
  } from "@/components/ui/select";
14
14
  import {
15
15
  switchRole,
16
- useAclSnapshot,
16
+ useAclState,
17
17
  type AclIdentity,
18
18
  type Role,
19
19
  } from "@/lib/nocobase/acl";
@@ -34,24 +34,41 @@ export function RoleSwitcher({
34
34
  showWhenUnavailable = false,
35
35
  }: RoleSwitcherProps) {
36
36
  const { data: identity, isLoading } = useGetIdentity<AclIdentity>();
37
- const acl = useAclSnapshot();
37
+ const acl = useAclState();
38
38
  const [switching, setSwitching] = useState(false);
39
39
  const [error, setError] = useState<string>();
40
+ const permissions = acl.status === "ready" ? acl.permissions : undefined;
40
41
 
41
42
  const roles = useMemo(
42
43
  () =>
43
44
  getRoleOptions({
44
45
  roles: identity?.roles ?? [],
45
- roleMode: acl.data.roleMode,
46
- allowAnonymous: acl.data.allowAnonymous,
46
+ roleMode: permissions?.roleMode,
47
+ allowAnonymous: permissions?.allowAnonymous,
47
48
  }),
48
- [acl.data.allowAnonymous, acl.data.roleMode, identity?.roles]
49
+ [identity?.roles, permissions?.allowAnonymous, permissions?.roleMode]
49
50
  );
50
51
 
51
- const currentRole = acl.data.role ?? roles[0]?.name;
52
- const canSwitch = roles.length > 1 && acl.data.roleMode !== "only-use-union";
52
+ const currentRole = permissions?.currentRole ?? roles[0]?.name;
53
+ const canSwitch =
54
+ roles.length > 1 && permissions?.roleMode !== "only-use-union";
53
55
 
54
- if (isLoading) {
56
+ const handleRoleChange = async (value: string | null) => {
57
+ if (!value || value === currentRole) return;
58
+ setSwitching(true);
59
+ setError(undefined);
60
+ try {
61
+ await switchRole(value);
62
+ window.location.reload();
63
+ } catch (reason) {
64
+ setError(
65
+ reason instanceof Error ? reason.message : "Unable to switch role"
66
+ );
67
+ setSwitching(false);
68
+ }
69
+ };
70
+
71
+ if (isLoading || acl.status === "idle" || acl.status === "loading") {
55
72
  return <Loader2 className="size-4 animate-spin text-muted-foreground" />;
56
73
  }
57
74
  if (!canSwitch && !showWhenUnavailable) return null;
@@ -74,21 +91,7 @@ export function RoleSwitcher({
74
91
  <Select
75
92
  value={currentRole}
76
93
  disabled={switching}
77
- onValueChange={(value) => {
78
- if (!value || value === currentRole) return;
79
- setSwitching(true);
80
- setError(undefined);
81
- void switchRole(value, { reloadAcl: false })
82
- .then(() => window.location.reload())
83
- .catch((reason) => {
84
- setError(
85
- reason instanceof Error
86
- ? reason.message
87
- : "Unable to switch role"
88
- );
89
- setSwitching(false);
90
- });
91
- }}
94
+ onValueChange={handleRoleChange}
92
95
  >
93
96
  <SelectTrigger
94
97
  className={cn("w-full min-w-52", triggerClassName)}
@@ -0,0 +1,81 @@
1
+ import { useMemo, type PropsWithChildren } from "react";
2
+
3
+ import {
4
+ AclStoreProvider,
5
+ type AclActionParams,
6
+ type AclState,
7
+ type AclStore,
8
+ } from "@/lib/nocobase/acl";
9
+
10
+ type PreviewRecordPermission = {
11
+ dataSourceKey?: string;
12
+ resource: string;
13
+ action: string;
14
+ id: string | number;
15
+ allowed: boolean;
16
+ };
17
+
18
+ export type AclPreviewProviderProps = PropsWithChildren<{
19
+ roles: string[];
20
+ permissions?: Record<string, AclActionParams>;
21
+ recordPermissions?: PreviewRecordPermission[];
22
+ }>;
23
+
24
+ export function AclPreviewProvider({
25
+ children,
26
+ roles,
27
+ permissions = {},
28
+ recordPermissions = [],
29
+ }: AclPreviewProviderProps) {
30
+ const store = useMemo<AclStore>(() => {
31
+ const resources = Array.from(
32
+ new Set(
33
+ Object.keys(permissions).map((permission) =>
34
+ permission.slice(0, permission.lastIndexOf(":"))
35
+ )
36
+ )
37
+ );
38
+ const state: AclState = {
39
+ status: "ready",
40
+ permissions: {
41
+ currentRole: roles.length > 1 ? "__union__" : roles[0],
42
+ roles,
43
+ roleMode: roles.length > 1 ? "allow-use-union" : "default",
44
+ resources,
45
+ actions: permissions,
46
+ actionAlias: {
47
+ list: "list",
48
+ get: "get",
49
+ create: "create",
50
+ update: "update",
51
+ destroy: "destroy",
52
+ },
53
+ snippets: [],
54
+ },
55
+ };
56
+ const recordState = new Map(
57
+ recordPermissions.map((permission) => [
58
+ `${permission.dataSourceKey ?? "main"}:${permission.resource}:${
59
+ permission.action
60
+ }:${permission.id}`,
61
+ permission.allowed,
62
+ ])
63
+ );
64
+
65
+ return {
66
+ getState: () => state,
67
+ subscribe: () => () => undefined,
68
+ load: async () => state,
69
+ retry: async () => state,
70
+ clear: () => undefined,
71
+ recordPermissions: {
72
+ getState: () => recordState,
73
+ subscribe: () => () => undefined,
74
+ getPermission: ({ dataSourceKey = "main", resource, action, id }) =>
75
+ recordState.get(`${dataSourceKey}:${resource}:${action}:${id}`),
76
+ },
77
+ };
78
+ }, [permissions, recordPermissions, roles]);
79
+
80
+ return <AclStoreProvider store={store}>{children}</AclStoreProvider>;
81
+ }
@@ -15,6 +15,33 @@ type PropRow = [
15
15
  description: string
16
16
  ];
17
17
 
18
+ const canAccessProps: PropRow[] = [
19
+ [
20
+ "roles",
21
+ "RoleConstraint",
22
+ "undefined",
23
+ "Checks the current effective roles with anyOf, allOf, and noneOf.",
24
+ ],
25
+ [
26
+ "resource / action",
27
+ "string",
28
+ "undefined",
29
+ "Checks a NocoBase resource action. When roles are also present, both checks must pass.",
30
+ ],
31
+ [
32
+ "id / field",
33
+ "BaseKey / string",
34
+ "undefined",
35
+ "Optionally narrows the decision to one record or field.",
36
+ ],
37
+ [
38
+ "fallback",
39
+ "ReactNode",
40
+ "null",
41
+ "Renders when the access request is denied.",
42
+ ],
43
+ ];
44
+
18
45
  const pageProps: PropRow[] = [
19
46
  [
20
47
  "anyOf",
@@ -28,6 +55,12 @@ const pageProps: PropRow[] = [
28
55
  "undefined",
29
56
  "Requires every listed permission before rendering the page.",
30
57
  ],
58
+ [
59
+ "roles",
60
+ "RoleConstraint",
61
+ "undefined",
62
+ "Requires the current effective roles to match anyOf, allOf, and noneOf constraints.",
63
+ ],
31
64
  [
32
65
  "fallback",
33
66
  "ReactNode",
@@ -100,6 +133,21 @@ const fieldProps: PropRow[] = [
100
133
  ["children", "ReactNode", "required", "The protected field or field group."],
101
134
  ];
102
135
 
136
+ const runtimeApis: PropRow[] = [
137
+ [
138
+ "AclStoreProvider",
139
+ "{ store, children }",
140
+ "required",
141
+ "Connects React ACL hooks and components to the application aclStore. The Starter mounts it at the application root.",
142
+ ],
143
+ [
144
+ "useGetRoles",
145
+ "() => QueryResult<string[]>",
146
+ "-",
147
+ "Returns the current effective ACL role names, including all participating roles in union mode.",
148
+ ],
149
+ ];
150
+
103
151
  export function AclBoundaryApi() {
104
152
  return (
105
153
  <section className="space-y-5 border-t pt-8">
@@ -116,6 +164,11 @@ export function AclBoundaryApi() {
116
164
  </p>
117
165
  </div>
118
166
  <div className="space-y-7">
167
+ <ComponentApi
168
+ title="CanAccess"
169
+ description="The Starter access boundary for role-only, resource-only, or combined permission checks."
170
+ rows={canAccessProps}
171
+ />
119
172
  <ComponentApi
120
173
  title="AclPage"
121
174
  description="Protects a complete route or business page. This corresponds to the Page permission pattern above."
@@ -131,6 +184,11 @@ export function AclBoundaryApi() {
131
184
  description="Protects one field or field group. This corresponds to the Field permission pattern."
132
185
  rows={fieldProps}
133
186
  />
187
+ <ComponentApi
188
+ title="ACL context APIs"
189
+ description="Read the active role context from the application ACL store. Demo previews use an isolated in-memory store with the same evaluator."
190
+ rows={runtimeApis}
191
+ />
134
192
  </div>
135
193
  </section>
136
194
  );