@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
@@ -0,0 +1,198 @@
1
+ import { Check, Copy } from "lucide-react";
2
+ import { useMemo, useState } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Label } from "@/components/ui/label";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "@/components/ui/select";
21
+ import { Textarea } from "@/components/ui/textarea";
22
+
23
+ type Placement = "header" | "user-menu" | "settings" | "custom";
24
+ type UnavailableState = "hide" | "current-role";
25
+
26
+ const placementLabels: Record<Placement, string> = {
27
+ header: "Application header",
28
+ "user-menu": "User menu",
29
+ settings: "Account settings",
30
+ custom: "Custom surface",
31
+ };
32
+
33
+ const unavailableStateLabels: Record<UnavailableState, string> = {
34
+ "current-role": "Show current role",
35
+ hide: "Hide component",
36
+ };
37
+
38
+ const placementInstructions: Record<Placement, string> = {
39
+ header:
40
+ "Place it in the application header beside the existing account controls. Keep the trigger compact and preserve the responsive user menu.",
41
+ "user-menu":
42
+ "Place it inside the existing user/account popover as a full-width menu section, separated from profile and sign-out actions.",
43
+ settings:
44
+ "Place it in an account settings section with a field label and short explanation of how the active role changes visible data and actions.",
45
+ custom:
46
+ "Place it in the application surface described below, matching the surrounding layout and spacing.",
47
+ };
48
+
49
+ export function RoleSwitcherPromptGenerator() {
50
+ const [placement, setPlacement] = useState<Placement>("user-menu");
51
+ const [label, setLabel] = useState("Working as");
52
+ const [unavailableState, setUnavailableState] =
53
+ useState<UnavailableState>("current-role");
54
+ const [surface, setSurface] = useState(
55
+ "Use the Starter's existing account controls and visual language. Do not create a separate demo page."
56
+ );
57
+ const [copied, setCopied] = useState(false);
58
+ const [copyError, setCopyError] = useState(false);
59
+
60
+ const prompt = useMemo(
61
+ () => `Integrate the RoleSwitcher into the existing NocoBase Admin Starter.
62
+
63
+ Target placement
64
+ - Placement: ${placementLabels[placement]}
65
+ - ${placementInstructions[placement]}
66
+ - Application context: ${surface}
67
+
68
+ Component presentation
69
+ - Label: ${label.trim() ? label : "No label; pass label={false}."}
70
+ - When role switching is unavailable: ${
71
+ unavailableState === "current-role"
72
+ ? "Keep the current identity visible by passing showWhenUnavailable."
73
+ : "Hide the component by keeping showWhenUnavailable false."
74
+ }
75
+ - Use className and triggerClassName only to fit the existing surface; do not fork or restyle the internal Select implementation.
76
+
77
+ Integration requirements
78
+ - Import and reuse RoleSwitcher from the installed local entry point at @/extensions/nocobase-acl.
79
+ - Update the existing application surface instead of creating an isolated example page.
80
+ - Do not fetch, hard-code, or pass a duplicate role list. The component reads roles from the signed-in identity.
81
+ - Do not reimplement Full permissions, Anonymous, or role-mode logic. The component derives these options from the Starter ACL store.
82
+ - Do not add a separate role-switch mutation. The component already calls users:setDefaultRole and reloads the application after a successful switch.
83
+ - Preserve existing account, responsive navigation, and sign-out behavior.
84
+ - Return the complete updated component for the target surface and mention any imports that were added.`,
85
+ [label, placement, surface, unavailableState]
86
+ );
87
+
88
+ return (
89
+ <div className="grid items-start gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
90
+ <Card className="gap-0 py-0">
91
+ <CardHeader className="border-b py-4">
92
+ <CardTitle className="text-base">Placement settings</CardTitle>
93
+ <CardDescription>
94
+ Choose where the shared role switcher belongs and how its compact
95
+ state should appear.
96
+ </CardDescription>
97
+ </CardHeader>
98
+ <CardContent className="space-y-5 py-5">
99
+ <div className="space-y-2">
100
+ <Label>Placement</Label>
101
+ <Select
102
+ value={placement}
103
+ onValueChange={(value) =>
104
+ value && setPlacement(value as Placement)
105
+ }
106
+ >
107
+ <SelectTrigger className="w-full">
108
+ <SelectValue>{placementLabels[placement]}</SelectValue>
109
+ </SelectTrigger>
110
+ <SelectContent>
111
+ <SelectItem value="header">Application header</SelectItem>
112
+ <SelectItem value="user-menu">User menu</SelectItem>
113
+ <SelectItem value="settings">Account settings</SelectItem>
114
+ <SelectItem value="custom">Custom surface</SelectItem>
115
+ </SelectContent>
116
+ </Select>
117
+ </div>
118
+ <div className="space-y-2">
119
+ <Label htmlFor="role-switcher-label">Label</Label>
120
+ <Input
121
+ id="role-switcher-label"
122
+ value={label}
123
+ onChange={(event) => setLabel(event.target.value)}
124
+ placeholder="Leave empty for no label"
125
+ />
126
+ </div>
127
+ <div className="space-y-2">
128
+ <Label>Single-role behavior</Label>
129
+ <Select
130
+ value={unavailableState}
131
+ onValueChange={(value) =>
132
+ value && setUnavailableState(value as UnavailableState)
133
+ }
134
+ >
135
+ <SelectTrigger className="w-full">
136
+ <SelectValue>
137
+ {unavailableStateLabels[unavailableState]}
138
+ </SelectValue>
139
+ </SelectTrigger>
140
+ <SelectContent>
141
+ <SelectItem value="current-role">Show current role</SelectItem>
142
+ <SelectItem value="hide">Hide component</SelectItem>
143
+ </SelectContent>
144
+ </Select>
145
+ </div>
146
+ <div className="space-y-2">
147
+ <Label htmlFor="role-switcher-surface">Application context</Label>
148
+ <Textarea
149
+ id="role-switcher-surface"
150
+ value={surface}
151
+ onChange={(event) => setSurface(event.target.value)}
152
+ className="min-h-28"
153
+ />
154
+ </div>
155
+ </CardContent>
156
+ </Card>
157
+
158
+ <Card className="gap-0 overflow-hidden py-0">
159
+ <div className="flex items-center justify-between gap-3 border-b px-4 py-3">
160
+ <div>
161
+ <div className="text-sm font-medium">
162
+ Generated integration prompt
163
+ </div>
164
+ <div className="text-xs text-muted-foreground">
165
+ Updates as you change placement and presentation settings.
166
+ </div>
167
+ </div>
168
+ <Button
169
+ size="sm"
170
+ onClick={async () => {
171
+ try {
172
+ await navigator.clipboard.writeText(prompt);
173
+ setCopyError(false);
174
+ setCopied(true);
175
+ window.setTimeout(() => setCopied(false), 1500);
176
+ } catch {
177
+ setCopyError(true);
178
+ }
179
+ }}
180
+ >
181
+ {copied ? <Check /> : <Copy />}
182
+ {copied ? "Copied" : "Copy prompt"}
183
+ </Button>
184
+ </div>
185
+ {copyError ? (
186
+ <p className="border-b px-4 py-2 text-xs text-destructive">
187
+ Clipboard access failed. Select the prompt and copy it manually.
188
+ </p>
189
+ ) : null}
190
+ <CardContent className="p-0">
191
+ <pre className="max-h-[560px] overflow-auto whitespace-pre-wrap bg-muted/25 p-5 font-mono text-xs leading-5 text-muted-foreground">
192
+ {prompt}
193
+ </pre>
194
+ </CardContent>
195
+ </Card>
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import {
5
+ AclScenarioPromptGenerator,
6
+ type AclPromptGeneratorConfig,
7
+ } from "./prompt-generator";
8
+
9
+ export function AclScenarioSection({
10
+ eyebrow,
11
+ title,
12
+ description,
13
+ children,
14
+ prompt,
15
+ }: {
16
+ eyebrow: string;
17
+ title: string;
18
+ description: string;
19
+ children: ReactNode;
20
+ prompt: AclPromptGeneratorConfig;
21
+ }) {
22
+ return (
23
+ <section className="space-y-5 border-t pt-8">
24
+ <div>
25
+ <Badge variant="outline">{eyebrow}</Badge>
26
+ <h2 className="mt-3 text-2xl font-semibold tracking-[-0.025em]">
27
+ {title}
28
+ </h2>
29
+ <p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
30
+ {description}
31
+ </p>
32
+ </div>
33
+ {children}
34
+ <AclScenarioPromptGenerator config={prompt} />
35
+ </section>
36
+ );
37
+ }
@@ -0,0 +1,71 @@
1
+ import type { AppExtension } from "@/app/extension";
2
+ import { Blocks, PanelsTopLeft, ShieldCheck } from "lucide-react";
3
+ import { lazy } from "react";
4
+ import { Outlet, Route } from "react-router";
5
+ import { LazyAclRoute } from "./demo/lazy-route";
6
+
7
+ const AclComponentsPage = lazy(() =>
8
+ import("./demo/components").then((module) => ({
9
+ default: module.AclComponentsPage,
10
+ }))
11
+ );
12
+ const AclPatternsPage = lazy(() =>
13
+ import("./demo").then((module) => ({ default: module.AclPatternsPage }))
14
+ );
15
+
16
+ const nocobaseAclExtension: AppExtension = {
17
+ id: "nocobase-acl",
18
+ resources: [
19
+ {
20
+ name: "acl-integration",
21
+ meta: {
22
+ label: "Access control",
23
+ icon: <ShieldCheck />,
24
+ description: "NocoBase ACL integration for admin applications.",
25
+ acl: { type: "authenticated" },
26
+ },
27
+ },
28
+ {
29
+ name: "acl-components",
30
+ list: "/acl",
31
+ meta: {
32
+ parent: "acl-integration",
33
+ label: "Role switcher",
34
+ icon: <Blocks />,
35
+ acl: { type: "authenticated" },
36
+ },
37
+ },
38
+ {
39
+ name: "acl-patterns",
40
+ list: "/acl/patterns",
41
+ meta: {
42
+ parent: "acl-integration",
43
+ label: "Permission patterns",
44
+ icon: <PanelsTopLeft />,
45
+ acl: { type: "authenticated" },
46
+ },
47
+ },
48
+ ],
49
+ routes: (
50
+ <Route key="nocobase-acl" path="/acl" element={<Outlet />}>
51
+ <Route
52
+ index
53
+ element={
54
+ <LazyAclRoute>
55
+ <AclComponentsPage />
56
+ </LazyAclRoute>
57
+ }
58
+ />
59
+ <Route
60
+ path="patterns"
61
+ element={
62
+ <LazyAclRoute>
63
+ <AclPatternsPage />
64
+ </LazyAclRoute>
65
+ }
66
+ />
67
+ </Route>
68
+ ),
69
+ };
70
+
71
+ export default nocobaseAclExtension;
@@ -0,0 +1 @@
1
+ export * from "./components";
@@ -0,0 +1,314 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "vite";
3
+
4
+ const server = await createServer({
5
+ appType: "custom",
6
+ logLevel: "silent",
7
+ server: { middlewareMode: true },
8
+ });
9
+
10
+ try {
11
+ const {
12
+ canAccessWithSnapshot,
13
+ getAclDataForDataSource,
14
+ resolveActionPermission,
15
+ } = await server.ssrLoadModule("/src/lib/nocobase/acl/action.ts");
16
+ const { resolveAclDataSourceKey } = await server.ssrLoadModule(
17
+ "/src/lib/nocobase/acl/data-source.ts"
18
+ );
19
+ const { filterMenuItemsByAcl, findFirstAccessibleRoute } =
20
+ await server.ssrLoadModule("/src/lib/nocobase/acl/menu.ts");
21
+ const {
22
+ clearRecordPermissions,
23
+ getRecordActionPermission,
24
+ updateRecordPermissions,
25
+ } = await server.ssrLoadModule("/src/lib/nocobase/acl/record-permissions.ts");
26
+ const { RoleSwitcher } = await server.ssrLoadModule(
27
+ "/registry/nocobase-acl/components/role-switcher.tsx"
28
+ );
29
+ const { getRoleOptions, resolveRoleTitle } = await server.ssrLoadModule(
30
+ "/registry/nocobase-acl/components/role-options.ts"
31
+ );
32
+
33
+ const snapshot = {
34
+ status: "ready",
35
+ version: 1,
36
+ data: {
37
+ role: "editor",
38
+ roleMode: "allow-use-union",
39
+ resources: ["blog_posts"],
40
+ actionAlias: {
41
+ list: "view",
42
+ get: "view",
43
+ },
44
+ actions: {
45
+ "blog_posts:view": { fields: ["id", "title"] },
46
+ "blog_posts:update": { fields: ["title"] },
47
+ },
48
+ strategy: {
49
+ actions: ["view:all"],
50
+ },
51
+ snippets: ["pm.*"],
52
+ },
53
+ meta: {},
54
+ };
55
+
56
+ assert.deepEqual(
57
+ resolveActionPermission({
58
+ snapshot,
59
+ resource: "blog_posts",
60
+ action: "list",
61
+ }),
62
+ { fields: ["id", "title"] }
63
+ );
64
+ assert.equal(
65
+ canAccessWithSnapshot(snapshot, {
66
+ resource: "blog_posts",
67
+ action: "create",
68
+ }),
69
+ false
70
+ );
71
+
72
+ const externalSnapshot = {
73
+ ...snapshot,
74
+ data: {
75
+ ...snapshot.data,
76
+ allowAll: true,
77
+ actionAlias: { list: "view" },
78
+ },
79
+ meta: {
80
+ dataSources: {
81
+ analytics: {
82
+ allowAll: false,
83
+ resources: ["orders"],
84
+ actionAlias: { list: "read" },
85
+ actions: { "orders:read": {} },
86
+ },
87
+ },
88
+ },
89
+ };
90
+ assert.deepEqual(getAclDataForDataSource(externalSnapshot, "analytics"), {
91
+ ...externalSnapshot.data,
92
+ allowAll: false,
93
+ resources: ["orders"],
94
+ actionAlias: { list: "read" },
95
+ actions: { "orders:read": {} },
96
+ snippets: externalSnapshot.data.snippets,
97
+ });
98
+ assert.equal(
99
+ canAccessWithSnapshot(externalSnapshot, {
100
+ resource: "orders",
101
+ action: "list",
102
+ params: { meta: { dataSourceKey: "analytics" } },
103
+ }),
104
+ true
105
+ );
106
+ assert.equal(
107
+ resolveAclDataSourceKey({
108
+ acl: {
109
+ type: "collection",
110
+ dataSourceKey: "analytics",
111
+ },
112
+ }),
113
+ "analytics"
114
+ );
115
+ assert.equal(
116
+ resolveAclDataSourceKey(
117
+ { dataSourceKey: "reporting" },
118
+ {
119
+ acl: {
120
+ type: "collection",
121
+ dataSourceKey: "analytics",
122
+ },
123
+ }
124
+ ),
125
+ "reporting"
126
+ );
127
+ assert.equal(
128
+ getRecordActionPermission({
129
+ resource: "blog_posts",
130
+ action: "update",
131
+ id: 3,
132
+ }),
133
+ undefined
134
+ );
135
+ assert.equal(
136
+ canAccessWithSnapshot(snapshot, {
137
+ resource: "categories",
138
+ action: "list",
139
+ }),
140
+ true
141
+ );
142
+ assert.equal(
143
+ canAccessWithSnapshot(snapshot, {
144
+ resource: "settings",
145
+ action: "list",
146
+ params: {
147
+ resource: {
148
+ name: "settings",
149
+ meta: { acl: { type: "snippet", name: "*" } },
150
+ },
151
+ },
152
+ }),
153
+ true
154
+ );
155
+ assert.equal(
156
+ canAccessWithSnapshot(
157
+ {
158
+ ...snapshot,
159
+ data: {
160
+ ...snapshot.data,
161
+ snippets: ["pm", "pm.*", "!pm.data-source-manager*"],
162
+ },
163
+ },
164
+ {
165
+ resource: "settings",
166
+ action: "list",
167
+ params: {
168
+ resource: {
169
+ name: "settings",
170
+ meta: {
171
+ acl: { type: "snippet", name: "pm.data-source-manager" },
172
+ },
173
+ },
174
+ },
175
+ }
176
+ ),
177
+ false
178
+ );
179
+ assert.equal(
180
+ updateRecordPermissions({
181
+ resource: "blog_posts",
182
+ recordIds: [4],
183
+ allowedActions: { update: [] },
184
+ }),
185
+ true
186
+ );
187
+ assert.equal(
188
+ getRecordActionPermission({
189
+ resource: "blog_posts",
190
+ action: "update",
191
+ id: 4,
192
+ }),
193
+ false
194
+ );
195
+ assert.equal(
196
+ canAccessWithSnapshot(snapshot, {
197
+ resource: "settings",
198
+ action: "list",
199
+ params: {
200
+ resource: {
201
+ name: "settings",
202
+ meta: { acl: { type: "snippet", name: "pm.acl" } },
203
+ },
204
+ },
205
+ }),
206
+ true
207
+ );
208
+ assert.equal(
209
+ canAccessWithSnapshot(snapshot, {
210
+ resource: "blog_posts",
211
+ action: "edit",
212
+ params: { field: "title" },
213
+ }),
214
+ true
215
+ );
216
+ assert.equal(
217
+ canAccessWithSnapshot(snapshot, {
218
+ resource: "blog_posts",
219
+ action: "edit",
220
+ params: { field: "status" },
221
+ }),
222
+ false
223
+ );
224
+
225
+ clearRecordPermissions();
226
+ updateRecordPermissions({
227
+ resource: "blog_posts",
228
+ recordIds: [1, 2],
229
+ allowedActions: {
230
+ view: [1, 2],
231
+ update: [1],
232
+ destroy: [],
233
+ },
234
+ });
235
+ assert.equal(
236
+ canAccessWithSnapshot(snapshot, {
237
+ resource: "blog_posts",
238
+ action: "edit",
239
+ params: { id: 1 },
240
+ }),
241
+ true
242
+ );
243
+ assert.equal(
244
+ canAccessWithSnapshot(snapshot, {
245
+ resource: "blog_posts",
246
+ action: "edit",
247
+ params: { id: 2 },
248
+ }),
249
+ false
250
+ );
251
+ assert.equal(
252
+ updateRecordPermissions({
253
+ resource: "blog_posts",
254
+ recordIds: [1, 2],
255
+ allowedActions: {
256
+ view: [1, 2],
257
+ update: [1],
258
+ destroy: [],
259
+ },
260
+ }),
261
+ false
262
+ );
263
+
264
+ const filteredMenu = filterMenuItemsByAcl(
265
+ [
266
+ {
267
+ key: "restricted-parent",
268
+ name: "restricted-parent",
269
+ route: "/restricted-parent",
270
+ children: [
271
+ {
272
+ key: "public-child",
273
+ name: "public-child",
274
+ route: "/public-child",
275
+ meta: { acl: { type: "authenticated" } },
276
+ children: [],
277
+ },
278
+ ],
279
+ },
280
+ ],
281
+ {
282
+ ...snapshot,
283
+ data: { ...snapshot.data, strategy: { actions: [] } },
284
+ }
285
+ );
286
+ assert.equal(filteredMenu[0].route, undefined);
287
+ assert.equal(findFirstAccessibleRoute(filteredMenu), "/public-child");
288
+
289
+ assert.deepEqual(
290
+ getRoleOptions({
291
+ roles: [
292
+ { name: "admin", title: "Administrator" },
293
+ { name: "member", title: "Member" },
294
+ ],
295
+ roleMode: "allow-use-union",
296
+ allowAnonymous: true,
297
+ }),
298
+ [
299
+ { name: "__union__", title: "Full permissions" },
300
+ { name: "admin", title: "Administrator" },
301
+ { name: "member", title: "Member" },
302
+ { name: "anonymous", title: "Anonymous" },
303
+ ]
304
+ );
305
+ assert.equal(
306
+ resolveRoleTitle({ name: "admin", title: '{{t("Admin")}}' }),
307
+ "Admin"
308
+ );
309
+
310
+ assert.equal(typeof RoleSwitcher, "function");
311
+ console.log("NocoBase ACL regression tests passed");
312
+ } finally {
313
+ await server.close();
314
+ }
@@ -0,0 +1,70 @@
1
+ # NocoBase AI Registry
2
+
3
+ The Registry is split by capability so applications only install the code and dependencies they use.
4
+
5
+ These entries target the compatible NocoBase admin starter family. The host must
6
+ provide the `@` source alias, `@/lib/nocobase/client`, the standard shadcn UI
7
+ components, and (for `@nocobase/ai`) the `@/app/extension` discovery contract.
8
+ Applications with a different host can still reuse the providers and components,
9
+ but should supply their own `AIService` adapter and integration entry.
10
+
11
+ - `@nocobase/ai-runtime`: NocoBase service adapter, chat state, streaming transport, conversation history, Tool execution, and page-context registries.
12
+ - `@nocobase/ai-chat`: Chat windows, containers, triggers, page-element APIs, built-in Tool cards, and the high-level `NocoBaseAIRootProvider`.
13
+ - `@nocobase/ai-react-hook-form`: Optional React Hook Form adapter for the built-in Form filler.
14
+ - `@nocobase/ai`: The complete Demo extension and routes.
15
+
16
+ ## Recommended provider
17
+
18
+ Use one root provider around every chat surface and page element that should participate in AI interactions:
19
+
20
+ ```tsx
21
+ <NocoBaseAIRootProvider service={service}>
22
+ <YourApplication />
23
+ </NocoBaseAIRootProvider>
24
+ ```
25
+
26
+ It installs the AI runtime, built-in Tool renderers, Form registry, frontend Tool registry, and page-context resolver in the required order. The lower-level providers remain available for advanced integrations.
27
+
28
+ ## Page context
29
+
30
+ Prefer `useAIPageElementHandle` when a page element is both registered and referenced by a task or scope:
31
+
32
+ ```tsx
33
+ const customer = useAIPageElementHandle({
34
+ id: "customer-detail",
35
+ title: "Customer detail",
36
+ getContext: () => customerRecord,
37
+ });
38
+
39
+ return (
40
+ <AIPageContextScope context={customer.context}>
41
+ <section ref={customer.ref}>...</section>
42
+ </AIPageContextScope>
43
+ );
44
+ ```
45
+
46
+ Context is resolved again immediately before sending, so the model receives the latest page values. Resolution failures block the request by default. Set `contextFailurePolicy="omit"` on `NocoBaseAIRootProvider` only when sending without unavailable context is an intentional product decision.
47
+
48
+ Nested scopes replace the parent scope by default. Use `mode="append"` to compose parent and child context.
49
+
50
+ ## Form filler
51
+
52
+ Register forms with `useAIForm`. Sending a registered form Context automatically enables the fixed `formFiller` Tool; tasks do not need to add it to `skillSettings.tools`.
53
+
54
+ The runtime applies only declared, editable, type-compatible fields. It never submits or saves the form. React Hook Form users can install `@nocobase/ai-react-hook-form` and import `applyReactHookFormValues` from `src/extensions/nocobase-ai/adapters/react-hook-form`.
55
+
56
+ ## Frontend Tools
57
+
58
+ Frontend Tools are page-local browser actions and remain separate from Form filler. Use `defineAIFrontendTool` for typed registration, provide a JSON input schema, and choose `ASK` or `ALLOW`. Runtime permission controls approval; prompts should not simulate approval behavior.
59
+
60
+ ## Tool cards
61
+
62
+ Pass application-specific message renderers through `NocoBaseAIRootProvider.toolRenderers`. Built-in renderers remain available automatically. A custom renderer receives the Tool part, disabled state, edit/approve/reject actions, and a composer-focus callback.
63
+
64
+ ## Local Registry development
65
+
66
+ - `pnpm registry:build` regenerates `registry.json` and the static shadcn Registry output.
67
+ - `pnpm registry:install` installs the Registry source only when the target directory is missing.
68
+ - `pnpm registry:preview` intentionally replaces the local installed preview with the authoritative source.
69
+
70
+ Normal install and `postinstall` never overwrite an existing `src/extensions/nocobase-ai` directory, so downstream applications can edit and commit installed source.