@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
|
@@ -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.
|