Skip to content

Commit aaa764a

Browse files
authored
Website: Blueprints gallery integration for single-click Playground presets (#1759)
## Motivation for the change, related issues Adds the Blueprint Gallery to the Playground UI as a first-class feature. ![CleanShot 2024-10-10 at 13 40 17@2x](https://github.com/user-attachments/assets/4e30e0e6-da9b-4dfa-9b21-6d15b7c0e44b) ## Implementation ### Sourcing the list of Blueprints GitHub automation maintains a JSON index of all the Blueprints in the directory at https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json. This PR just sources that data and adds a DataView where it can be browsed. It ships a new `useFetch()` React hook that makes the state management around `fetch()` easier. Maybe this simplistic hook will be enough, or maybe we'll quickly replace it with `react-query` or so. We'll see. ### Fetch proxy for offline caching This PR ships a "proxy" service worker route for offline caching of cross-origin requests. Playground is now an offline-enabled PWA and anytime we rely on network resources, we must consider what happens when there's network connectivity. For same-origin request, that's easy. We can just cache them in the `fetch` event handler in the service worker. However, that event handler won't handle cross-origin requests. Therefore, this PR ships a special same-origin `/proxy/network-first-fetch/` route that can cache cross-origin requests in `CacheStorage`. For example, the following request would fetch the list of all the Blueprints from the Blueprints directory: ``` https://playground.wordpress.net/proxy/network-first-fetch/https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json ``` The service worker would be notified, then it would strip the `https://playground.wordpress.net/proxy/network-first-fetch/` prefix, and source the actual information from `https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json`. If the network is available, it will always request the fresh data. If the network is not available, it would source the data from `CacheStorage` if available. ## Testing Instructions Interact with the gallery, confirm it works in the offline mode, confirm the blueprints load without issues and that the rest of the UI works the same as before this PR. ## Follow-up work - [ ] Revisit the design based on user feedback and @jarekmorawski's explorations in [Figma](https://www.figma.com/design/VmNXsvW4mKyKQecQUIUZm4/%5BProject%5D-Playground-2.0?node-id=0-1) - [ ] Integrate with the Blueprints builder when it's ready - [ ] Add GitHub connection to make contributing Blueprints easy - [ ] Integrate with the upcoming visual Blueprints builder cc @brandonpayton @bgrgicak @mtias
1 parent 49d0c43 commit aaa764a

File tree

12 files changed

+1171
-304
lines changed

12 files changed

+1171
-304
lines changed

package-lock.json

Lines changed: 470 additions & 230 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@types/ini": "4.1.0",
7272
"@types/react-transition-group": "4.4.11",
7373
"@types/wicg-file-system-access": "2023.10.5",
74+
"@wordpress/dataviews": "4.5.0",
7475
"ajv": "8.12.0",
7576
"async-lock": "1.4.1",
7677
"axios": "1.6.1",

packages/playground/components/src/icons.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export const file = (
108108
export const ClockIcon = (props?: React.SVGProps<SVGSVGElement>) => (
109109
<svg
110110
xmlns="http://www.w3.org/2000/svg"
111-
width="14"
112-
height="14"
111+
width="18"
112+
height="18"
113113
viewBox="0 0 14 14"
114114
fill="none"
115115
{...props}

packages/playground/remote/service-worker.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,42 @@ self.addEventListener('fetch', (event) => {
229229
);
230230
}
231231

232+
/**
233+
* A proxy that enables offline caching of cross-origin requests.
234+
*
235+
* For example, the following request fetching the list of all the Blueprints
236+
* from the Blueprints directory:
237+
*
238+
* https://playground.wordpress.net/proxy/network-first-fetch/https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json
239+
*
240+
* would be proxied to:
241+
*
242+
* https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json
243+
*
244+
* And the response would be cached for when Playground is running in the
245+
* offline mode.
246+
*/
247+
if (url.pathname.startsWith('/proxy/')) {
248+
const segments = url.pathname.split('/');
249+
const command = segments[2];
250+
switch (command) {
251+
case 'network-first-fetch': {
252+
const proxiedUrl =
253+
url.pathname.substring(
254+
'/proxy/'.length + command.length + 1
255+
) +
256+
(url?.search ? '?' + url.search : '') +
257+
(url?.hash ? '#' + url.hash : '');
258+
const requestWithTargetUrl = cloneRequest(event.request, {
259+
url: proxiedUrl,
260+
});
261+
return event.respondWith(
262+
requestWithTargetUrl.then(networkFirstFetch)
263+
);
264+
}
265+
}
266+
}
267+
232268
if (!shouldCacheUrl(new URL(event.request.url))) {
233269
/**
234270
* It's safe to use the regular `fetch` function here.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import css from './style.module.css';
2+
import {
3+
Button,
4+
Flex,
5+
FlexItem,
6+
Spinner,
7+
Icon,
8+
__experimentalText as Text,
9+
__experimentalVStack as VStack,
10+
__experimentalHStack as HStack,
11+
} from '@wordpress/components';
12+
import { chevronLeft } from '@wordpress/icons';
13+
import { DataViews } from '@wordpress/dataviews';
14+
import type { Field, View } from '@wordpress/dataviews';
15+
import classNames from 'classnames';
16+
import { useState } from 'react';
17+
import { PlaygroundRoute, redirectTo } from '../../../lib/state/url/router';
18+
import { joinPaths } from '@php-wasm/util';
19+
import useFetch from '../../../lib/hooks/use-fetch';
20+
import { useAppDispatch } from '../../../lib/state/redux/store';
21+
import { setSiteManagerSection } from '../../../lib/state/redux/slice-ui';
22+
23+
type BlueprintsIndexEntry = {
24+
title: string;
25+
description: string;
26+
author: string;
27+
categories: string[];
28+
path: string;
29+
};
30+
31+
export function BlueprintsPanel({
32+
className,
33+
mobileUi,
34+
}: {
35+
className: string;
36+
mobileUi?: boolean;
37+
}) {
38+
// @TODO: memoize across component loads.
39+
const { data, isLoading, isError } = useFetch<
40+
Record<string, BlueprintsIndexEntry>
41+
>(
42+
'/proxy/network-first-fetch/https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json'
43+
);
44+
45+
const [view, setView] = useState<View>({
46+
type: 'list',
47+
fields: ['header', 'description'],
48+
});
49+
50+
const dispatch = useAppDispatch();
51+
52+
let indexEntries: BlueprintsIndexEntry[] = data
53+
? Object.entries(data).map(([path, entry]) => ({ ...entry, path }))
54+
: [];
55+
56+
if (view.search) {
57+
indexEntries = indexEntries.filter((entry) => {
58+
return [entry.title, entry.description]
59+
.join(' ')
60+
.toLocaleLowerCase()
61+
.includes(view.search!.toLocaleLowerCase());
62+
});
63+
}
64+
65+
function previewBlueprint(blueprintPath: BlueprintsIndexEntry['path']) {
66+
redirectTo(
67+
PlaygroundRoute.newTemporarySite({
68+
query: {
69+
name: 'Blueprint preview',
70+
'blueprint-url': joinPaths(
71+
'https://raw.githubusercontent.com/WordPress/blueprints/trunk/',
72+
blueprintPath
73+
),
74+
},
75+
})
76+
);
77+
}
78+
79+
const fields: Field<BlueprintsIndexEntry>[] = [
80+
{
81+
id: 'header',
82+
label: 'Header',
83+
enableHiding: false,
84+
render: ({ item }) => {
85+
return (
86+
<HStack spacing={2} justify="space-between">
87+
<VStack spacing={0} style={{ flexGrow: 1 }}>
88+
<h3 className={css.blueprintTitle}>{item.title}</h3>
89+
<Text>
90+
By{' '}
91+
<a
92+
target="_blank"
93+
rel="noreferrer"
94+
href={`https://github.com/${item.author}`}
95+
>
96+
{item.author}
97+
</a>
98+
</Text>
99+
</VStack>
100+
<Button style={{ flexShrink: 0 }} variant="primary">
101+
Preview
102+
</Button>
103+
</HStack>
104+
);
105+
},
106+
},
107+
{
108+
id: 'description',
109+
label: 'Description',
110+
render: ({ item }) => {
111+
return <Text>{item.description}</Text>;
112+
},
113+
},
114+
];
115+
116+
return (
117+
<section
118+
className={classNames(className, css.blueprintsPanel, {
119+
[css.isMobile]: mobileUi,
120+
})}
121+
>
122+
<Flex
123+
gap={0}
124+
direction="column"
125+
justify="flex-start"
126+
expanded={true}
127+
>
128+
<FlexItem style={{ flexShrink: 0, paddingBottom: 24 }}>
129+
<FlexItem style={{ flexShrink: 0 }}>
130+
<Flex
131+
direction="row"
132+
gap={2}
133+
justify="space-between"
134+
align="center"
135+
expanded={true}
136+
className={css.padded}
137+
style={{ paddingBottom: 10 }}
138+
>
139+
{mobileUi && (
140+
<FlexItem>
141+
<Button
142+
variant="link"
143+
label="Back to sites list"
144+
icon={() => (
145+
<Icon
146+
icon={chevronLeft}
147+
size={38}
148+
/>
149+
)}
150+
className={css.grayLinkDark}
151+
onClick={() => {
152+
dispatch(
153+
setSiteManagerSection('sidebar')
154+
);
155+
}}
156+
/>
157+
</FlexItem>
158+
)}
159+
<FlexItem style={{ flexGrow: 1 }}>
160+
<Flex
161+
direction="column"
162+
gap={0.25}
163+
expanded={true}
164+
>
165+
<h2 className={css.sectionTitle}>
166+
Blueprints Gallery
167+
</h2>
168+
</Flex>
169+
</FlexItem>
170+
</Flex>
171+
</FlexItem>
172+
<FlexItem className={css.paddedH}>
173+
<p>
174+
Blueprints are predefined configurations for setting
175+
up WordPress. Here you can find all the Blueprints
176+
from the WordPress{' '}
177+
<a
178+
href="https://github.com/WordPress/blueprints"
179+
target="_blank"
180+
rel="noreferrer"
181+
>
182+
Blueprints gallery
183+
</a>
184+
. Try them out in Playground and learn more in the{' '}
185+
<a
186+
href="https://wordpress.github.io/wordpress-playground/blueprints"
187+
target="_blank"
188+
rel="noreferrer"
189+
>
190+
Blueprints documentation
191+
</a>
192+
.
193+
</p>
194+
</FlexItem>
195+
</FlexItem>
196+
<FlexItem style={{ alignSelf: 'stretch', overflowY: 'scroll' }}>
197+
<div style={{ paddingTop: 0 }}>
198+
{isLoading ? (
199+
<Spinner />
200+
) : isError ? (
201+
<p>
202+
Could not load the Blueprints from the gallery.
203+
Try again later.
204+
</p>
205+
) : (
206+
<DataViews<BlueprintsIndexEntry>
207+
data={indexEntries as BlueprintsIndexEntry[]}
208+
view={view}
209+
onChangeView={setView}
210+
onChangeSelection={(newSelection) => {
211+
if (newSelection?.length) {
212+
previewBlueprint(newSelection[0]);
213+
}
214+
}}
215+
search={true}
216+
isLoading={isLoading}
217+
fields={fields}
218+
header={null}
219+
getItemId={(item) => item?.path}
220+
paginationInfo={{
221+
totalItems: indexEntries.length,
222+
totalPages: 1,
223+
}}
224+
defaultLayouts={{
225+
list: {},
226+
}}
227+
/>
228+
)}
229+
</div>
230+
</FlexItem>
231+
</Flex>
232+
</section>
233+
);
234+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* Site details */
2+
3+
.blueprints-panel {
4+
--padding-size: 24px;
5+
6+
background: #ffffff;
7+
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.08);
8+
border-radius: var(--site-manager-border-radius);
9+
overflow: hidden;
10+
flex-grow: 1;
11+
12+
&.is-mobile {
13+
:global(.dataviews-view-list .dataviews-view-list__item) {
14+
padding-left: 0;
15+
padding-right: 0;
16+
}
17+
}
18+
19+
&:not(.is-mobile) {
20+
width: 500px;
21+
}
22+
23+
/* Hide the Blueprint thumbnail. It doesn't seem possible with the component props. */
24+
:global(.dataviews__view-actions) {
25+
padding-top: 0;
26+
padding-left: var(--padding-size);
27+
padding-right: var(--padding-size);
28+
}
29+
:global(.dataviews__view-actions > :not(.dataviews__search)) {
30+
display: none;
31+
}
32+
33+
:global(.dataviews-view-list__primary-field),
34+
:global(.dataviews-view-list__media-wrapper) {
35+
display: none;
36+
}
37+
:global(.dataviews-view-list__fields) {
38+
flex-direction: column;
39+
gap: 12px;
40+
}
41+
:global(.dataviews-view-list__field-wrapper) {
42+
width: 100%;
43+
}
44+
}
45+
46+
.padded {
47+
padding: var(--padding-size);
48+
}
49+
50+
.padded-h {
51+
padding: 0 var(--padding-size);
52+
}
53+
54+
.section-title {
55+
/* Provide enough space for most autogenerated site names to avoid
56+
flickering site manager panel width when switching between
57+
sites having names of different lengths */
58+
width: 20ch;
59+
height: 24px;
60+
margin: 0;
61+
62+
/* Inside auto layout */
63+
flex: none;
64+
order: 0;
65+
align-self: stretch;
66+
flex-grow: 0;
67+
68+
font-size: 18px;
69+
line-height: 24px;
70+
font-weight: 500;
71+
/* identical to box height, or 150% */
72+
letter-spacing: -0.01em;
73+
74+
/* Gutenberg/Gray 900 */
75+
color: var(--color-gray-900);
76+
}
77+
78+
.blueprint-title {
79+
display: block;
80+
width: 100%;
81+
margin: 0;
82+
}

0 commit comments

Comments
 (0)