@pantheon-systems/create-p1-starter-kit 0.12.0 → 0.14.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/README.md +3 -3
- package/lib/cli.js +122 -53
- package/lib/cli.test.js +55 -0
- package/lib/copy-template.js +37 -0
- package/lib/copy-template.test.js +42 -1
- package/lib/messages.js +1 -1
- package/package.json +7 -4
- package/template/README.md +82 -0
- package/template/__tests__/auth-route.test.ts +1 -1
- package/template/__tests__/chatbot-flag-context.test.ts +55 -0
- package/template/__tests__/data-list-block-sub-components.test.tsx +246 -0
- package/template/__tests__/data-list-block-utils.test.ts +84 -0
- package/template/__tests__/data-list-block.test.ts +198 -0
- package/template/__tests__/published-page-routes.test.ts +86 -0
- package/template/__tests__/seo-metadata.test.ts +1 -1
- package/template/__tests__/styles-canvas-scope.test.ts +1 -1
- package/template/__tests__/widget-logout.test.ts +114 -0
- package/template/app/[...puckPath]/client.tsx +38 -10
- package/template/app/[...puckPath]/page.tsx +10 -117
- package/template/app/[...puckPath]/widget-logout.ts +36 -0
- package/template/app/p1/(editor)/[[...p1]]/editor-client.tsx +33 -140
- package/template/app/page.tsx +11 -69
- package/template/app/published-pages.tsx +23 -0
- package/template/app/styles.css +1 -1
- package/template/app/welcome-block.tsx +25 -0
- package/template/ci-examples/github-actions-sync-puck-registry.yml +15 -7
- package/template/components/ChatbotFlagProvider.tsx +3 -8
- package/template/components/p1-lockup.tsx +2 -1
- package/template/components/puck/image-block.tsx +5 -2
- package/template/components/puck/welcome-block-render.tsx +30 -37
- package/template/components/welcome-block.module.css +124 -0
- package/template/eslint.config.js +72 -1
- package/template/gitignore +43 -0
- package/template/lib/chatbot-flag/ai-generate.ts +1 -1
- package/template/lib/chatbot-flag/flag-context.ts +32 -0
- package/template/lib/page-seo.ts +1 -1
- package/template/package.json +5 -5
- package/template/scripts/__tests__/sync-puck-registry.test.ts +3 -3
- package/template/scripts/sync-puck-registry.ts +18 -18
- package/template/vitest.config.ts +17 -20
- package/template/CHANGELOG.md +0 -76
- package/template/__tests__/published-page-404.test.ts +0 -65
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LogoutOutcome } from "@pantheon-systems/puck-css";
|
|
3
|
+
import { runWidgetLogout } from "../app/[...puckPath]/widget-logout";
|
|
4
|
+
|
|
5
|
+
// Records the effects in order, so each test asserts the whole sequence a
|
|
6
|
+
// logout attempt produces rather than one call in isolation.
|
|
7
|
+
function recorder(logout: () => Promise<LogoutOutcome>) {
|
|
8
|
+
const calls: string[] = [];
|
|
9
|
+
return {
|
|
10
|
+
calls,
|
|
11
|
+
fx: {
|
|
12
|
+
logout,
|
|
13
|
+
navigate: (url: string) => calls.push(`navigate:${url}`),
|
|
14
|
+
reload: () => calls.push("reload"),
|
|
15
|
+
setBusy: (busy: boolean) => calls.push(`busy:${busy}`),
|
|
16
|
+
setError: (message: string | null) =>
|
|
17
|
+
calls.push(`error:${message ?? "cleared"}`),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LOGOUT_URL = "https://example.auth0.com/v2/logout?client_id=abc";
|
|
23
|
+
|
|
24
|
+
describe("runWidgetLogout", () => {
|
|
25
|
+
it("navigates to the Auth0 logout URL when the session ended", async () => {
|
|
26
|
+
const { calls, fx } = recorder(async () => ({
|
|
27
|
+
status: "signed_out",
|
|
28
|
+
logoutUrl: LOGOUT_URL,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
await runWidgetLogout(fx);
|
|
32
|
+
|
|
33
|
+
// Stays busy: the navigation replaces the page, so releasing the button
|
|
34
|
+
// would only flash it back to "Log out" on the way out.
|
|
35
|
+
expect(calls).toEqual([
|
|
36
|
+
"busy:true",
|
|
37
|
+
"error:cleared",
|
|
38
|
+
`navigate:${LOGOUT_URL}`,
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("keeps the menu open and shows why when logout failed", async () => {
|
|
43
|
+
const { calls, fx } = recorder(async () => ({
|
|
44
|
+
status: "error",
|
|
45
|
+
message: "Broker logout failed (503)",
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
await runWidgetLogout(fx);
|
|
49
|
+
|
|
50
|
+
// Still signed in and retryable, so the button must come back.
|
|
51
|
+
expect(calls).toEqual([
|
|
52
|
+
"busy:true",
|
|
53
|
+
"error:cleared",
|
|
54
|
+
"error:Broker logout failed (503)",
|
|
55
|
+
"busy:false",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("reloads when there was no session to end", async () => {
|
|
60
|
+
const { calls, fx } = recorder(async () => ({ status: "no_session" }));
|
|
61
|
+
|
|
62
|
+
await runWidgetLogout(fx);
|
|
63
|
+
|
|
64
|
+
expect(calls).toEqual(["busy:true", "error:cleared", "reload"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("reports a thrown error instead of leaving the button stuck", async () => {
|
|
68
|
+
const { calls, fx } = recorder(async () => {
|
|
69
|
+
throw new Error("Failed to fetch");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await runWidgetLogout(fx);
|
|
73
|
+
|
|
74
|
+
expect(calls).toEqual([
|
|
75
|
+
"busy:true",
|
|
76
|
+
"error:cleared",
|
|
77
|
+
"error:Failed to fetch",
|
|
78
|
+
"busy:false",
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("falls back to a generic message when something non-Error is thrown", async () => {
|
|
83
|
+
const { calls, fx } = recorder(async () => {
|
|
84
|
+
throw "socket closed";
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await runWidgetLogout(fx);
|
|
88
|
+
|
|
89
|
+
expect(calls).toEqual([
|
|
90
|
+
"busy:true",
|
|
91
|
+
"error:cleared",
|
|
92
|
+
"error:Logout failed",
|
|
93
|
+
"busy:false",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("clears a previous failure before retrying", async () => {
|
|
98
|
+
const { calls, fx } = recorder(async () => ({
|
|
99
|
+
status: "signed_out",
|
|
100
|
+
logoutUrl: LOGOUT_URL,
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
await runWidgetLogout(fx);
|
|
104
|
+
await runWidgetLogout(fx);
|
|
105
|
+
|
|
106
|
+
// The second attempt clears the slot again; a stale message must not sit
|
|
107
|
+
// under a logout that is now succeeding.
|
|
108
|
+
expect(calls.slice(3)).toEqual([
|
|
109
|
+
"busy:true",
|
|
110
|
+
"error:cleared",
|
|
111
|
+
`navigate:${LOGOUT_URL}`,
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { Data } from "@puckeditor/core";
|
|
5
|
-
import { RenderClient } from "@pantheon-systems/puck-css";
|
|
5
|
+
import { RenderClient, performLogout, P1_LOGGED_IN_KEY } from "@pantheon-systems/puck-css";
|
|
6
6
|
import config from "../../puck.config";
|
|
7
|
+
import { runWidgetLogout } from "./widget-logout";
|
|
7
8
|
|
|
8
9
|
function EditIcon() {
|
|
9
10
|
return (
|
|
@@ -35,10 +36,12 @@ function LogoutIcon() {
|
|
|
35
36
|
export function P1EditWidget({ route }: { route: string }) {
|
|
36
37
|
const [hasToken, setHasToken] = useState(false);
|
|
37
38
|
const [open, setOpen] = useState(false);
|
|
39
|
+
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
40
|
+
const [logoutError, setLogoutError] = useState<string | null>(null);
|
|
38
41
|
|
|
39
42
|
useEffect(() => {
|
|
40
43
|
const flag = typeof localStorage !== "undefined"
|
|
41
|
-
? localStorage.getItem(
|
|
44
|
+
? localStorage.getItem(P1_LOGGED_IN_KEY)
|
|
42
45
|
: null;
|
|
43
46
|
setHasToken(!!flag);
|
|
44
47
|
}, []);
|
|
@@ -63,6 +66,22 @@ export function P1EditWidget({ route }: { route: string }) {
|
|
|
63
66
|
|
|
64
67
|
const editHref = `/p1${route === "/" ? "" : route}`;
|
|
65
68
|
|
|
69
|
+
const handleLogout = () =>
|
|
70
|
+
runWidgetLogout({
|
|
71
|
+
logout: () =>
|
|
72
|
+
performLogout({
|
|
73
|
+
cssBaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL ?? "http://localhost:8787",
|
|
74
|
+
}),
|
|
75
|
+
navigate: (url) => {
|
|
76
|
+
window.location.href = url;
|
|
77
|
+
},
|
|
78
|
+
reload: () => {
|
|
79
|
+
window.location.reload();
|
|
80
|
+
},
|
|
81
|
+
setBusy: setIsLoggingOut,
|
|
82
|
+
setError: setLogoutError,
|
|
83
|
+
});
|
|
84
|
+
|
|
66
85
|
return (
|
|
67
86
|
<div
|
|
68
87
|
data-p1-widget
|
|
@@ -145,12 +164,8 @@ export function P1EditWidget({ route }: { route: string }) {
|
|
|
145
164
|
<div style={{ height: 1, background: "#e0e0e0", margin: "6px 4px" }} />
|
|
146
165
|
<button
|
|
147
166
|
role="menuitem"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
localStorage.removeItem("p1_auth_token");
|
|
151
|
-
localStorage.removeItem("css_broker_token");
|
|
152
|
-
window.location.reload();
|
|
153
|
-
}}
|
|
167
|
+
disabled={isLoggingOut}
|
|
168
|
+
onClick={handleLogout}
|
|
154
169
|
style={{
|
|
155
170
|
display: "flex",
|
|
156
171
|
alignItems: "center",
|
|
@@ -163,15 +178,28 @@ export function P1EditWidget({ route }: { route: string }) {
|
|
|
163
178
|
textDecoration: "none",
|
|
164
179
|
background: "transparent",
|
|
165
180
|
border: "none",
|
|
166
|
-
cursor: "pointer",
|
|
181
|
+
cursor: isLoggingOut ? "not-allowed" : "pointer",
|
|
167
182
|
fontFamily: "inherit",
|
|
168
183
|
}}
|
|
169
184
|
onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
|
|
170
185
|
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
|
|
171
186
|
>
|
|
172
187
|
<span style={{ color: "#888", flex: "0 0 auto" }}><LogoutIcon /></span>
|
|
173
|
-
Log out
|
|
188
|
+
{isLoggingOut ? "Logging out…" : "Log out"}
|
|
174
189
|
</button>
|
|
190
|
+
{logoutError && (
|
|
191
|
+
<p
|
|
192
|
+
role="alert"
|
|
193
|
+
style={{
|
|
194
|
+
margin: "2px 10px 6px",
|
|
195
|
+
fontSize: 12,
|
|
196
|
+
lineHeight: 1.4,
|
|
197
|
+
color: "#b3261e",
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{logoutError}
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
175
203
|
</div>
|
|
176
204
|
)}
|
|
177
205
|
</div>
|
|
@@ -1,130 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Catch-all route that renders user-facing pages generated by Puck.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline lives in the SDK — see createPublishedPage in
|
|
5
|
+
* app/published-pages.tsx for what this route is made of.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import type { Metadata } from "next";
|
|
7
|
-
import { notFound } from "next/navigation";
|
|
8
|
-
import {
|
|
9
|
-
loadRemoteDatasourceContext,
|
|
10
|
-
extractReferencedDatasourceIds,
|
|
11
|
-
resolveDataTemplates,
|
|
12
|
-
pagePathFromCatchAllSegments,
|
|
13
|
-
} from "@pantheon-systems/puck-css/server";
|
|
14
|
-
import {
|
|
15
|
-
createCssQueryFetchers,
|
|
16
|
-
loadPublishedPage,
|
|
17
|
-
loadRouteTemplateKeys,
|
|
18
|
-
} from "@pantheon-systems/p1-next-sdk/server";
|
|
19
|
-
import { REMOTE_DATASOURCE_FETCHERS } from "../../lib/remote-datasource-fetchers";
|
|
20
|
-
import { ContentUnavailable } from "../../components/content-unavailable";
|
|
21
|
-
import { resolvePageMetadata } from "../../lib/page-seo";
|
|
22
|
-
import { Client } from "./client";
|
|
23
|
-
|
|
24
|
-
const getCssQueryFetchers = cache(() => createCssQueryFetchers());
|
|
25
|
-
|
|
26
|
-
// Document namespaces that live alongside pages but are never routable.
|
|
27
|
-
const INTERNAL_PATH_PREFIXES = ["/_registry", "/_redirects"];
|
|
28
|
-
|
|
29
|
-
// Lowercased to match the server, which normalizes document paths to lower case
|
|
30
|
-
// before looking them up — so /_Redirects/x resolves the same record as
|
|
31
|
-
// /_redirects/x and must be refused just the same.
|
|
32
|
-
function isInternalPath(path: string): boolean {
|
|
33
|
-
const normalized = path.toLowerCase();
|
|
34
|
-
return INTERNAL_PATH_PREFIXES.some(
|
|
35
|
-
(prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),
|
|
36
|
-
);
|
|
37
|
-
}
|
|
8
|
+
import { published } from "../published-pages";
|
|
38
9
|
|
|
39
10
|
/**
|
|
40
11
|
* Backstop only: publishing calls revalidatePath for the affected routes, so
|
|
41
12
|
* cached pages normally refresh the moment their content changes. This bounds
|
|
42
13
|
* how long an edit made outside that path (a direct API write, a restored
|
|
43
14
|
* branch) can stay stale.
|
|
15
|
+
*
|
|
16
|
+
* Must stay a literal here: Next.js statically analyzes segment-config exports,
|
|
17
|
+
* so a value re-exported from the factory would go undetected.
|
|
44
18
|
*/
|
|
45
19
|
export const revalidate = 300;
|
|
46
20
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* renderable at all; without it every path here renders fully dynamically and
|
|
51
|
-
* no response is ever cacheable. Unlisted paths render on first request and are
|
|
52
|
-
* cached from then on (dynamicParams defaults to true).
|
|
53
|
-
*/
|
|
54
|
-
export function generateStaticParams(): { puckPath: string[] }[] {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function generateMetadata({
|
|
59
|
-
params,
|
|
60
|
-
}: {
|
|
61
|
-
params: Promise<{ puckPath: string[] }>;
|
|
62
|
-
}): Promise<Metadata> {
|
|
63
|
-
const { puckPath = [] } = await params;
|
|
64
|
-
const path = pagePathFromCatchAllSegments(puckPath);
|
|
65
|
-
|
|
66
|
-
if (isInternalPath(path)) {
|
|
67
|
-
return { title: "Not Found" };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = await loadPublishedPage(path);
|
|
71
|
-
if (result.status === "missing") return { title: "Not Found" };
|
|
72
|
-
if (result.status === "unavailable") {
|
|
73
|
-
return { title: "Temporarily unavailable" };
|
|
74
|
-
}
|
|
75
|
-
return resolvePageMetadata({ pageData: result.data, path });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default async function Page({
|
|
79
|
-
params,
|
|
80
|
-
}: {
|
|
81
|
-
params: Promise<{ puckPath: string[] }>;
|
|
82
|
-
}) {
|
|
83
|
-
const { puckPath = [] } = await params;
|
|
84
|
-
const path = pagePathFromCatchAllSegments(puckPath);
|
|
85
|
-
|
|
86
|
-
if (isInternalPath(path)) {
|
|
87
|
-
notFound();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = await loadPublishedPage(path);
|
|
91
|
-
|
|
92
|
-
// A real 404, so misses are not cached as successes now that this route is
|
|
93
|
-
// statically renderable. An outage is deliberately not a 404 — that would
|
|
94
|
-
// deindex published pages over a transient blip.
|
|
95
|
-
if (result.status === "missing") {
|
|
96
|
-
notFound();
|
|
97
|
-
}
|
|
98
|
-
if (result.status === "unavailable") {
|
|
99
|
-
return <ContentUnavailable />;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const data = result.data;
|
|
103
|
-
|
|
104
|
-
const [routeTemplateKeys, cssQueryFetchers] = await Promise.all([
|
|
105
|
-
loadRouteTemplateKeys(),
|
|
106
|
-
getCssQueryFetchers(),
|
|
107
|
-
]);
|
|
108
|
-
const builtinFetchers = [...REMOTE_DATASOURCE_FETCHERS, ...cssQueryFetchers];
|
|
109
|
-
|
|
110
|
-
const referencedDatasourceIds = extractReferencedDatasourceIds(data);
|
|
111
|
-
const context = await loadRemoteDatasourceContext({
|
|
112
|
-
fetchImpl: fetch,
|
|
113
|
-
pagePath: path,
|
|
114
|
-
routeTemplateKeys,
|
|
115
|
-
builtinFetchers,
|
|
116
|
-
referencedDatasourceIds,
|
|
117
|
-
});
|
|
118
|
-
const resolvedData = await resolveDataTemplates(data, context);
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<Client
|
|
122
|
-
data={resolvedData}
|
|
123
|
-
pageMetadata={{
|
|
124
|
-
route: path,
|
|
125
|
-
documentName: data.root.props?.title as string | undefined,
|
|
126
|
-
pageType: "page",
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
21
|
+
export const generateStaticParams = published.generateStaticParams;
|
|
22
|
+
export const generateMetadata = published.generateMetadata;
|
|
23
|
+
export default published.Page;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LogoutOutcome } from "@pantheon-systems/puck-css";
|
|
2
|
+
|
|
3
|
+
export type WidgetLogoutEffects = {
|
|
4
|
+
logout: () => Promise<LogoutOutcome>;
|
|
5
|
+
navigate: (url: string) => void;
|
|
6
|
+
reload: () => void;
|
|
7
|
+
setBusy: (busy: boolean) => void;
|
|
8
|
+
setError: (message: string | null) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs one logout attempt for the widget. Kept outside the component so each
|
|
13
|
+
* outcome — navigate away, stay and explain, reload — can be exercised without
|
|
14
|
+
* a browser.
|
|
15
|
+
*/
|
|
16
|
+
export async function runWidgetLogout(fx: WidgetLogoutEffects): Promise<void> {
|
|
17
|
+
fx.setBusy(true);
|
|
18
|
+
fx.setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const outcome = await fx.logout();
|
|
21
|
+
if (outcome.status === "signed_out") {
|
|
22
|
+
fx.navigate(outcome.logoutUrl);
|
|
23
|
+
return; // navigation takes over
|
|
24
|
+
}
|
|
25
|
+
if (outcome.status === "error") {
|
|
26
|
+
// Still signed in and retryable — keep the menu open and say why.
|
|
27
|
+
fx.setError(outcome.message);
|
|
28
|
+
fx.setBusy(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
fx.reload(); // no_session: drop any stale widget state
|
|
32
|
+
} catch (err) {
|
|
33
|
+
fx.setError(err instanceof Error ? err.message : "Logout failed");
|
|
34
|
+
fx.setBusy(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
wrapConfigForEditorPreview,
|
|
15
15
|
P1QueryProvider,
|
|
16
16
|
editorPathHref,
|
|
17
|
+
EditorReloadOverlay,
|
|
17
18
|
} from "@pantheon-systems/puck-css";
|
|
18
19
|
import { DatasourceRegistryProvider, DatasourceDataProvider } from "@pantheon-systems/puck-css/fields";
|
|
19
20
|
import { LoadingMessage } from "@pantheon-systems/puck-css/pds";
|
|
@@ -30,42 +31,12 @@ import "@pantheon-systems/puck-css/pds/styles.css";
|
|
|
30
31
|
|
|
31
32
|
import { ChatbotFlagProvider } from "../../../../components/ChatbotFlagProvider";
|
|
32
33
|
import { P1Lockup } from "../../../../components/p1-lockup";
|
|
34
|
+
import styles from "../../../../components/welcome-block.module.css";
|
|
33
35
|
import config from "../../../../puck.config";
|
|
34
36
|
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../../lib/chatbot-flag/feature-gate";
|
|
35
37
|
import { createGenerateWithAIHandler } from "../../../../lib/chatbot-flag/ai-generate";
|
|
36
38
|
import { getDraftRequestChannel } from "../../../../lib/chatbot-flag/draft-request-channel";
|
|
37
39
|
|
|
38
|
-
const DEFAULT_PAGE_DATA = {
|
|
39
|
-
root: { props: { title: "New page" } },
|
|
40
|
-
content: [],
|
|
41
|
-
zones: {},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const DEFAULT_ROOT_PAGE_DATA = {
|
|
45
|
-
root: { props: { title: "Welcome | P1 site" } },
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "P1WelcomeBlock",
|
|
49
|
-
props: {
|
|
50
|
-
id: "seed-welcome",
|
|
51
|
-
heading: "Welcome to your new Pantheon P1 Site.",
|
|
52
|
-
description: "You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages.",
|
|
53
|
-
ctaLabel: "Sign-in to P1",
|
|
54
|
-
ctaHref: "/p1",
|
|
55
|
-
footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
56
|
-
loggedInHeading: "Welcome to your new Pantheon P1 Site.",
|
|
57
|
-
loggedInDescription: "You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
|
|
58
|
-
loggedInCtaLabel: "Edit this page with P1 Visual Editor",
|
|
59
|
-
loggedInCtaHref: "/p1",
|
|
60
|
-
loggedInSecondaryLabel: "Go to P1 Dashboard",
|
|
61
|
-
loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
62
|
-
showLogo: true,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
zones: {},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
40
|
let p1Config: ReturnType<typeof createNextConfig> | null = null;
|
|
70
41
|
let p1ConfigError: string | null = null;
|
|
71
42
|
|
|
@@ -83,33 +54,31 @@ function P1SignInPage() {
|
|
|
83
54
|
const { login, isLoading, error } = useP1Auth();
|
|
84
55
|
|
|
85
56
|
return (
|
|
86
|
-
<div className=
|
|
87
|
-
<
|
|
57
|
+
<div className={styles.surface}>
|
|
58
|
+
<div className={styles.inner}>
|
|
59
|
+
<P1Lockup />
|
|
88
60
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
61
|
+
<h1 className={styles.heading}>
|
|
62
|
+
Your Collaborative Website Management Workspace.
|
|
63
|
+
</h1>
|
|
64
|
+
<p className={styles.description}>
|
|
65
|
+
Log in to your Pantheon P1 account to edit your P1 powered website.
|
|
66
|
+
If you don't have yet a Pantheon P1 account, contact us{" "}
|
|
67
|
+
<a href="https://pantheon.io/contact-us" className={styles.link}>here</a>.
|
|
68
|
+
</p>
|
|
97
69
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
70
|
+
<div className={styles.actions}>
|
|
71
|
+
<button
|
|
72
|
+
className={styles.button}
|
|
73
|
+
onClick={() => void login()}
|
|
74
|
+
disabled={isLoading}
|
|
75
|
+
>
|
|
76
|
+
{isLoading ? "Signing in..." : "Continue"}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
107
79
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{error}
|
|
111
|
-
</p>
|
|
112
|
-
)}
|
|
80
|
+
{error && <p className={styles.error}>{error}</p>}
|
|
81
|
+
</div>
|
|
113
82
|
</div>
|
|
114
83
|
);
|
|
115
84
|
}
|
|
@@ -120,7 +89,6 @@ export function EditorClientWrapper() {
|
|
|
120
89
|
const pathname = usePathname();
|
|
121
90
|
const path = editorPagePathFromUrlPath(pathname);
|
|
122
91
|
const [userRole, setUserRole] = useState<ContentRole>('editor');
|
|
123
|
-
const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
|
|
124
92
|
|
|
125
93
|
if (!p1Config) {
|
|
126
94
|
return (
|
|
@@ -145,7 +113,7 @@ export function EditorClientWrapper() {
|
|
|
145
113
|
loginFallback={<P1SignInPage />}
|
|
146
114
|
>
|
|
147
115
|
<ChatbotFlagProvider>
|
|
148
|
-
<EditorContent path={path}
|
|
116
|
+
<EditorContent path={path} />
|
|
149
117
|
</ChatbotFlagProvider>
|
|
150
118
|
</P1App>
|
|
151
119
|
{process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
|
|
@@ -206,15 +174,8 @@ function RoleSwitcher({
|
|
|
206
174
|
);
|
|
207
175
|
}
|
|
208
176
|
|
|
209
|
-
function EditorContent({
|
|
210
|
-
path,
|
|
211
|
-
lastGoodStateRef,
|
|
212
|
-
}: {
|
|
213
|
-
path: string;
|
|
214
|
-
lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
|
|
215
|
-
}) {
|
|
177
|
+
function EditorContent({ path }: { path: string }) {
|
|
216
178
|
const router = useRouter();
|
|
217
|
-
const { getToken } = useP1Auth();
|
|
218
179
|
const { data: editorCtx } = useEditorContext(path);
|
|
219
180
|
const {
|
|
220
181
|
context: remoteDatasourceContext,
|
|
@@ -264,27 +225,10 @@ function EditorContent({
|
|
|
264
225
|
[router],
|
|
265
226
|
);
|
|
266
227
|
|
|
267
|
-
const
|
|
268
|
-
async (docPath: string, _error: Error) => {
|
|
269
|
-
const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
|
|
270
|
-
const token = await getToken();
|
|
271
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
272
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
273
|
-
const res = await fetch("/p1/api/structure/page", {
|
|
274
|
-
method: "POST",
|
|
275
|
-
headers,
|
|
276
|
-
body: JSON.stringify({ path: docPath, initialData }),
|
|
277
|
-
});
|
|
278
|
-
return res.ok;
|
|
279
|
-
},
|
|
280
|
-
[getToken],
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const { loading, error, puckKey, puckProps } = useP1Editor({
|
|
228
|
+
const { loading, reloading, hasContent, error, puckKey, puckProps } = useP1Editor({
|
|
284
229
|
documentPath: path,
|
|
285
230
|
puckConfig: editorConfig,
|
|
286
231
|
additionalPlugins,
|
|
287
|
-
onDocumentNotFound: handleDocumentNotFound,
|
|
288
232
|
pluginOptions: {
|
|
289
233
|
onDocumentSelect: handleDocumentSelect,
|
|
290
234
|
onGenerateWithAI: createGenerateWithAIHandler(draftRequests, chatbotEnabled),
|
|
@@ -305,24 +249,17 @@ function EditorContent({
|
|
|
305
249
|
},
|
|
306
250
|
});
|
|
307
251
|
|
|
308
|
-
// Update last good state when loading completes successfully (ref passed from parent)
|
|
309
|
-
React.useEffect(() => {
|
|
310
|
-
if (!loading && !error) {
|
|
311
|
-
lastGoodStateRef.current = { puckKey, puckProps };
|
|
312
|
-
}
|
|
313
|
-
}, [loading, error, puckKey, puckProps]);
|
|
314
|
-
|
|
315
252
|
if (redirecting) {
|
|
316
253
|
return <LoadingMessage message="Redirecting" data-testid="editor-redirecting" />;
|
|
317
254
|
}
|
|
318
255
|
|
|
319
|
-
|
|
320
|
-
if (loading && !lastGoodStateRef.current) {
|
|
256
|
+
if (loading) {
|
|
321
257
|
return <LoadingMessage message="Loading document" data-testid="editor-loading" />;
|
|
322
258
|
}
|
|
323
259
|
|
|
324
|
-
//
|
|
325
|
-
|
|
260
|
+
// A failed load with a document already on screen keeps that document; only a
|
|
261
|
+
// failure with nothing to fall back on takes over the view.
|
|
262
|
+
if (error && !hasContent) {
|
|
326
263
|
return (
|
|
327
264
|
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
328
265
|
<h3>Error loading document</h3>
|
|
@@ -331,57 +268,13 @@ function EditorContent({
|
|
|
331
268
|
);
|
|
332
269
|
}
|
|
333
270
|
|
|
334
|
-
// Use current state if loaded, otherwise keep showing last good state
|
|
335
|
-
const displayState = (!loading && !error)
|
|
336
|
-
? { puckKey, puckProps }
|
|
337
|
-
: lastGoodStateRef.current ?? { puckKey, puckProps };
|
|
338
|
-
|
|
339
271
|
return (
|
|
340
272
|
<div className="puck-editor-theme" style={{ position: "relative" }}>
|
|
341
|
-
{
|
|
342
|
-
{loading && lastGoodStateRef.current && (
|
|
343
|
-
<div
|
|
344
|
-
style={{
|
|
345
|
-
position: "fixed",
|
|
346
|
-
top: "50%",
|
|
347
|
-
left: "50%",
|
|
348
|
-
transform: "translate(-50%, -50%)",
|
|
349
|
-
zIndex: 9999,
|
|
350
|
-
background: "rgba(255, 255, 255, 0.95)",
|
|
351
|
-
padding: "1rem 2rem",
|
|
352
|
-
borderRadius: "8px",
|
|
353
|
-
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
354
|
-
fontFamily: "system-ui",
|
|
355
|
-
fontSize: "14px",
|
|
356
|
-
color: "#333",
|
|
357
|
-
fontWeight: 500,
|
|
358
|
-
display: "flex",
|
|
359
|
-
alignItems: "center",
|
|
360
|
-
gap: "0.75rem",
|
|
361
|
-
}}
|
|
362
|
-
>
|
|
363
|
-
<div
|
|
364
|
-
style={{
|
|
365
|
-
width: "16px",
|
|
366
|
-
height: "16px",
|
|
367
|
-
border: "2px solid #e0e0e0",
|
|
368
|
-
borderTopColor: "#2563eb",
|
|
369
|
-
borderRadius: "50%",
|
|
370
|
-
animation: "spin 0.6s linear infinite",
|
|
371
|
-
}}
|
|
372
|
-
/>
|
|
373
|
-
Switching workstream...
|
|
374
|
-
<style>{`
|
|
375
|
-
@keyframes spin {
|
|
376
|
-
to { transform: rotate(360deg); }
|
|
377
|
-
}
|
|
378
|
-
`}</style>
|
|
379
|
-
</div>
|
|
380
|
-
)}
|
|
273
|
+
<EditorReloadOverlay reloading={reloading} />
|
|
381
274
|
<DatasourceRegistryProvider registry={editorCtx?.remoteDatasourceRegistry ?? []}>
|
|
382
275
|
<DatasourceDataProvider context={remoteDatasourceContext}>
|
|
383
276
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
384
|
-
<Puck key={`${
|
|
277
|
+
<Puck key={`${puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
385
278
|
</DatasourceDataProvider>
|
|
386
279
|
</DatasourceRegistryProvider>
|
|
387
280
|
</div>
|