@percepta/create 3.5.2 → 3.6.0
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/dist/index.js +61 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-versions.json +1 -1
- package/templates/monorepo/package.json.template +1 -1
- package/templates/webapp/README.md +2 -0
- package/templates/webapp/e2e/rbac.spec.ts +28 -26
- package/templates/webapp/package.json.template +2 -2
- package/templates/webapp/src/app/(app)/layout.tsx +4 -8
- package/templates/webapp/src/app/(app)/page.tsx +148 -7
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +29 -11
- package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +34 -12
- package/templates/webapp/src/app/(auth)/layout.tsx +30 -4
- package/templates/webapp/src/app/{(admin) → (settings)}/layout.tsx +6 -10
- package/templates/webapp/src/app/{(admin)/admin/_components/AdminTabs.tsx → (settings)/settings/_components/AccessControlTabs.tsx} +10 -6
- package/templates/webapp/src/app/{(admin)/admin/_lib/accessAdmin.ts → (settings)/settings/_lib/accessSettings.ts} +6 -6
- package/templates/webapp/src/app/{(admin)/admin → (settings)/settings}/page.tsx +99 -52
- package/templates/webapp/src/app/layout.tsx +1 -1
- package/templates/webapp/src/components/Header.tsx +27 -16
- package/templates/webapp/src/styles/globals.css +785 -8
|
@@ -18,10 +18,12 @@ test.describe("RBAC access", () => {
|
|
|
18
18
|
page,
|
|
19
19
|
}) => {
|
|
20
20
|
test.setTimeout(60_000);
|
|
21
|
-
await signIn(page, users.customerAdmin, "/
|
|
21
|
+
await signIn(page, users.customerAdmin, "/settings?tab=assignments");
|
|
22
22
|
|
|
23
|
-
await expect(
|
|
24
|
-
|
|
23
|
+
await expect(
|
|
24
|
+
page.getByRole("heading", { name: "Access control" }),
|
|
25
|
+
).toBeVisible();
|
|
26
|
+
await expect(page.getByRole("link", { name: "Settings" })).toBeVisible();
|
|
25
27
|
|
|
26
28
|
const userAssignments = page.getByRole("button", {
|
|
27
29
|
name: "User assignments",
|
|
@@ -67,18 +69,18 @@ test.describe("RBAC access", () => {
|
|
|
67
69
|
await expectAppAccess(browser, users.nonAppUser, false);
|
|
68
70
|
|
|
69
71
|
await page.goto("/");
|
|
70
|
-
await expect(page
|
|
71
|
-
0,
|
|
72
|
-
);
|
|
72
|
+
await expect(getAppHomeHeading(page)).toHaveCount(0);
|
|
73
73
|
await expectNotFound(page);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("app admin can see
|
|
76
|
+
test("app admin can see access control and main app but cannot edit default role mappings", async ({
|
|
77
77
|
page,
|
|
78
78
|
}) => {
|
|
79
|
-
await signIn(page, users.appAdmin, "/
|
|
79
|
+
await signIn(page, users.appAdmin, "/settings?tab=assignments");
|
|
80
80
|
|
|
81
|
-
await expect(
|
|
81
|
+
await expect(
|
|
82
|
+
page.getByRole("heading", { name: "Access control" }),
|
|
83
|
+
).toBeVisible();
|
|
82
84
|
await expect(
|
|
83
85
|
page
|
|
84
86
|
.getByText("Only customer admins can edit default application roles.")
|
|
@@ -93,33 +95,31 @@ test.describe("RBAC access", () => {
|
|
|
93
95
|
).toHaveAttribute("aria-disabled", "true");
|
|
94
96
|
|
|
95
97
|
await page.goto("/");
|
|
96
|
-
await expect(
|
|
97
|
-
page.getByRole("heading", { name: /Welcome to/ }),
|
|
98
|
-
).toBeVisible();
|
|
98
|
+
await expect(getAppHomeHeading(page)).toBeVisible();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
test("app user can enter the main app but cannot reach
|
|
101
|
+
test("app user can enter the main app but cannot reach settings", async ({
|
|
102
102
|
page,
|
|
103
103
|
}) => {
|
|
104
104
|
await signIn(page, users.appUser, "/");
|
|
105
105
|
|
|
106
|
-
await expect(
|
|
107
|
-
|
|
108
|
-
).toBeVisible();
|
|
109
|
-
await expect(page.getByRole("link", { name: "Admin" })).toHaveCount(0);
|
|
106
|
+
await expect(getAppHomeHeading(page)).toBeVisible();
|
|
107
|
+
await expect(page.getByRole("link", { name: "Settings" })).toHaveCount(0);
|
|
110
108
|
|
|
111
|
-
await page.goto("/
|
|
112
|
-
await expect(
|
|
109
|
+
await page.goto("/settings");
|
|
110
|
+
await expect(
|
|
111
|
+
page.getByRole("heading", { name: "Access control" }),
|
|
112
|
+
).toHaveCount(0);
|
|
113
113
|
await expectNotFound(page);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
test("non app user cannot enter the app", async ({ page }) => {
|
|
117
117
|
await signIn(page, users.nonAppUser, "/");
|
|
118
118
|
|
|
119
|
-
await expect(page
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
await expect(getAppHomeHeading(page)).toHaveCount(0);
|
|
120
|
+
await expect(
|
|
121
|
+
page.getByRole("heading", { name: "Access control" }),
|
|
122
|
+
).toHaveCount(0);
|
|
123
123
|
await expectNotFound(page);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
@@ -140,6 +140,10 @@ async function expectNotFound(page: Page) {
|
|
|
140
140
|
).toBeVisible();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function getAppHomeHeading(page: Page) {
|
|
144
|
+
return page.getByRole("heading", { name: /Good afternoon/ });
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
async function expectAppAccess(
|
|
144
148
|
browser: Browser,
|
|
145
149
|
email: string,
|
|
@@ -155,9 +159,7 @@ async function expectAppAccess(
|
|
|
155
159
|
|
|
156
160
|
try {
|
|
157
161
|
await signIn(page, email, "/");
|
|
158
|
-
return await page
|
|
159
|
-
.getByRole("heading", { name: /Welcome to/ })
|
|
160
|
-
.count();
|
|
162
|
+
return await getAppHomeHeading(page).count();
|
|
161
163
|
} finally {
|
|
162
164
|
await context.close();
|
|
163
165
|
}
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
|
|
53
53
|
"@opentelemetry/sdk-node": "^0.203.0",
|
|
54
54
|
"@__REPO_NAME__/auth": "workspace:*",
|
|
55
|
-
"@percepta/access-control": "0.
|
|
55
|
+
"@percepta/access-control": "0.8.0",
|
|
56
56
|
"@percepta/ai": "0.1.0",
|
|
57
57
|
"@percepta/database": "0.1.1",
|
|
58
|
-
"@percepta/design": "0.
|
|
58
|
+
"@percepta/design": "0.4.0",
|
|
59
59
|
"@percepta/inngest": "0.1.0",
|
|
60
60
|
"@percepta/logger": "0.1.0",
|
|
61
61
|
"@percepta/next-utils": "0.2.1",
|
|
@@ -20,7 +20,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|
|
20
20
|
canManageDefaultRoles(session.user.id),
|
|
21
21
|
canManageAppRoles(session.user.id),
|
|
22
22
|
]);
|
|
23
|
-
const
|
|
23
|
+
const showSettingsLink = canManageDefaults || canManageRoles;
|
|
24
24
|
|
|
25
25
|
if (!canAccessApp) {
|
|
26
26
|
notFound();
|
|
@@ -28,13 +28,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<>
|
|
31
|
-
<Header
|
|
32
|
-
<main>
|
|
33
|
-
<div className="
|
|
34
|
-
<div className="mx-auto max-w-6xl">
|
|
35
|
-
<div className="rounded-lg bg-white p-8">{children}</div>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
31
|
+
<Header showSettingsLink={showSettingsLink} />
|
|
32
|
+
<main className="app-shell-main">
|
|
33
|
+
<div className="app-home">{children}</div>
|
|
38
34
|
</main>
|
|
39
35
|
</>
|
|
40
36
|
);
|
|
@@ -5,15 +5,156 @@ export const metadata: Metadata = {
|
|
|
5
5
|
description: "__APP_TITLE__",
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const metrics = [
|
|
9
|
+
{ label: "Open projects", value: "12", delta: "+2", tone: "good" },
|
|
10
|
+
{ label: "Tasks this week", value: "47", delta: "+8%", tone: "good" },
|
|
11
|
+
{ label: "Time saved", value: "36h", delta: "+12%", tone: "good" },
|
|
12
|
+
{ label: "On-track rate", value: "91%", delta: "-2%", tone: "warn" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const activity = [
|
|
16
|
+
{
|
|
17
|
+
item: "Q2 procurement plan",
|
|
18
|
+
owner: "Mira K.",
|
|
19
|
+
status: "Approved",
|
|
20
|
+
updated: "12m ago",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
item: "Inventory forecast - West",
|
|
24
|
+
owner: "System",
|
|
25
|
+
status: "Running",
|
|
26
|
+
updated: "24m ago",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
item: "Allergen contamination rules",
|
|
30
|
+
owner: "Devon A.",
|
|
31
|
+
status: "Review",
|
|
32
|
+
updated: "1h ago",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
item: "Cocoa supplier - ABC Inc.",
|
|
36
|
+
owner: "System",
|
|
37
|
+
status: "Flagged",
|
|
38
|
+
updated: "2h ago",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
item: "New brand line variant",
|
|
42
|
+
owner: "Priya S.",
|
|
43
|
+
status: "Draft",
|
|
44
|
+
updated: "3h ago",
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const reviews = [
|
|
49
|
+
{ title: "Supplier change - Cocoa", due: "Due in 2 days" },
|
|
50
|
+
{ title: "Allergen rule update", due: "Due in 5 days" },
|
|
51
|
+
{ title: "Q2 production sequence", due: "Due in 9 days" },
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
8
54
|
export default function HomePage() {
|
|
9
55
|
return (
|
|
10
|
-
<div className="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
56
|
+
<div className="app-home-screen">
|
|
57
|
+
<section className="app-home-hero">
|
|
58
|
+
<div className="app-home-heading">
|
|
59
|
+
<p className="app-kicker">Overview</p>
|
|
60
|
+
<h1 className="app-title">Good afternoon, App Admin</h1>
|
|
61
|
+
<p className="app-subtitle">
|
|
62
|
+
Welcome to __APP_TITLE__. Three items want attention; the workspace
|
|
63
|
+
is ready for your team's first workflows.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="app-actions">
|
|
67
|
+
<span className="app-button-secondary">Last 7 days</span>
|
|
68
|
+
<span className="app-button-primary">New workflow</span>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section className="app-dashboard" aria-label="Workspace overview">
|
|
73
|
+
<div className="app-metrics">
|
|
74
|
+
{metrics.map((metric) => (
|
|
75
|
+
<article className="app-metric-card" key={metric.label}>
|
|
76
|
+
<div className="app-metric-top">
|
|
77
|
+
<span className="app-metric-label">{metric.label}</span>
|
|
78
|
+
<span
|
|
79
|
+
className={
|
|
80
|
+
metric.tone === "warn" ? "app-chip app-chip-warn" : "app-chip"
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
{metric.delta}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="app-metric-value">{metric.value}</div>
|
|
87
|
+
<div className="app-sparkline" aria-hidden={true} />
|
|
88
|
+
</article>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="app-content-grid">
|
|
93
|
+
<section className="app-panel">
|
|
94
|
+
<div className="app-panel-header">
|
|
95
|
+
<h2 className="app-panel-title">Recent activity</h2>
|
|
96
|
+
<span className="app-review-meta">View all</span>
|
|
97
|
+
</div>
|
|
98
|
+
<table className="app-table">
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th>Item</th>
|
|
102
|
+
<th>Owner</th>
|
|
103
|
+
<th>Status</th>
|
|
104
|
+
<th>Updated</th>
|
|
105
|
+
</tr>
|
|
106
|
+
</thead>
|
|
107
|
+
<tbody>
|
|
108
|
+
{activity.map((row) => (
|
|
109
|
+
<tr key={row.item}>
|
|
110
|
+
<td>{row.item}</td>
|
|
111
|
+
<td className="app-table-muted">{row.owner}</td>
|
|
112
|
+
<td>
|
|
113
|
+
<span className="app-status">{row.status}</span>
|
|
114
|
+
</td>
|
|
115
|
+
<td className="app-table-muted">{row.updated}</td>
|
|
116
|
+
</tr>
|
|
117
|
+
))}
|
|
118
|
+
</tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</section>
|
|
121
|
+
|
|
122
|
+
<div className="grid gap-5">
|
|
123
|
+
<section className="app-panel">
|
|
124
|
+
<div className="app-panel-header">
|
|
125
|
+
<h2 className="app-panel-title">Needs your review</h2>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="app-review-list">
|
|
128
|
+
{reviews.map((review) => (
|
|
129
|
+
<div className="app-review-item" key={review.title}>
|
|
130
|
+
<div>
|
|
131
|
+
<div className="app-review-title">{review.title}</div>
|
|
132
|
+
<div className="app-review-meta">{review.due}</div>
|
|
133
|
+
</div>
|
|
134
|
+
<span className="app-review-action">Review</span>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
<section className="app-panel">
|
|
141
|
+
<div className="app-panel-header">
|
|
142
|
+
<h2 className="app-panel-title">On call today</h2>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="app-oncall-list">
|
|
145
|
+
<div className="app-avatar-stack" aria-label="On call users">
|
|
146
|
+
<span>MK</span>
|
|
147
|
+
<span>DA</span>
|
|
148
|
+
<span>PS</span>
|
|
149
|
+
<span>AL</span>
|
|
150
|
+
<span>JR</span>
|
|
151
|
+
</div>
|
|
152
|
+
<p className="app-review-meta">4 more teammates available</p>
|
|
153
|
+
</div>
|
|
154
|
+
</section>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
17
158
|
</div>
|
|
18
159
|
);
|
|
19
160
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { Button, Input } from "@percepta/design";
|
|
5
|
+
import { ArrowRight } from "lucide-react";
|
|
5
6
|
import Link from "next/link";
|
|
6
7
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
8
|
import React, { useCallback } from "react";
|
|
@@ -64,26 +65,34 @@ export function CredentialsSignInForm() {
|
|
|
64
65
|
);
|
|
65
66
|
|
|
66
67
|
return (
|
|
67
|
-
<div className="
|
|
68
|
-
<div className="
|
|
69
|
-
<
|
|
70
|
-
|
|
68
|
+
<div className="grid gap-8">
|
|
69
|
+
<div className="grid gap-3">
|
|
70
|
+
<p className="app-auth-kicker">
|
|
71
|
+
<span>01</span>
|
|
72
|
+
<span>Sign in</span>
|
|
73
|
+
</p>
|
|
74
|
+
<h1 className="app-auth-title">Welcome back.</h1>
|
|
75
|
+
<p className="app-auth-copy text-sm">
|
|
71
76
|
Need an account?{" "}
|
|
72
77
|
<Link
|
|
73
|
-
className="
|
|
78
|
+
className="app-auth-link"
|
|
74
79
|
href={`/auth/signup?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
75
80
|
>
|
|
76
81
|
Create one
|
|
77
82
|
</Link>
|
|
78
83
|
</p>
|
|
79
84
|
</div>
|
|
80
|
-
<form className="
|
|
81
|
-
<div className="
|
|
85
|
+
<form className="app-auth-form" onSubmit={handleSubmit(submit)}>
|
|
86
|
+
<div className="app-auth-fields">
|
|
82
87
|
<Controller
|
|
83
88
|
control={control}
|
|
84
89
|
name="email"
|
|
85
90
|
render={({ field, fieldState }) => (
|
|
86
|
-
<FormItem
|
|
91
|
+
<FormItem
|
|
92
|
+
className="app-auth-field"
|
|
93
|
+
label="Email"
|
|
94
|
+
fieldState={fieldState}
|
|
95
|
+
>
|
|
87
96
|
<Input {...field} type="email" autoComplete="email" />
|
|
88
97
|
</FormItem>
|
|
89
98
|
)}
|
|
@@ -92,7 +101,11 @@ export function CredentialsSignInForm() {
|
|
|
92
101
|
control={control}
|
|
93
102
|
name="password"
|
|
94
103
|
render={({ field, fieldState }) => (
|
|
95
|
-
<FormItem
|
|
104
|
+
<FormItem
|
|
105
|
+
className="app-auth-field"
|
|
106
|
+
label="Password"
|
|
107
|
+
fieldState={fieldState}
|
|
108
|
+
>
|
|
96
109
|
<Input
|
|
97
110
|
{...field}
|
|
98
111
|
type="password"
|
|
@@ -103,8 +116,13 @@ export function CredentialsSignInForm() {
|
|
|
103
116
|
/>
|
|
104
117
|
</div>
|
|
105
118
|
<div className="flex justify-end">
|
|
106
|
-
<Button
|
|
107
|
-
|
|
119
|
+
<Button
|
|
120
|
+
className="app-auth-submit"
|
|
121
|
+
type="submit"
|
|
122
|
+
loading={isSubmitting}
|
|
123
|
+
>
|
|
124
|
+
<span>Sign In</span>
|
|
125
|
+
<ArrowRight aria-hidden={true} />
|
|
108
126
|
</Button>
|
|
109
127
|
</div>
|
|
110
128
|
</form>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { Button, Input } from "@percepta/design";
|
|
5
|
+
import { ArrowRight } from "lucide-react";
|
|
5
6
|
import Link from "next/link";
|
|
6
7
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
8
|
import React, { useCallback } from "react";
|
|
@@ -59,26 +60,34 @@ export function CredentialsSignUpForm() {
|
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
return (
|
|
62
|
-
<div className="
|
|
63
|
-
<div className="
|
|
64
|
-
<
|
|
65
|
-
|
|
63
|
+
<div className="grid gap-8">
|
|
64
|
+
<div className="grid gap-3">
|
|
65
|
+
<p className="app-auth-kicker">
|
|
66
|
+
<span>01</span>
|
|
67
|
+
<span>Request access</span>
|
|
68
|
+
</p>
|
|
69
|
+
<h1 className="app-auth-title">Create account.</h1>
|
|
70
|
+
<p className="app-auth-copy text-sm">
|
|
66
71
|
Already have an account?{" "}
|
|
67
72
|
<Link
|
|
68
|
-
className="
|
|
73
|
+
className="app-auth-link"
|
|
69
74
|
href={`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`}
|
|
70
75
|
>
|
|
71
76
|
Sign in
|
|
72
77
|
</Link>
|
|
73
78
|
</p>
|
|
74
79
|
</div>
|
|
75
|
-
<form className="
|
|
76
|
-
<div className="
|
|
80
|
+
<form className="app-auth-form" onSubmit={handleSubmit(submit)}>
|
|
81
|
+
<div className="app-auth-fields">
|
|
77
82
|
<Controller
|
|
78
83
|
control={control}
|
|
79
84
|
name="name"
|
|
80
85
|
render={({ field, fieldState }) => (
|
|
81
|
-
<FormItem
|
|
86
|
+
<FormItem
|
|
87
|
+
className="app-auth-field"
|
|
88
|
+
label="Name"
|
|
89
|
+
fieldState={fieldState}
|
|
90
|
+
>
|
|
82
91
|
<Input {...field} autoComplete="name" />
|
|
83
92
|
</FormItem>
|
|
84
93
|
)}
|
|
@@ -87,7 +96,11 @@ export function CredentialsSignUpForm() {
|
|
|
87
96
|
control={control}
|
|
88
97
|
name="email"
|
|
89
98
|
render={({ field, fieldState }) => (
|
|
90
|
-
<FormItem
|
|
99
|
+
<FormItem
|
|
100
|
+
className="app-auth-field"
|
|
101
|
+
label="Email"
|
|
102
|
+
fieldState={fieldState}
|
|
103
|
+
>
|
|
91
104
|
<Input {...field} type="email" autoComplete="email" />
|
|
92
105
|
</FormItem>
|
|
93
106
|
)}
|
|
@@ -96,15 +109,24 @@ export function CredentialsSignUpForm() {
|
|
|
96
109
|
control={control}
|
|
97
110
|
name="password"
|
|
98
111
|
render={({ field, fieldState }) => (
|
|
99
|
-
<FormItem
|
|
112
|
+
<FormItem
|
|
113
|
+
className="app-auth-field"
|
|
114
|
+
label="Password"
|
|
115
|
+
fieldState={fieldState}
|
|
116
|
+
>
|
|
100
117
|
<Input {...field} type="password" autoComplete="new-password" />
|
|
101
118
|
</FormItem>
|
|
102
119
|
)}
|
|
103
120
|
/>
|
|
104
121
|
</div>
|
|
105
122
|
<div className="flex justify-end">
|
|
106
|
-
<Button
|
|
107
|
-
|
|
123
|
+
<Button
|
|
124
|
+
className="app-auth-submit"
|
|
125
|
+
type="submit"
|
|
126
|
+
loading={isSubmitting}
|
|
127
|
+
>
|
|
128
|
+
<span>Create Account</span>
|
|
129
|
+
<ArrowRight aria-hidden={true} />
|
|
108
130
|
</Button>
|
|
109
131
|
</div>
|
|
110
132
|
</form>
|
|
@@ -5,11 +5,37 @@ export default function AuthLayout({
|
|
|
5
5
|
}: {
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
}) {
|
|
8
|
+
const appTitle = "__APP_TITLE__";
|
|
9
|
+
const appInitial = appTitle.slice(0, 1).toUpperCase();
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
|
-
<div className="
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
<div className="app-auth-shell">
|
|
13
|
+
<section className="app-auth-story" aria-label={`${appTitle} starter`}>
|
|
14
|
+
<div className="app-auth-brand">
|
|
15
|
+
<span className="app-auth-mark" aria-hidden={true}>
|
|
16
|
+
{appInitial}
|
|
17
|
+
</span>
|
|
18
|
+
<span>{appTitle}</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div className="grid max-w-xl gap-6">
|
|
22
|
+
<p className="app-auth-kicker">The workspace</p>
|
|
23
|
+
<h2 className="app-auth-title">
|
|
24
|
+
A quieter place to do your team's most considered work.
|
|
25
|
+
</h2>
|
|
26
|
+
<p className="app-auth-copy">
|
|
27
|
+
Keep decisions, handoffs, and daily operations in one calm
|
|
28
|
+
workspace built for focused teams.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="flex justify-between gap-4 text-xs uppercase text-muted-foreground">
|
|
33
|
+
<span>Workspace 1.0</span>
|
|
34
|
+
<span>Made for teams</span>
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<div className="app-auth-card">{children}</div>
|
|
13
39
|
</div>
|
|
14
40
|
);
|
|
15
41
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
canManageDefaultRoles,
|
|
8
8
|
} from "../../services/access/AppAccessControl";
|
|
9
9
|
|
|
10
|
-
export default async function
|
|
10
|
+
export default async function SettingsLayout({
|
|
11
11
|
children,
|
|
12
12
|
}: {
|
|
13
13
|
children: ReactNode;
|
|
@@ -22,21 +22,17 @@ export default async function AdminLayout({
|
|
|
22
22
|
canManageDefaultRoles(session.user.id),
|
|
23
23
|
canManageAppRoles(session.user.id),
|
|
24
24
|
]);
|
|
25
|
-
const
|
|
25
|
+
const showSettingsLink = canManageDefaults || canManageRoles;
|
|
26
26
|
|
|
27
|
-
if (!
|
|
27
|
+
if (!showSettingsLink) {
|
|
28
28
|
notFound();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<>
|
|
33
|
-
<Header
|
|
34
|
-
<main>
|
|
35
|
-
<div className="
|
|
36
|
-
<div className="mx-auto max-w-6xl">
|
|
37
|
-
<div className="rounded-lg bg-white p-8">{children}</div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
33
|
+
<Header showSettingsLink={showSettingsLink} />
|
|
34
|
+
<main className="app-shell-main">
|
|
35
|
+
<div className="app-home">{children}</div>
|
|
40
36
|
</main>
|
|
41
37
|
</>
|
|
42
38
|
);
|
|
@@ -3,25 +3,29 @@
|
|
|
3
3
|
import { Tabs, TabsList, TabsTrigger } from "@percepta/design";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type AccessControlTab = "assignments" | "roles";
|
|
7
7
|
|
|
8
8
|
const tabs = [
|
|
9
|
-
{ href: "/
|
|
9
|
+
{ href: "/settings?tab=roles", label: "Roles", tab: "roles" },
|
|
10
10
|
{
|
|
11
|
-
href: "/
|
|
11
|
+
href: "/settings?tab=assignments",
|
|
12
12
|
label: "Assignments",
|
|
13
13
|
tab: "assignments",
|
|
14
14
|
},
|
|
15
15
|
] as const satisfies ReadonlyArray<{
|
|
16
16
|
readonly href: string;
|
|
17
17
|
readonly label: string;
|
|
18
|
-
readonly tab:
|
|
18
|
+
readonly tab: AccessControlTab;
|
|
19
19
|
}>;
|
|
20
20
|
|
|
21
|
-
export function
|
|
21
|
+
export function AccessControlTabs({
|
|
22
|
+
activeTab,
|
|
23
|
+
}: {
|
|
24
|
+
readonly activeTab: AccessControlTab;
|
|
25
|
+
}) {
|
|
22
26
|
return (
|
|
23
27
|
<Tabs value={activeTab}>
|
|
24
|
-
<TabsList aria-label="
|
|
28
|
+
<TabsList aria-label="Access control views">
|
|
25
29
|
{tabs.map((tab) => (
|
|
26
30
|
<TabsTrigger asChild={true} key={tab.tab} value={tab.tab}>
|
|
27
31
|
<Link href={tab.href}>{tab.label}</Link>
|
|
@@ -12,13 +12,13 @@ export type AccessAppRole = AppRole<typeof accessManifest>;
|
|
|
12
12
|
export const assignableRoles: readonly AccessAppRole[] =
|
|
13
13
|
accessManifest.system.assignableRoles ?? [];
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface SettingsPermissions {
|
|
16
16
|
readonly canManageAppRoles: boolean;
|
|
17
17
|
readonly canManageDefaultRoles: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export async function
|
|
21
|
-
if (!
|
|
20
|
+
export async function requireAnySettingsPermission(): Promise<SettingsPermissions> {
|
|
21
|
+
if (!isAccessControlUiEnabled()) {
|
|
22
22
|
notFound();
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -55,8 +55,8 @@ export function readAssignableRole(formData: FormData): AccessAppRole {
|
|
|
55
55
|
return role as AccessAppRole;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
const
|
|
58
|
+
function isAccessControlUiEnabled(): boolean {
|
|
59
|
+
const accessControlUI: { readonly enabled?: boolean } | undefined =
|
|
60
60
|
accessManifest.adminUI;
|
|
61
|
-
return
|
|
61
|
+
return accessControlUI?.enabled !== false;
|
|
62
62
|
}
|