@nocobase/portal-template-default 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
package/README.MD CHANGED
@@ -14,7 +14,15 @@ NOCOBASE_API_URL=https://develop.v2.test.nocobase.com/nocobase/api/__app/cms
14
14
 
15
15
  In development, the app auto-detects the proxy target from `NOCOBASE_API_URL` when that value is an absolute URL. The configured API URL can stay on the original sub-app path such as `/nocobase/api/__app/cms`.
16
16
 
17
- Refine resource names must match NocoBase collection names. For example, the supplied Swagger document exposes `collectionCategories`, so use that name when registering a corresponding Refine resource.
17
+ Application resource names must match NocoBase collection names. The included
18
+ Users example targets the default `users` collection, so it works without a
19
+ project-specific business collection.
20
+
21
+ Application-owned React translations live in `src/locales`. The entry registers
22
+ an `app` namespace for business pages and augments the default `starter`
23
+ namespace for shared controls. Feature-specific Registry translations remain
24
+ inside their own extension namespace and are not mixed into application locale
25
+ files.
18
26
 
19
27
  ## Available Scripts
20
28
 
@@ -30,6 +38,87 @@ pnpm dev
30
38
  pnpm build
31
39
  ```
32
40
 
41
+ ### Extensions and the NocoBase Shadcn Registry
42
+
43
+ The starter separates framework-owned extension support from installed Registry
44
+ source:
45
+
46
+ - `src/app/extension.ts` defines the generic extension contract.
47
+ - `src/app/extensions.tsx` discovers `src/extensions/*/extension.tsx` and adds
48
+ its providers, Refine resources, and routes to the app.
49
+ - `registry/<name>` contains the canonical authoring source for a Registry
50
+ package while it is developed in this repository.
51
+ - `src/extensions/<name>` contains installed, application-owned source.
52
+ Downstream applications can commit this directory like any other source code.
53
+
54
+ `registry.config.json` is the only authoring manifest. In addition to standard
55
+ Shadcn Registry item fields, each item has a local `source` mapping used to
56
+ generate its standard `files` array. The same mapping lets the starter's
57
+ `postinstall` hook copy a locally authored extension only when its target is
58
+ missing. Existing installed source is never compared, updated, or overwritten.
59
+ Optional entries can set `source.install` to `false`; they remain available to
60
+ Registry builds and local previews but are not added by `postinstall`.
61
+
62
+ You can run the same missing-only installation explicitly with:
63
+
64
+ ```bash
65
+ pnpm registry:install
66
+ ```
67
+
68
+ This repository authors optional NocoBase integration entries. Internationalization
69
+ is delivered as one cohesive Registry entry:
70
+
71
+ - `i18n` provides frontend-owned UI resources, selected server-generated
72
+ namespaces such as `lm-collections`, the application translation provider,
73
+ locale persistence, `X-Locale` synchronization, legacy `{{t(...)}}`
74
+ compatibility, the language switcher, and the lazy-loaded showcase with its
75
+ prompt generator.
76
+
77
+ The AI integration is available through four incremental entries:
78
+
79
+ - `ai-runtime` provides AI services, providers, transports, and
80
+ controllers. Its NocoBase requests use the starter's shared
81
+ `src/lib/nocobase/client.ts` client.
82
+ - `ai-chat` adds the reusable chat components and depends on the
83
+ runtime entry.
84
+ - `ai-react-hook-form` adds the optional React Hook Form adapter for the fixed
85
+ Form filler Tool.
86
+ - `ai` adds the application extension integration and Demo pages and depends on the
87
+ chat entry.
88
+
89
+ To refresh the local preview from the canonical Registry source, run:
90
+
91
+ ```bash
92
+ pnpm registry:preview
93
+ ```
94
+
95
+ This is the only command that intentionally replaces the local preview at
96
+ `src/extensions/nocobase-ai`. Normal `pnpm dev`, `pnpm install`, and
97
+ `pnpm registry:install` do not overwrite an installed extension.
98
+
99
+ Build the standard Shadcn Registry JSON files into `public/r` with:
100
+
101
+ ```bash
102
+ pnpm registry:build
103
+ ```
104
+
105
+ With the development server running, a compatible starter can also add the
106
+ complete item through Shadcn:
107
+
108
+ ```bash
109
+ pnpm dlx shadcn@latest add @nocobase/ai
110
+ ```
111
+
112
+ Shadcn does not keep a Registry item lock or merge local changes. Re-running
113
+ `add` downloads the current item, skips identical files, and asks before
114
+ replacing different files. Use `--diff` to inspect an update and `--overwrite`
115
+ to accept the Registry version explicitly.
116
+
117
+ The Registry alias is configured in `components.json` and resolves to
118
+ `http://localhost:5173/r/{name}.json` during local development. The same generic
119
+ extension host and Registry source mapping can be reused for future Registry
120
+ packages without adding package-specific logic to `App.tsx` or the installer.
121
+
33
122
  ### Running the production server.
34
123
 
35
124
  ```bash
package/components.json CHANGED
@@ -21,5 +21,7 @@
21
21
  },
22
22
  "menuColor": "default",
23
23
  "menuAccent": "subtle",
24
- "registries": {}
24
+ "registries": {
25
+ "@nocobase": "http://localhost:5173/r/{name}.json"
26
+ }
25
27
  }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@nocobase/portal-template-default",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
- "dependencies": {
5
+ "devDependencies": {
6
+ "@ai-sdk/react": "2.0.0",
6
7
  "@base-ui/react": "^1.6.0",
8
+ "@dicebear/core": "^9.4.3",
7
9
  "@fontsource-variable/geist": "^5.3.0",
8
10
  "@hookform/resolvers": "^5.0.1",
11
+ "@nocobase/ai-employee-avatars": "^1.0.2",
9
12
  "@refinedev/cli": "^2.16.48",
10
13
  "@refinedev/core": "^5.0.8",
11
14
  "@refinedev/devtools": "^2.0.1",
@@ -15,12 +18,18 @@
15
18
  "@refinedev/rest": "^2.1.0",
16
19
  "@refinedev/simple-rest": "^6.0.0",
17
20
  "@tailwindcss/vite": "^4.1.8",
21
+ "@tanstack/react-query": "^5.81.5",
18
22
  "@tanstack/react-table": "^8.2.6",
23
+ "ai": "5.0.0",
19
24
  "class-variance-authority": "^0.7.1",
20
25
  "clsx": "^2.1.1",
21
26
  "date-fns": "^4.1.0",
22
27
  "dayjs": "^1.10.7",
28
+ "echarts": "^5.5.0",
29
+ "echarts-for-react": "3.0.2",
23
30
  "embla-carousel-react": "^8.6.0",
31
+ "eventsource-parser": "3.1.0",
32
+ "ignore": "^7.0.6",
24
33
  "input-otp": "^1.4.2",
25
34
  "lucide-react": "^0.487.0",
26
35
  "next-themes": "^0.4.6",
@@ -29,16 +38,16 @@
29
38
  "react-day-picker": "10.0.1",
30
39
  "react-dom": "19.1.0",
31
40
  "react-hook-form": "^7.57.0",
41
+ "react-markdown": "^10.1.0",
32
42
  "react-resizable-panels": "^4.12.2",
33
43
  "react-router": "^7.0.2",
34
44
  "recharts": "^3.8.0",
45
+ "remark-gfm": "^4.0.1",
35
46
  "shadcn": "^4.13.1",
36
47
  "sonner": "^2.0.3",
37
48
  "tailwind-merge": "^3.2.0",
38
49
  "tw-animate-css": "^1.2.5",
39
- "zod": "^3.24.3"
40
- },
41
- "devDependencies": {
50
+ "zod": "^3.24.3",
42
51
  "@eslint/js": "^9.25.0",
43
52
  "@tailwindcss/postcss": "^4.1.4",
44
53
  "@types/node": "^20",
@@ -51,6 +60,7 @@
51
60
  "eslint-plugin-react-hooks": "^5.2.0",
52
61
  "eslint-plugin-react-refresh": "^0.4.19",
53
62
  "globals": "^16.0.0",
63
+ "i18next": "^25.3.2",
54
64
  "tailwindcss": "^4.1.4",
55
65
  "typescript": "^5.8.3",
56
66
  "typescript-eslint": "^8.30.1",
@@ -64,8 +74,14 @@
64
74
  "url": "git+https://github.com/nocobase/portal-template-default.git"
65
75
  },
66
76
  "scripts": {
77
+ "postinstall": "node ./scripts/registry.mjs install-missing",
67
78
  "dev": "vite",
68
79
  "build": "tsc && refine build",
80
+ "registry:install": "node ./scripts/registry.mjs install-missing",
81
+ "registry:preview": "node ./scripts/registry.mjs preview",
82
+ "registry:build": "node ./scripts/registry.mjs build && shadcn build ./registry.json -o ./public/r",
83
+ "test:acl": "node registry/nocobase-acl/tests/acl-regression.mjs",
84
+ "test:i18n": "node registry/nocobase-i18n/tests/i18n-regression.mjs",
69
85
  "start": "refine start",
70
86
  "refine": "refine"
71
87
  },
@@ -0,0 +1,19 @@
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
+ - `AclPage` combines page-level permissions.
8
+ - `AclRegion` hides or replaces individual data regions.
9
+ - `AclField` applies NocoBase field whitelists.
10
+ - `RoleSwitcher` can be placed in any application surface. It supports
11
+ normal roles, Anonymous when enabled, and Full permissions through the
12
+ `__union__` role when the system role mode allows it.
13
+
14
+ Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
15
+ the `collection`, `snippet`, and `route` ACL metadata variants when the resource
16
+ should participate in NocoBase permission checks.
17
+
18
+ The underlying `accessControlProvider`, `roles:check` cache, record permissions,
19
+ and Refine integration are provided by the compatible Starter.
@@ -0,0 +1,106 @@
1
+ import type { BaseKey } from "@refinedev/core";
2
+ import type { PropsWithChildren, ReactNode } from "react";
3
+
4
+ import { AccessDenied } from "@/components/access-control/access-denied";
5
+ import {
6
+ canAccessWithSnapshot,
7
+ useAclSnapshot,
8
+ } from "@/lib/nocobase/acl";
9
+
10
+ export type AclPermission = {
11
+ resource: string;
12
+ action: string;
13
+ id?: BaseKey;
14
+ field?: string;
15
+ dataSourceKey?: string;
16
+ };
17
+
18
+ export type AclPageProps = PropsWithChildren<{
19
+ anyOf?: AclPermission[];
20
+ allOf?: AclPermission[];
21
+ fallback?: ReactNode;
22
+ }>;
23
+
24
+ export type AclRegionProps = PropsWithChildren<
25
+ Omit<AclPermission, "field"> & {
26
+ fallback?: "hidden" | "forbidden" | ReactNode;
27
+ }
28
+ >;
29
+
30
+ export type AclFieldProps = PropsWithChildren<{
31
+ resource: string;
32
+ action: string;
33
+ field: string;
34
+ dataSourceKey?: string;
35
+ fallback?: ReactNode;
36
+ }>;
37
+
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
+ });
51
+
52
+ export function AclPage({
53
+ children,
54
+ anyOf,
55
+ allOf,
56
+ fallback = <AccessDenied />,
57
+ }: 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));
61
+
62
+ return anyAllowed && allAllowed ? children : fallback;
63
+ }
64
+
65
+ export function AclRegion({
66
+ children,
67
+ resource,
68
+ action,
69
+ id,
70
+ dataSourceKey,
71
+ fallback = "hidden",
72
+ }: AclRegionProps) {
73
+ const snapshot = useAclSnapshot();
74
+ const allowed = canAccess(snapshot, {
75
+ resource,
76
+ action,
77
+ id,
78
+ dataSourceKey,
79
+ });
80
+
81
+ if (allowed) return children;
82
+ if (fallback === "hidden") return null;
83
+ if (fallback === "forbidden") {
84
+ return <AccessDenied className="min-h-40" />;
85
+ }
86
+ return fallback;
87
+ }
88
+
89
+ export function AclField({
90
+ children,
91
+ resource,
92
+ action,
93
+ field,
94
+ dataSourceKey,
95
+ fallback = null,
96
+ }: AclFieldProps) {
97
+ const snapshot = useAclSnapshot();
98
+ return canAccess(snapshot, {
99
+ resource,
100
+ action,
101
+ field,
102
+ dataSourceKey,
103
+ })
104
+ ? children
105
+ : fallback;
106
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./acl-boundary";
2
+ export * from "./role-options";
3
+ export * from "./role-switcher";
@@ -0,0 +1,36 @@
1
+ import type { Role, RoleMode } from "@/lib/nocobase/acl";
2
+ import { resolveTranslatableText } from "@/lib/i18n";
3
+
4
+ export const UNION_ROLE = "__union__";
5
+ export const ANONYMOUS_ROLE = "anonymous";
6
+
7
+ export function resolveRoleTitle(role?: Role) {
8
+ return resolveTranslatableText(role?.title || role?.name || "Role", {
9
+ ns: "starter",
10
+ });
11
+ }
12
+
13
+ export function getRoleOptions({
14
+ roles,
15
+ roleMode,
16
+ allowAnonymous = false,
17
+ }: {
18
+ roles: Role[];
19
+ roleMode?: RoleMode;
20
+ allowAnonymous?: boolean;
21
+ }) {
22
+ if (roleMode === "only-use-union") {
23
+ return [{ name: UNION_ROLE, title: "Full permissions" }];
24
+ }
25
+
26
+ const options = roles.filter(
27
+ (role) => role.name !== UNION_ROLE && role.name !== ANONYMOUS_ROLE
28
+ );
29
+ if (allowAnonymous) {
30
+ options.push({ name: ANONYMOUS_ROLE, title: "Anonymous" });
31
+ }
32
+ if (roleMode === "allow-use-union") {
33
+ options.unshift({ name: UNION_ROLE, title: "Full permissions" });
34
+ }
35
+ return options;
36
+ }
@@ -0,0 +1,135 @@
1
+ import { useGetIdentity } from "@refinedev/core";
2
+ import { Loader2, ShieldCheck } from "lucide-react";
3
+ import { useMemo, useState, type ReactNode } from "react";
4
+
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectSeparator,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import {
15
+ switchRole,
16
+ useAclSnapshot,
17
+ type AclIdentity,
18
+ type Role,
19
+ } from "@/lib/nocobase/acl";
20
+ import { cn } from "@/lib/utils";
21
+ import { getRoleOptions, resolveRoleTitle, UNION_ROLE } from "./role-options";
22
+
23
+ export type RoleSwitcherProps = {
24
+ className?: string;
25
+ triggerClassName?: string;
26
+ label?: ReactNode | false;
27
+ showWhenUnavailable?: boolean;
28
+ };
29
+
30
+ export function RoleSwitcher({
31
+ className,
32
+ triggerClassName,
33
+ label = "Switch role",
34
+ showWhenUnavailable = false,
35
+ }: RoleSwitcherProps) {
36
+ const { data: identity, isLoading } = useGetIdentity<AclIdentity>();
37
+ const acl = useAclSnapshot();
38
+ const [switching, setSwitching] = useState(false);
39
+ const [error, setError] = useState<string>();
40
+
41
+ const roles = useMemo(
42
+ () =>
43
+ getRoleOptions({
44
+ roles: identity?.roles ?? [],
45
+ roleMode: acl.data.roleMode,
46
+ allowAnonymous: acl.data.allowAnonymous,
47
+ }),
48
+ [acl.data.allowAnonymous, acl.data.roleMode, identity?.roles]
49
+ );
50
+
51
+ const currentRole = acl.data.role ?? roles[0]?.name;
52
+ const canSwitch = roles.length > 1 && acl.data.roleMode !== "only-use-union";
53
+
54
+ if (isLoading) {
55
+ return <Loader2 className="size-4 animate-spin text-muted-foreground" />;
56
+ }
57
+ if (!canSwitch && !showWhenUnavailable) return null;
58
+
59
+ if (!canSwitch) {
60
+ return (
61
+ <div className={cn("flex items-center gap-2 text-sm", className)}>
62
+ <ShieldCheck className="size-4 text-muted-foreground" />
63
+ <span className="text-muted-foreground">Current role</span>
64
+ <Badge variant="secondary">{getRoleTitle(roles, currentRole)}</Badge>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div className={cn("space-y-2", className)}>
71
+ {label === false ? null : (
72
+ <p className="text-xs font-medium text-muted-foreground">{label}</p>
73
+ )}
74
+ <Select
75
+ value={currentRole}
76
+ 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
+ }}
92
+ >
93
+ <SelectTrigger
94
+ className={cn("w-full min-w-52", triggerClassName)}
95
+ aria-label="Switch role"
96
+ >
97
+ {switching ? <Loader2 className="animate-spin" /> : <ShieldCheck />}
98
+ <SelectValue>{getRoleTitle(roles, currentRole)}</SelectValue>
99
+ </SelectTrigger>
100
+ <SelectContent>
101
+ {roles.map((role, index) => (
102
+ <RoleOption
103
+ key={role.name}
104
+ role={role}
105
+ showSeparator={index === 1 && roles[0]?.name === UNION_ROLE}
106
+ />
107
+ ))}
108
+ </SelectContent>
109
+ </Select>
110
+ {error ? <p className="text-xs text-destructive">{error}</p> : null}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ function RoleOption({
116
+ role,
117
+ showSeparator,
118
+ }: {
119
+ role: Role;
120
+ showSeparator: boolean;
121
+ }) {
122
+ return (
123
+ <>
124
+ {showSeparator ? <SelectSeparator /> : null}
125
+ <SelectItem value={role.name}>{resolveRoleTitle(role)}</SelectItem>
126
+ </>
127
+ );
128
+ }
129
+
130
+ function getRoleTitle(roles: Role[], roleName?: string) {
131
+ return resolveRoleTitle(
132
+ roles.find((role) => role.name === roleName) ??
133
+ (roleName ? { name: roleName } : undefined)
134
+ );
135
+ }
@@ -0,0 +1,186 @@
1
+ import { Card } from "@/components/ui/card";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+
11
+ type PropRow = [
12
+ name: string,
13
+ type: string,
14
+ defaultValue: string,
15
+ description: string
16
+ ];
17
+
18
+ const pageProps: PropRow[] = [
19
+ [
20
+ "anyOf",
21
+ "AclPermission[]",
22
+ "undefined",
23
+ "Allows the page when any listed permission is available.",
24
+ ],
25
+ [
26
+ "allOf",
27
+ "AclPermission[]",
28
+ "undefined",
29
+ "Requires every listed permission before rendering the page.",
30
+ ],
31
+ [
32
+ "fallback",
33
+ "ReactNode",
34
+ "AccessDenied",
35
+ "Replaces the complete page when its permission test fails.",
36
+ ],
37
+ ["children", "ReactNode", "required", "The protected page content."],
38
+ ];
39
+
40
+ const regionProps: PropRow[] = [
41
+ [
42
+ "resource",
43
+ "string",
44
+ "required",
45
+ "NocoBase collection or ACL resource name.",
46
+ ],
47
+ [
48
+ "action",
49
+ "string",
50
+ "required",
51
+ "Resource action such as list, show, create, edit, or delete.",
52
+ ],
53
+ [
54
+ "id",
55
+ "BaseKey",
56
+ "undefined",
57
+ "Optional record ID for record-scoped permission checks.",
58
+ ],
59
+ [
60
+ "dataSourceKey",
61
+ "string",
62
+ "undefined",
63
+ "Selects a non-main NocoBase data source.",
64
+ ],
65
+ [
66
+ "fallback",
67
+ '"hidden" | "forbidden" | ReactNode',
68
+ '"hidden"',
69
+ "Hides the region or replaces it with a local denied state.",
70
+ ],
71
+ ["children", "ReactNode", "required", "The protected page region."],
72
+ ];
73
+
74
+ const fieldProps: PropRow[] = [
75
+ [
76
+ "resource",
77
+ "string",
78
+ "required",
79
+ "NocoBase collection containing the field.",
80
+ ],
81
+ [
82
+ "action",
83
+ "string",
84
+ "required",
85
+ "The action whose field whitelist should be checked.",
86
+ ],
87
+ ["field", "string", "required", "Exact NocoBase field name."],
88
+ [
89
+ "dataSourceKey",
90
+ "string",
91
+ "undefined",
92
+ "Selects a non-main NocoBase data source.",
93
+ ],
94
+ [
95
+ "fallback",
96
+ "ReactNode",
97
+ "null",
98
+ "Optional read-only replacement for a denied field.",
99
+ ],
100
+ ["children", "ReactNode", "required", "The protected field or field group."],
101
+ ];
102
+
103
+ export function AclBoundaryApi() {
104
+ return (
105
+ <section className="space-y-5 border-t pt-8">
106
+ <div>
107
+ <p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
108
+ Permission boundaries
109
+ </p>
110
+ <h2 className="mt-2 text-xl font-semibold tracking-tight">
111
+ Components behind the patterns
112
+ </h2>
113
+ <p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
114
+ Use the smallest boundary that matches the intended user experience:
115
+ complete page, independent region, or individual field.
116
+ </p>
117
+ </div>
118
+ <div className="space-y-7">
119
+ <ComponentApi
120
+ title="AclPage"
121
+ description="Protects a complete route or business page. This corresponds to the Page permission pattern above."
122
+ rows={pageProps}
123
+ />
124
+ <ComponentApi
125
+ title="AclRegion"
126
+ description="Protects one independent area inside a larger page. This corresponds to the Region permission pattern."
127
+ rows={regionProps}
128
+ />
129
+ <ComponentApi
130
+ title="AclField"
131
+ description="Protects one field or field group. This corresponds to the Field permission pattern."
132
+ rows={fieldProps}
133
+ />
134
+ </div>
135
+ </section>
136
+ );
137
+ }
138
+
139
+ function ComponentApi({
140
+ title,
141
+ description,
142
+ rows,
143
+ }: {
144
+ title: string;
145
+ description: string;
146
+ rows: PropRow[];
147
+ }) {
148
+ return (
149
+ <div className="space-y-3">
150
+ <div>
151
+ <h3 className="font-semibold">{title}</h3>
152
+ <p className="mt-1 text-sm text-muted-foreground">{description}</p>
153
+ </div>
154
+ <Card className="gap-0 overflow-hidden py-0">
155
+ <Table>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHead>Prop</TableHead>
159
+ <TableHead>Type</TableHead>
160
+ <TableHead>Default</TableHead>
161
+ <TableHead>Description</TableHead>
162
+ </TableRow>
163
+ </TableHeader>
164
+ <TableBody>
165
+ {rows.map(([name, type, defaultValue, propDescription]) => (
166
+ <TableRow key={name}>
167
+ <TableCell className="font-mono text-xs font-medium">
168
+ {name}
169
+ </TableCell>
170
+ <TableCell className="font-mono text-xs text-muted-foreground">
171
+ {type}
172
+ </TableCell>
173
+ <TableCell className="font-mono text-xs text-muted-foreground">
174
+ {defaultValue}
175
+ </TableCell>
176
+ <TableCell className="min-w-72 whitespace-normal text-muted-foreground">
177
+ {propDescription}
178
+ </TableCell>
179
+ </TableRow>
180
+ ))}
181
+ </TableBody>
182
+ </Table>
183
+ </Card>
184
+ </div>
185
+ );
186
+ }