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