@nocobase/portal-template-default 1.0.0 → 1.0.1
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 +70 -0
- package/components.json +3 -1
- package/package.json +19 -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 +29 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +130 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +115 -0
- package/registry/nocobase-acl/demo/components.tsx +210 -0
- package/registry/nocobase-acl/demo/index.tsx +474 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +15 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +115 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +179 -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 +313 -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 +174 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +345 -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 +367 -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 +110 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +62 -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 +52 -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 +163 -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.config.json +143 -0
- package/scripts/registry.mjs +161 -0
- package/src/App.css +95 -0
- package/src/App.tsx +127 -50
- package/src/app/extension.ts +9 -0
- package/src/app/extensions.tsx +28 -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 +42 -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/header.tsx +4 -1
- package/src/components/app-shell/sidebar.tsx +34 -4
- 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 +262 -0
- package/src/lib/nocobase/error.ts +32 -0
- package/src/providers/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +2 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ShieldCheck } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/ui/card";
|
|
11
|
+
import {
|
|
12
|
+
Table,
|
|
13
|
+
TableBody,
|
|
14
|
+
TableCell,
|
|
15
|
+
TableHead,
|
|
16
|
+
TableHeader,
|
|
17
|
+
TableRow,
|
|
18
|
+
} from "@/components/ui/table";
|
|
19
|
+
import { RoleSwitcher } from "../components";
|
|
20
|
+
import { RoleSwitcherPromptGenerator } from "./role-switcher-prompt-generator";
|
|
21
|
+
|
|
22
|
+
type PropRow = [name: string, type: string, defaultValue: string, description: string];
|
|
23
|
+
|
|
24
|
+
const roleSwitcherProps: PropRow[] = [
|
|
25
|
+
[
|
|
26
|
+
"className",
|
|
27
|
+
"string",
|
|
28
|
+
"undefined",
|
|
29
|
+
"Styles the component wrapper so it can fit a toolbar, menu, or settings layout.",
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"triggerClassName",
|
|
33
|
+
"string",
|
|
34
|
+
"undefined",
|
|
35
|
+
"Styles only the Select trigger without changing the wrapper.",
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"label",
|
|
39
|
+
"ReactNode | false",
|
|
40
|
+
'"Switch role"',
|
|
41
|
+
"Replaces the field label. Pass false for a compact control without a label.",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"showWhenUnavailable",
|
|
45
|
+
"boolean",
|
|
46
|
+
"false",
|
|
47
|
+
"Shows the current role as a non-interactive badge when the user cannot switch roles.",
|
|
48
|
+
],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const roleBehaviors = [
|
|
52
|
+
["Multiple assigned roles", "Renders a Select using roles from the current Refine identity."],
|
|
53
|
+
["allow-use-union", "Prepends Full permissions using the __union__ role."],
|
|
54
|
+
["only-use-union", "Shows Full permissions as the current non-interactive role."],
|
|
55
|
+
["allowAnonymous", "Appends Anonymous to the available role options."],
|
|
56
|
+
["One available role", "Returns null, or shows the current-role badge when showWhenUnavailable is true."],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function AclComponentsPage() {
|
|
60
|
+
return (
|
|
61
|
+
<div className="space-y-12 pb-12">
|
|
62
|
+
<header className="space-y-2">
|
|
63
|
+
<h1 className="text-2xl font-semibold tracking-[-0.025em] sm:text-3xl">
|
|
64
|
+
Role switcher
|
|
65
|
+
</h1>
|
|
66
|
+
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
67
|
+
A reusable role selector backed by the Starter's NocoBase ACL store
|
|
68
|
+
and current Refine identity.
|
|
69
|
+
</p>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<section className="space-y-5 border-t pt-8">
|
|
73
|
+
<SectionTitle
|
|
74
|
+
eyebrow="Role selection"
|
|
75
|
+
title="RoleSwitcher"
|
|
76
|
+
description="The component resolves the signed-in user's roles and NocoBase role mode automatically. Business pages only decide its placement and presentation."
|
|
77
|
+
/>
|
|
78
|
+
<div className="grid items-start gap-5 xl:grid-cols-2">
|
|
79
|
+
<Card>
|
|
80
|
+
<CardHeader>
|
|
81
|
+
<CardTitle>Live component</CardTitle>
|
|
82
|
+
<CardDescription>
|
|
83
|
+
Uses the current identity and active NocoBase ACL response.
|
|
84
|
+
</CardDescription>
|
|
85
|
+
</CardHeader>
|
|
86
|
+
<CardContent>
|
|
87
|
+
<RoleSwitcher
|
|
88
|
+
showWhenUnavailable
|
|
89
|
+
className="w-full max-w-sm"
|
|
90
|
+
label="Working as"
|
|
91
|
+
/>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
<Card>
|
|
95
|
+
<CardHeader>
|
|
96
|
+
<CardTitle>Unavailable state</CardTitle>
|
|
97
|
+
<CardDescription>
|
|
98
|
+
With one usable role, showWhenUnavailable keeps identity visible
|
|
99
|
+
without presenting a disabled Select.
|
|
100
|
+
</CardDescription>
|
|
101
|
+
</CardHeader>
|
|
102
|
+
<CardContent>
|
|
103
|
+
<div className="flex min-h-9 items-center gap-2 text-sm">
|
|
104
|
+
<ShieldCheck className="size-4 text-muted-foreground" />
|
|
105
|
+
<span className="text-muted-foreground">Current role</span>
|
|
106
|
+
<Badge variant="secondary">Member</Badge>
|
|
107
|
+
</div>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</div>
|
|
111
|
+
</section>
|
|
112
|
+
|
|
113
|
+
<section className="space-y-5 border-t pt-8">
|
|
114
|
+
<SectionTitle
|
|
115
|
+
eyebrow="Prompt generator"
|
|
116
|
+
title="Place the role switcher in an existing application surface"
|
|
117
|
+
description="Describe the target placement and presentation. The generated prompt integrates the exported component without rebuilding its role-loading or switching behavior."
|
|
118
|
+
/>
|
|
119
|
+
<RoleSwitcherPromptGenerator />
|
|
120
|
+
</section>
|
|
121
|
+
|
|
122
|
+
<section className="space-y-5 border-t pt-8">
|
|
123
|
+
<SectionTitle
|
|
124
|
+
eyebrow="Component API"
|
|
125
|
+
title="Role switcher props"
|
|
126
|
+
description="Role data, union mode, Anonymous, persistence, and reload behavior are built in rather than exposed as duplicated application props."
|
|
127
|
+
/>
|
|
128
|
+
<PropsTable rows={roleSwitcherProps} />
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<section className="space-y-5 border-t pt-8">
|
|
132
|
+
<SectionTitle
|
|
133
|
+
eyebrow="Automatic behavior"
|
|
134
|
+
title="Role options follow NocoBase configuration"
|
|
135
|
+
description="These states are derived from the identity and roles:check response, so applications do not maintain a second role list."
|
|
136
|
+
/>
|
|
137
|
+
<Card className="gap-0 overflow-hidden py-0">
|
|
138
|
+
<Table>
|
|
139
|
+
<TableHeader>
|
|
140
|
+
<TableRow>
|
|
141
|
+
<TableHead>Condition</TableHead>
|
|
142
|
+
<TableHead>Rendered behavior</TableHead>
|
|
143
|
+
</TableRow>
|
|
144
|
+
</TableHeader>
|
|
145
|
+
<TableBody>
|
|
146
|
+
{roleBehaviors.map(([condition, behavior]) => (
|
|
147
|
+
<TableRow key={condition}>
|
|
148
|
+
<TableCell className="font-medium">{condition}</TableCell>
|
|
149
|
+
<TableCell className="text-muted-foreground">{behavior}</TableCell>
|
|
150
|
+
</TableRow>
|
|
151
|
+
))}
|
|
152
|
+
</TableBody>
|
|
153
|
+
</Table>
|
|
154
|
+
</Card>
|
|
155
|
+
</section>
|
|
156
|
+
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function PropsTable({ rows }: { rows: PropRow[] }) {
|
|
162
|
+
return (
|
|
163
|
+
<Card className="gap-0 overflow-hidden py-0">
|
|
164
|
+
<Table>
|
|
165
|
+
<TableHeader>
|
|
166
|
+
<TableRow>
|
|
167
|
+
<TableHead>Prop</TableHead>
|
|
168
|
+
<TableHead>Type</TableHead>
|
|
169
|
+
<TableHead>Default</TableHead>
|
|
170
|
+
<TableHead>Description</TableHead>
|
|
171
|
+
</TableRow>
|
|
172
|
+
</TableHeader>
|
|
173
|
+
<TableBody>
|
|
174
|
+
{rows.map(([name, type, defaultValue, description]) => (
|
|
175
|
+
<TableRow key={name}>
|
|
176
|
+
<TableCell className="font-mono text-xs font-medium">{name}</TableCell>
|
|
177
|
+
<TableCell className="font-mono text-xs text-muted-foreground">{type}</TableCell>
|
|
178
|
+
<TableCell className="font-mono text-xs text-muted-foreground">{defaultValue}</TableCell>
|
|
179
|
+
<TableCell className="min-w-72 whitespace-normal text-muted-foreground">
|
|
180
|
+
{description}
|
|
181
|
+
</TableCell>
|
|
182
|
+
</TableRow>
|
|
183
|
+
))}
|
|
184
|
+
</TableBody>
|
|
185
|
+
</Table>
|
|
186
|
+
</Card>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function SectionTitle({
|
|
191
|
+
eyebrow,
|
|
192
|
+
title,
|
|
193
|
+
description,
|
|
194
|
+
}: {
|
|
195
|
+
eyebrow: string;
|
|
196
|
+
title: string;
|
|
197
|
+
description: string;
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<div>
|
|
201
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
202
|
+
{eyebrow}
|
|
203
|
+
</p>
|
|
204
|
+
<h2 className="mt-2 text-xl font-semibold tracking-tight">{title}</h2>
|
|
205
|
+
<p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
206
|
+
{description}
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Check,
|
|
3
|
+
Eye,
|
|
4
|
+
LayoutPanelTop,
|
|
5
|
+
LockKeyhole,
|
|
6
|
+
Pencil,
|
|
7
|
+
Plus,
|
|
8
|
+
Trash2,
|
|
9
|
+
UserRoundCog,
|
|
10
|
+
UsersRound,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
|
|
13
|
+
import { Badge } from "@/components/ui/badge";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardContent,
|
|
18
|
+
CardDescription,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
} from "@/components/ui/card";
|
|
22
|
+
import { Input } from "@/components/ui/input";
|
|
23
|
+
import { AclBoundaryApi } from "./boundary-api";
|
|
24
|
+
import { AclScenarioSection } from "./scenario-section";
|
|
25
|
+
|
|
26
|
+
const demoUsers = [
|
|
27
|
+
{
|
|
28
|
+
id: 1,
|
|
29
|
+
name: "Alex Johnson",
|
|
30
|
+
username: "alex",
|
|
31
|
+
email: "alex@example.com",
|
|
32
|
+
phone: "+1 202 555 0148",
|
|
33
|
+
role: "Administrator",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 2,
|
|
37
|
+
name: "Jamie Chen",
|
|
38
|
+
username: "jamie",
|
|
39
|
+
email: "jamie@example.com",
|
|
40
|
+
phone: "+1 202 555 0186",
|
|
41
|
+
role: "Member",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 3,
|
|
45
|
+
name: "Morgan Lee",
|
|
46
|
+
username: "morgan",
|
|
47
|
+
email: "morgan@example.com",
|
|
48
|
+
phone: "+1 202 555 0162",
|
|
49
|
+
role: "Member",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function AclPatternsPage() {
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-10 pb-12">
|
|
56
|
+
<header className="space-y-2">
|
|
57
|
+
<h1 className="text-2xl font-semibold tracking-[-0.025em] sm:text-3xl">
|
|
58
|
+
Permission patterns
|
|
59
|
+
</h1>
|
|
60
|
+
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
61
|
+
Compare the authorized and restricted outcomes of navigation, pages,
|
|
62
|
+
regions, record actions, and form fields using the same fixed data.
|
|
63
|
+
</p>
|
|
64
|
+
</header>
|
|
65
|
+
|
|
66
|
+
<AclScenarioSection
|
|
67
|
+
eyebrow="Navigation permission"
|
|
68
|
+
title="Inaccessible resources disappear from navigation"
|
|
69
|
+
description="A denied collection is removed from the sidebar and its direct route is guarded. Other accessible navigation remains unchanged."
|
|
70
|
+
prompt={{
|
|
71
|
+
title: "Resource navigation",
|
|
72
|
+
description:
|
|
73
|
+
"Generate Refine resources, sidebar behavior, and guarded routes backed by NocoBase collections.",
|
|
74
|
+
defaultScene: "User and role administration",
|
|
75
|
+
defaultTarget:
|
|
76
|
+
"Allow the Users resource but remove Roles from navigation and block its direct URL.",
|
|
77
|
+
requirements: `- Mark collection resources with meta.acl.type = "collection".
|
|
78
|
+
- Let the Starter filter inaccessible sidebar items and choose the first accessible default route.
|
|
79
|
+
- Wrap list, create, show, and edit routes with ResourceAccessGuard.
|
|
80
|
+
- Mark non-collection utility pages as authenticated, snippet, route, or acl: false instead of treating them as collections.
|
|
81
|
+
- If a parent route is denied but an accessible child remains, navigation must continue to the child route.`,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<OutcomeComparison
|
|
85
|
+
allowedTitle="Users permission granted"
|
|
86
|
+
deniedTitle="Users permission denied"
|
|
87
|
+
allowed={<NavigationPreview showUsers />}
|
|
88
|
+
denied={<NavigationPreview showUsers={false} />}
|
|
89
|
+
/>
|
|
90
|
+
</AclScenarioSection>
|
|
91
|
+
|
|
92
|
+
<AclScenarioSection
|
|
93
|
+
eyebrow="Page permission"
|
|
94
|
+
title="A complete page is either available or replaced"
|
|
95
|
+
description="Page-level permission is useful when the whole route has one business purpose. Denial produces an understandable destination instead of a blank screen."
|
|
96
|
+
prompt={{
|
|
97
|
+
title: "Page permission",
|
|
98
|
+
description:
|
|
99
|
+
"Generate a complete page whose availability depends on NocoBase permissions.",
|
|
100
|
+
defaultScene: "User directory and account administration page",
|
|
101
|
+
defaultTarget: "Require users:list before rendering the complete page.",
|
|
102
|
+
requirements: `- Wrap the complete business page with AclPage.
|
|
103
|
+
- Use anyOf when one accessible collection is enough, or allOf when every permission is required.
|
|
104
|
+
- Provide a useful page-level fallback instead of rendering an empty screen.
|
|
105
|
+
- Keep each inner data region independently protected when the page combines multiple collections.`,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<OutcomeComparison
|
|
109
|
+
allowedTitle="Page allowed"
|
|
110
|
+
deniedTitle="Page denied"
|
|
111
|
+
allowed={<UserDirectoryPreview />}
|
|
112
|
+
denied={<PageDeniedPreview />}
|
|
113
|
+
/>
|
|
114
|
+
</AclScenarioSection>
|
|
115
|
+
|
|
116
|
+
<AclScenarioSection
|
|
117
|
+
eyebrow="Region permission"
|
|
118
|
+
title="Only the protected region changes"
|
|
119
|
+
description="On a page with multiple data sources, denying Roles must not hide the Users table. The restricted panel can be hidden or replaced with a local explanation."
|
|
120
|
+
prompt={{
|
|
121
|
+
title: "Region permission",
|
|
122
|
+
description:
|
|
123
|
+
"Generate a multi-table page where each region follows its own collection ACL.",
|
|
124
|
+
defaultScene: "Identity administration dashboard",
|
|
125
|
+
defaultTarget:
|
|
126
|
+
"Keep Users visible while replacing the denied Roles panel with a local fallback.",
|
|
127
|
+
requirements: `- Keep the outer page visible when its page-level permission is satisfied.
|
|
128
|
+
- Wrap every collection-backed panel in its own AclRegion.
|
|
129
|
+
- Enable each data query only when the corresponding list permission is allowed.
|
|
130
|
+
- Use fallback="hidden" for optional panels and fallback="forbidden" or a custom ReactNode when the missing region should be explained.
|
|
131
|
+
- Never use permission for one collection to hide unrelated tables or controls.`,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<OutcomeComparison
|
|
135
|
+
allowedTitle="Both regions allowed"
|
|
136
|
+
deniedTitle="Roles region denied"
|
|
137
|
+
allowed={<RegionPreview showRoles />}
|
|
138
|
+
denied={<RegionPreview showRoles={false} />}
|
|
139
|
+
/>
|
|
140
|
+
</AclScenarioSection>
|
|
141
|
+
|
|
142
|
+
<AclScenarioSection
|
|
143
|
+
eyebrow="Action and record permission"
|
|
144
|
+
title="Only unavailable actions are removed"
|
|
145
|
+
description="Collection permission controls global actions such as Create. Record ACL metadata controls Edit and Delete for each individual row."
|
|
146
|
+
prompt={{
|
|
147
|
+
title: "Action permission",
|
|
148
|
+
description:
|
|
149
|
+
"Generate a data table whose toolbar and row actions respond to collection and record scopes.",
|
|
150
|
+
defaultScene: "User administration table",
|
|
151
|
+
defaultTarget:
|
|
152
|
+
"Allow all actions for one user record and show a view-only row when update and delete are denied.",
|
|
153
|
+
requirements: `- Use the Starter dataProvider so X-With-ACL-Meta captures NocoBase allowedActions from list, get, and getMany responses.
|
|
154
|
+
- Use Refine Create, Show, Edit, and Delete buttons; do not manually compare user IDs or role names.
|
|
155
|
+
- Collection-level denial hides the action everywhere.
|
|
156
|
+
- Record-level allowedActions must hide or disable only the affected row action.
|
|
157
|
+
- Keep 403 responses authenticated and let NocoBase perform the final action validation.`,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<OutcomeComparison
|
|
161
|
+
allowedTitle="Create and row actions allowed"
|
|
162
|
+
deniedTitle="Create, Update, and Delete denied"
|
|
163
|
+
allowed={<ActionsPreview allowed />}
|
|
164
|
+
denied={<ActionsPreview allowed={false} />}
|
|
165
|
+
/>
|
|
166
|
+
</AclScenarioSection>
|
|
167
|
+
|
|
168
|
+
<AclScenarioSection
|
|
169
|
+
eyebrow="Field permission"
|
|
170
|
+
title="A form can mix editable, read-only, and hidden fields"
|
|
171
|
+
description="Field ACL does not have to hide the complete form. Only protected inputs change, while permitted fields remain usable."
|
|
172
|
+
prompt={{
|
|
173
|
+
title: "Field permission",
|
|
174
|
+
description:
|
|
175
|
+
"Generate a form or detail page whose individual fields follow NocoBase field ACL.",
|
|
176
|
+
defaultScene: "User profile edit form",
|
|
177
|
+
defaultTarget:
|
|
178
|
+
"Allow nickname editing, make email read-only, and hide phone when those fields are denied.",
|
|
179
|
+
requirements: `- Wrap each protected field or field group with AclField.
|
|
180
|
+
- Pass the collection resource, Refine action, and exact field name.
|
|
181
|
+
- Choose whether a denied field is hidden, replaced by read-only output, or shown with a custom fallback.
|
|
182
|
+
- Do not infer field access from the overall edit permission.
|
|
183
|
+
- Submit only values the current UI is allowed to edit; NocoBase still performs final field validation.`,
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<OutcomeComparison
|
|
187
|
+
allowedTitle="All fields editable"
|
|
188
|
+
deniedTitle="Email read-only and phone hidden"
|
|
189
|
+
allowed={<ProfileFormPreview restricted={false} />}
|
|
190
|
+
denied={<ProfileFormPreview restricted />}
|
|
191
|
+
/>
|
|
192
|
+
</AclScenarioSection>
|
|
193
|
+
|
|
194
|
+
<AclBoundaryApi />
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function OutcomeComparison({
|
|
200
|
+
allowedTitle,
|
|
201
|
+
deniedTitle,
|
|
202
|
+
allowed,
|
|
203
|
+
denied,
|
|
204
|
+
}: {
|
|
205
|
+
allowedTitle: string;
|
|
206
|
+
deniedTitle: string;
|
|
207
|
+
allowed: React.ReactNode;
|
|
208
|
+
denied: React.ReactNode;
|
|
209
|
+
}) {
|
|
210
|
+
return (
|
|
211
|
+
<div className="grid items-start gap-5 xl:grid-cols-2">
|
|
212
|
+
<div className="min-w-0 space-y-3">
|
|
213
|
+
<div className="flex items-center gap-2 text-sm font-medium text-emerald-700 dark:text-emerald-400">
|
|
214
|
+
<Check className="size-4" />
|
|
215
|
+
{allowedTitle}
|
|
216
|
+
</div>
|
|
217
|
+
{allowed}
|
|
218
|
+
</div>
|
|
219
|
+
<div className="min-w-0 space-y-3">
|
|
220
|
+
<div className="flex items-center gap-2 text-sm font-medium text-amber-700 dark:text-amber-400">
|
|
221
|
+
<LockKeyhole className="size-4" />
|
|
222
|
+
{deniedTitle}
|
|
223
|
+
</div>
|
|
224
|
+
{denied}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function NavigationPreview({ showUsers }: { showUsers: boolean }) {
|
|
231
|
+
return (
|
|
232
|
+
<div className="grid min-h-48 grid-cols-[150px_minmax(0,1fr)] overflow-hidden rounded-xl border bg-background">
|
|
233
|
+
<aside className="border-r bg-muted/35 p-3">
|
|
234
|
+
<div className="mb-3 text-xs font-semibold text-muted-foreground">Workspace</div>
|
|
235
|
+
<NavItem icon={<LayoutPanelTop />} label="Dashboard" />
|
|
236
|
+
{showUsers ? <NavItem icon={<UsersRound />} label="Users" active /> : null}
|
|
237
|
+
<NavItem icon={<UserRoundCog />} label="Roles" />
|
|
238
|
+
</aside>
|
|
239
|
+
<div className="flex items-center justify-center p-5 text-center">
|
|
240
|
+
{showUsers ? (
|
|
241
|
+
<div>
|
|
242
|
+
<UsersRound className="mx-auto size-7 text-primary" />
|
|
243
|
+
<div className="mt-2 font-medium">Users</div>
|
|
244
|
+
<div className="mt-1 text-xs text-muted-foreground">Route is available</div>
|
|
245
|
+
</div>
|
|
246
|
+
) : (
|
|
247
|
+
<div>
|
|
248
|
+
<LayoutPanelTop className="mx-auto size-7 text-muted-foreground" />
|
|
249
|
+
<div className="mt-2 font-medium">Dashboard</div>
|
|
250
|
+
<div className="mt-1 max-w-48 text-xs text-muted-foreground">
|
|
251
|
+
Users is absent from navigation and its direct route is guarded.
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function NavItem({
|
|
261
|
+
icon,
|
|
262
|
+
label,
|
|
263
|
+
active = false,
|
|
264
|
+
}: {
|
|
265
|
+
icon: React.ReactNode;
|
|
266
|
+
label: string;
|
|
267
|
+
active?: boolean;
|
|
268
|
+
}) {
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
className={`mb-1 flex items-center gap-2 rounded-md px-2 py-2 text-xs ${
|
|
272
|
+
active ? "bg-primary text-primary-foreground" : "text-muted-foreground"
|
|
273
|
+
}`}
|
|
274
|
+
>
|
|
275
|
+
<span className="[&_svg]:size-3.5">{icon}</span>
|
|
276
|
+
{label}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function UserDirectoryPreview() {
|
|
282
|
+
return (
|
|
283
|
+
<Card>
|
|
284
|
+
<CardHeader>
|
|
285
|
+
<CardTitle>User directory</CardTitle>
|
|
286
|
+
<CardDescription>3 users</CardDescription>
|
|
287
|
+
</CardHeader>
|
|
288
|
+
<CardContent>
|
|
289
|
+
<CompactUsersTable />
|
|
290
|
+
</CardContent>
|
|
291
|
+
</Card>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function PageDeniedPreview() {
|
|
296
|
+
return (
|
|
297
|
+
<div className="flex min-h-64 items-center justify-center rounded-xl border border-dashed bg-background p-6 text-center">
|
|
298
|
+
<div>
|
|
299
|
+
<LockKeyhole className="mx-auto size-8 text-muted-foreground" />
|
|
300
|
+
<div className="mt-3 font-medium">You cannot open this page</div>
|
|
301
|
+
<p className="mx-auto mt-1 max-w-xs text-sm text-muted-foreground">
|
|
302
|
+
Ask an administrator for access to the user directory or return to an
|
|
303
|
+
available page.
|
|
304
|
+
</p>
|
|
305
|
+
<Button className="mt-4" variant="outline" size="sm">
|
|
306
|
+
Return to dashboard
|
|
307
|
+
</Button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function RegionPreview({ showRoles }: { showRoles: boolean }) {
|
|
314
|
+
return (
|
|
315
|
+
<div
|
|
316
|
+
className={`grid gap-3 rounded-xl border bg-background p-3 ${
|
|
317
|
+
showRoles
|
|
318
|
+
? "md:grid-cols-[minmax(0,1.3fr)_minmax(180px,0.7fr)]"
|
|
319
|
+
: "grid-cols-1"
|
|
320
|
+
}`}
|
|
321
|
+
>
|
|
322
|
+
<Card size="sm">
|
|
323
|
+
<CardHeader>
|
|
324
|
+
<CardTitle>Users</CardTitle>
|
|
325
|
+
</CardHeader>
|
|
326
|
+
<CardContent>
|
|
327
|
+
<CompactUsersTable limit={2} />
|
|
328
|
+
</CardContent>
|
|
329
|
+
</Card>
|
|
330
|
+
{showRoles ? (
|
|
331
|
+
<Card size="sm">
|
|
332
|
+
<CardHeader>
|
|
333
|
+
<CardTitle>Roles</CardTitle>
|
|
334
|
+
</CardHeader>
|
|
335
|
+
<CardContent className="space-y-2">
|
|
336
|
+
<RoleRow title="Administrator" name="admin" />
|
|
337
|
+
<RoleRow title="Member" name="member" />
|
|
338
|
+
</CardContent>
|
|
339
|
+
</Card>
|
|
340
|
+
) : null}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ActionsPreview({ allowed }: { allowed: boolean }) {
|
|
346
|
+
return (
|
|
347
|
+
<Card>
|
|
348
|
+
<CardHeader className="flex-row items-center justify-between gap-3">
|
|
349
|
+
<div>
|
|
350
|
+
<CardTitle>Users</CardTitle>
|
|
351
|
+
<CardDescription>Record actions</CardDescription>
|
|
352
|
+
</div>
|
|
353
|
+
{allowed ? (
|
|
354
|
+
<Button size="sm">
|
|
355
|
+
<Plus /> New user
|
|
356
|
+
</Button>
|
|
357
|
+
) : null}
|
|
358
|
+
</CardHeader>
|
|
359
|
+
<CardContent className="overflow-x-auto">
|
|
360
|
+
<table className="w-full min-w-[420px] text-sm">
|
|
361
|
+
<thead>
|
|
362
|
+
<tr className="border-b text-left text-muted-foreground">
|
|
363
|
+
<th className="pb-3 font-medium">User</th>
|
|
364
|
+
<th className="pb-3 font-medium">Role</th>
|
|
365
|
+
<th className="pb-3 text-right font-medium">Actions</th>
|
|
366
|
+
</tr>
|
|
367
|
+
</thead>
|
|
368
|
+
<tbody>
|
|
369
|
+
{demoUsers.slice(0, 2).map((user) => (
|
|
370
|
+
<tr key={user.id} className="border-b last:border-0">
|
|
371
|
+
<td className="py-3 font-medium">{user.name}</td>
|
|
372
|
+
<td className="py-3 text-muted-foreground">{user.role}</td>
|
|
373
|
+
<td className="py-3">
|
|
374
|
+
<div className="flex justify-end gap-1">
|
|
375
|
+
<Button variant="ghost" size="icon-sm" aria-label="View user">
|
|
376
|
+
<Eye />
|
|
377
|
+
</Button>
|
|
378
|
+
{allowed ? (
|
|
379
|
+
<>
|
|
380
|
+
<Button variant="ghost" size="icon-sm" aria-label="Edit user">
|
|
381
|
+
<Pencil />
|
|
382
|
+
</Button>
|
|
383
|
+
<Button variant="ghost" size="icon-sm" aria-label="Delete user">
|
|
384
|
+
<Trash2 />
|
|
385
|
+
</Button>
|
|
386
|
+
</>
|
|
387
|
+
) : null}
|
|
388
|
+
</div>
|
|
389
|
+
</td>
|
|
390
|
+
</tr>
|
|
391
|
+
))}
|
|
392
|
+
</tbody>
|
|
393
|
+
</table>
|
|
394
|
+
</CardContent>
|
|
395
|
+
</Card>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function ProfileFormPreview({ restricted }: { restricted: boolean }) {
|
|
400
|
+
const user = demoUsers[1];
|
|
401
|
+
return (
|
|
402
|
+
<Card>
|
|
403
|
+
<CardHeader>
|
|
404
|
+
<CardTitle>Edit profile</CardTitle>
|
|
405
|
+
<CardDescription>{user.name}</CardDescription>
|
|
406
|
+
</CardHeader>
|
|
407
|
+
<CardContent className="space-y-4">
|
|
408
|
+
<DemoField label="Nickname">
|
|
409
|
+
<Input defaultValue={user.name} />
|
|
410
|
+
</DemoField>
|
|
411
|
+
<DemoField label="Email">
|
|
412
|
+
{restricted ? (
|
|
413
|
+
<div className="rounded-md border bg-muted/45 px-3 py-2 text-sm text-muted-foreground">
|
|
414
|
+
{user.email}
|
|
415
|
+
<Badge className="ml-2" variant="outline">Read only</Badge>
|
|
416
|
+
</div>
|
|
417
|
+
) : (
|
|
418
|
+
<Input defaultValue={user.email} />
|
|
419
|
+
)}
|
|
420
|
+
</DemoField>
|
|
421
|
+
{!restricted ? (
|
|
422
|
+
<DemoField label="Phone">
|
|
423
|
+
<Input defaultValue={user.phone} />
|
|
424
|
+
</DemoField>
|
|
425
|
+
) : null}
|
|
426
|
+
<Button size="sm">Save changes</Button>
|
|
427
|
+
</CardContent>
|
|
428
|
+
</Card>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function DemoField({ label, children }: { label: string; children: React.ReactNode }) {
|
|
433
|
+
return (
|
|
434
|
+
<label className="block space-y-2">
|
|
435
|
+
<span className="text-xs font-medium">{label}</span>
|
|
436
|
+
{children}
|
|
437
|
+
</label>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function CompactUsersTable({ limit = 3 }: { limit?: number }) {
|
|
442
|
+
return (
|
|
443
|
+
<div className="overflow-x-auto">
|
|
444
|
+
<table className="w-full min-w-[360px] text-sm">
|
|
445
|
+
<thead>
|
|
446
|
+
<tr className="border-b text-left text-muted-foreground">
|
|
447
|
+
<th className="pb-3 font-medium">User</th>
|
|
448
|
+
<th className="pb-3 font-medium">Email</th>
|
|
449
|
+
</tr>
|
|
450
|
+
</thead>
|
|
451
|
+
<tbody>
|
|
452
|
+
{demoUsers.slice(0, limit).map((user) => (
|
|
453
|
+
<tr key={user.id} className="border-b last:border-0">
|
|
454
|
+
<td className="py-3">
|
|
455
|
+
<div className="font-medium">{user.name}</div>
|
|
456
|
+
<div className="text-xs text-muted-foreground">{user.username}</div>
|
|
457
|
+
</td>
|
|
458
|
+
<td className="py-3 text-muted-foreground">{user.email}</td>
|
|
459
|
+
</tr>
|
|
460
|
+
))}
|
|
461
|
+
</tbody>
|
|
462
|
+
</table>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function RoleRow({ title, name }: { title: string; name: string }) {
|
|
468
|
+
return (
|
|
469
|
+
<div className="rounded-lg border px-3 py-2">
|
|
470
|
+
<div className="text-sm font-medium">{title}</div>
|
|
471
|
+
<div className="mt-0.5 font-mono text-xs text-muted-foreground">{name}</div>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Suspense, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function LazyAclRoute({ children }: { children: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<Suspense
|
|
6
|
+
fallback={
|
|
7
|
+
<div className="flex min-h-80 items-center justify-center text-sm text-muted-foreground">
|
|
8
|
+
Loading access control demo…
|
|
9
|
+
</div>
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
{children}
|
|
13
|
+
</Suspense>
|
|
14
|
+
);
|
|
15
|
+
}
|