From cddec5ed1594d7391c357878fc82445424aa27a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 5 Sep 2024 12:57:56 +0200 Subject: [PATCH 001/110] Bring in a router and ensure site-slug is always present in the URL WIP --- package-lock.json | 30 +++++ package.json | 1 + .../ensure-playground-site-slug/index.tsx | 58 ++++++++++ .../playground/website/src/lib/redux-store.ts | 23 +++- .../website/src/lib/resolve-blueprint.ts | 9 ++ .../website/src/lib/router-hooks.ts | 103 ++++++++++++++++++ .../website/src/lib/site-storage.ts | 24 +++- packages/playground/website/src/main.tsx | 11 +- 8 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 packages/playground/website/src/components/ensure-playground-site-slug/index.tsx create mode 100644 packages/playground/website/src/lib/router-hooks.ts diff --git a/package-lock.json b/package-lock.json index 71d13addeb..d0363ea2f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-transition-group": "4.4.5", "unzipper": "0.10.11", "vite-plugin-api": "1.0.4", + "wouter": "3.3.5", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", "yargs": "17.7.2" @@ -34386,6 +34387,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "devOptional": true, @@ -39475,6 +39482,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -46579,6 +46595,20 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, + "node_modules/wouter": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz", + "integrity": "sha512-bx3fLQAMn+EhYbBdY3W1gw9ZfO/uchudxYMwOIBzF3HVgqNEEIT199vEoh7FLTC0Vz5+rpMO6NdFsOkGX1QQCw==", + "license": "Unlicense", + "dependencies": { + "mitt": "^3.0.1", + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "license": "MIT", diff --git a/package.json b/package.json index cc439fb3d6..f92cd891aa 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "react-transition-group": "4.4.5", "unzipper": "0.10.11", "vite-plugin-api": "1.0.4", + "wouter": "3.3.5", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", "yargs": "17.7.2" diff --git a/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx b/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx new file mode 100644 index 0000000000..055bedda5a --- /dev/null +++ b/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react'; +import { useLocation } from 'wouter'; +import { urlContainsSiteConfiguration } from '../../lib/resolve-blueprint'; +import { listSites } from '../../lib/site-storage'; +import { useSearchParams } from '../../lib/router-hooks'; + +// Treats the URL as the source of truth. Ensures we're always at a URL +// that contains a site slug. +export function EnsurePlaygroundSiteSlug({ + children, +}: { + children: React.ReactNode; +}) { + const [query, setQuery] = useSearchParams(); + const siteSlug = query.get('site-slug'); + const [, setLocation] = useLocation(); + + useEffect(() => { + if (siteSlug) { + if (!query.get('storage') || query.get('storage') === 'temporary') { + setQuery({ storage: 'browser' }); + } + return; + } + async function load() { + if (urlContainsSiteConfiguration()) { + if ( + !query.get('storage') || + query.get('storage') === 'temporary' + ) { + setQuery({ 'site-slug': 'temporary' }); + } else { + // @TODO: Create a new permanent site + const randomSlug = Math.random() + .toString(36) + .substring(2, 15); + setQuery({ 'site-slug': randomSlug }); + } + } else { + // Boot the most recently used site + const sites = await listSites(); + if (sites.length > 0) { + setQuery({ 'site-slug': sites[0].slug }); + } else { + setQuery({ 'site-slug': 'temporary' }); + } + } + } + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [siteSlug, setLocation]); + + if (!siteSlug || (siteSlug !== 'temporary' && !query.get('storage'))) { + return null; + } + + return children; +} diff --git a/packages/playground/website/src/lib/redux-store.ts b/packages/playground/website/src/lib/redux-store.ts index 9e7114dae2..87046ac07a 100644 --- a/packages/playground/website/src/lib/redux-store.ts +++ b/packages/playground/website/src/lib/redux-store.ts @@ -9,6 +9,7 @@ import { import { directoryHandleToOpfsPath } from '@wp-playground/storage'; import type { MountDevice } from '@php-wasm/web'; import { PlaygroundClient } from '@wp-playground/client'; +import { useDispatch, useSelector } from 'react-redux'; export type ActiveModal = | 'error-report' @@ -42,6 +43,7 @@ export type SiteListing = { // Define the state types interface AppState { + activeSite?: SiteInfo; activeModal: string | null; offline: boolean; siteListing: SiteListing; @@ -94,6 +96,9 @@ const slice = createSlice({ getOpfsHandle: (state) => state.opfsMountDescriptor, }, reducers: { + setActiveSite: (state, action: PayloadAction) => { + state.activeSite = action.payload; + }, setPlaygroundClient: ( state, action: PayloadAction @@ -143,8 +148,12 @@ const slice = createSlice({ }); // Export actions -export const { setActiveModal, setOpfsMountDescriptor, setPlaygroundClient } = - slice.actions; +export const { + setActiveModal, + setOpfsMountDescriptor, + setPlaygroundClient, + setActiveSite, +} = slice.actions; // Redux thunk for adding a site export function addSite(siteInfo: SiteInfo) { @@ -235,6 +244,16 @@ listSites().then( ) ); +export function useAppSelector( + selector: (state: PlaygroundReduxState) => T +): T { + return useSelector(selector); +} + +export function useAppDispatch() { + return useDispatch(); +} + // Define RootState type export type PlaygroundReduxState = ReturnType; diff --git a/packages/playground/website/src/lib/resolve-blueprint.ts b/packages/playground/website/src/lib/resolve-blueprint.ts index 47f62e50ed..1cf9bb7b66 100644 --- a/packages/playground/website/src/lib/resolve-blueprint.ts +++ b/packages/playground/website/src/lib/resolve-blueprint.ts @@ -4,6 +4,15 @@ import { makeBlueprint } from './make-blueprint'; const query = new URL(document.location.href).searchParams; const fragment = decodeURI(document.location.hash || '#').substring(1); +export function urlContainsSiteConfiguration() { + const queryKeys = new Set(Array.from(query.keys())); + const ignoredQueryKeys = new Set(['storage']); + const differentKeys = new Set( + [...queryKeys].filter((key) => !ignoredQueryKeys.has(key)) + ); + return fragment.length > 0 || differentKeys.size > 0; +} + export async function resolveBlueprint() { let blueprint: Blueprint; /* diff --git a/packages/playground/website/src/lib/router-hooks.ts b/packages/playground/website/src/lib/router-hooks.ts new file mode 100644 index 0000000000..b10c8777dc --- /dev/null +++ b/packages/playground/website/src/lib/router-hooks.ts @@ -0,0 +1,103 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSearch, useLocation } from 'wouter'; + +export function useSearchParams() { + const search = useSearch(); + const location = useCurrentUrl(); + const [, setLocation] = useLocation(); + return [ + useMemo( + () => new URL('?' + search, window.location.href).searchParams, + [search] + ), + useCallback( + (params: Record) => { + const currentUrl = new URL(location); + Object.entries(params).forEach(([key, value]) => { + currentUrl.searchParams.set(key, value); + }); + setLocation(currentUrl.toString()); + }, + [setLocation, location] + ), + ] as const; +} + +/** + * Wouter's useLocation hook doesn't reflect the search params or the fragment. + * This hook does. + * + * The code below is copied from wouter's useLocation hook. + * @see https://github.com/molefrog/wouter + */ + +/** + * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History + */ +const eventPopstate = 'popstate'; +const eventPushState = 'pushState'; +const eventReplaceState = 'replaceState'; +const eventHashchange = 'hashchange'; +const events = [ + eventPopstate, + eventPushState, + eventReplaceState, + eventHashchange, +]; + +const subscribeToLocationUpdates = (callback: () => void) => { + for (const event of events) { + window.addEventListener(event, callback); + } + return () => { + for (const event of events) { + window.removeEventListener(event, callback); + } + }; +}; + +const patchKey = Symbol.for('wouter_v3'); + +// While History API does have `popstate` event, the only +// proper way to listen to changes via `push/replaceState` +// is to monkey-patch these methods. +// +// See https://stackoverflow.com/a/4585031 +if ( + typeof window.history !== 'undefined' && + typeof window[patchKey as any] === 'undefined' +) { + for (const type of [eventPushState, eventReplaceState]) { + const original = window.history[type as keyof History]; + // TODO: we should be using unstable_batchedUpdates to avoid multiple re-renders, + // however that will require an additional peer dependency on react-dom. + // See: https://github.com/reactwg/react-18/discussions/86#discussioncomment-1567149 + // @ts-ignore + window.history[type as keyof History] = function (...args: any[]) { + const result = original.apply(this, args); + const event = new Event(type); + // @ts-ignore + event.arguments = args; + + window.dispatchEvent(event); + return result; + }; + } + + // patch history object only once + // See: https://github.com/molefrog/wouter/issues/167 + Object.defineProperty(window, patchKey, { value: true }); +} + +export function useCurrentUrl() { + const [url, setUrl] = useState(window.location.href); + useEffect(() => { + const unsubscribe = subscribeToLocationUpdates(() => { + setUrl(window.location.href); + }); + return () => { + unsubscribe(); + }; + }, []); + return url; +} diff --git a/packages/playground/website/src/lib/site-storage.ts b/packages/playground/website/src/lib/site-storage.ts index f6eed77bb3..ad08e90683 100644 --- a/packages/playground/website/src/lib/site-storage.ts +++ b/packages/playground/website/src/lib/site-storage.ts @@ -37,6 +37,8 @@ export type SiteLogo = { */ export type PhpExtensionBundle = 'light' | 'kitchen-sink'; +export type SiteLifecycleState = 'new' | 'booting' | 'booted'; + // TODO: Create a schema for this as the design matures /** * The Site metadata that is persisted. @@ -57,6 +59,7 @@ interface SiteMetadata { //whenLastLoaded: number; originalBlueprint?: Blueprint; + siteLifecycle: SiteLifecycleState; } /** @@ -65,6 +68,7 @@ interface SiteMetadata { export interface SiteInfo extends SiteMetadata { storage: SiteStorageType; slug: string; + siteLifecycle: SiteLifecycleState; } /** @@ -78,12 +82,15 @@ export type InitialSiteInfo = Omit; * @param initialInfo The starting configuration for the site. * @returns SiteInfo The new site info structure. */ -export function createNewSiteInfo(initialInfo: InitialSiteInfo): SiteInfo { +export function createNewSiteInfo( + initialInfo: Omit +): SiteInfo { return { id: crypto.randomUUID(), slug: deriveSlugFromSiteName(initialInfo.name), whenCreated: Date.now(), ...initialInfo, + siteLifecycle: 'new', }; } @@ -180,6 +187,19 @@ export async function listSites(): Promise { return opfsSites; } +export async function getSiteInfoBySlug( + slug: string +): Promise { + const opfsRoot = await navigator.storage.getDirectory(); + const siteDirectoryName = getDirectoryNameForSlug(slug); + const siteDirectory = await opfsRoot.getDirectoryHandle(siteDirectoryName); + if (!siteDirectory) { + return undefined; + } + + return readSiteFromDirectory(siteDirectory); +} + /** * Reads information for a single site from a given directory. * @@ -266,6 +286,7 @@ function deriveDefaultSite(slug: string): SiteInfo { wpVersion: LatestMinifiedWordPressVersion, phpVersion: LatestSupportedPHPVersion, phpExtensionBundle: 'kitchen-sink', + siteLifecycle: 'new', }; } @@ -309,6 +330,7 @@ function getSiteMetadataFromSiteInfo(site: SiteInfo): SiteMetadata { wpVersion: site.wpVersion, phpVersion: site.phpVersion, phpExtensionBundle: site.phpExtensionBundle, + siteLifecycle: site.siteLifecycle, }; if (site.logo !== undefined) { diff --git a/packages/playground/website/src/main.tsx b/packages/playground/website/src/main.tsx index 63ba350958..22794d938d 100644 --- a/packages/playground/website/src/main.tsx +++ b/packages/playground/website/src/main.tsx @@ -13,6 +13,8 @@ import { useEffect, useState } from '@wordpress/element'; import store from './lib/redux-store'; import { __experimentalNavigatorProvider as NavigatorProvider } from '@wordpress/components'; import { Layout } from './components/layout'; +import { EnsurePlaygroundSiteSlug } from './components/ensure-playground-site-slug'; +import { useSearchParams } from './lib/router-hooks'; collectWindowErrors(logger); @@ -58,12 +60,13 @@ if (currentConfiguration.wp === '6.3') { } function Main() { + const [query] = useSearchParams(); const [siteSlug, setSiteSlug] = useState( query.get('site-slug') ?? undefined ); useEffect(() => { - if (siteSlug && storage !== 'browser') { + if (siteSlug && query.get('storage') !== 'browser') { alert( 'Site slugs only work with browser storage. The site slug will be ignored.' ); @@ -72,6 +75,8 @@ function Main() { const { playground, url, iframeRef } = useBootPlayground({ blueprint }); + // @TODO: Source query args from the `useSearchParams();` hook, + // not from the initial URL. return ( -
+ +
+ ); From dad4104529971f505e3112ff8fe7b0e111cab124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 5 Sep 2024 14:24:03 +0200 Subject: [PATCH 002/110] Move Playground boot to an isolated component that responds to URL params --- .../ensure-playground-site-slug/index.tsx | 58 ----------- .../ensure-playground-site-is-selected.tsx | 63 ++++++++++++ .../ensure-playground-site-slug.tsx | 78 +++++++++++++++ .../ensure-playground-site/index.tsx | 16 +++ .../website/src/components/layout/index.tsx | 32 +++--- .../playground-configuration-group/form.tsx | 20 ++-- .../components/playground-viewport/index.tsx | 4 +- .../src/components/site-manager/index.tsx | 44 ++------- .../components/site-manager/sidebar/index.tsx | 54 +++++------ .../site-manager/site-info-panel/index.tsx | 4 +- .../site-manager/storage-type/index.tsx | 2 +- .../src/components/site-view/site-view.tsx | 4 +- .../components/toolbar-buttons/reset-site.tsx | 6 +- .../website/src/lib/resolve-blueprint.ts | 58 +++++++---- .../website/src/lib/site-storage.ts | 84 +++++++++++++++- packages/playground/website/src/main.tsx | 97 +++---------------- .../website/src/playground-context.tsx | 4 +- packages/playground/website/src/types.ts | 2 - 18 files changed, 361 insertions(+), 269 deletions(-) delete mode 100644 packages/playground/website/src/components/ensure-playground-site-slug/index.tsx create mode 100644 packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx create mode 100644 packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-slug.tsx create mode 100644 packages/playground/website/src/components/ensure-playground-site/index.tsx delete mode 100644 packages/playground/website/src/types.ts diff --git a/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx b/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx deleted file mode 100644 index 055bedda5a..0000000000 --- a/packages/playground/website/src/components/ensure-playground-site-slug/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect } from 'react'; -import { useLocation } from 'wouter'; -import { urlContainsSiteConfiguration } from '../../lib/resolve-blueprint'; -import { listSites } from '../../lib/site-storage'; -import { useSearchParams } from '../../lib/router-hooks'; - -// Treats the URL as the source of truth. Ensures we're always at a URL -// that contains a site slug. -export function EnsurePlaygroundSiteSlug({ - children, -}: { - children: React.ReactNode; -}) { - const [query, setQuery] = useSearchParams(); - const siteSlug = query.get('site-slug'); - const [, setLocation] = useLocation(); - - useEffect(() => { - if (siteSlug) { - if (!query.get('storage') || query.get('storage') === 'temporary') { - setQuery({ storage: 'browser' }); - } - return; - } - async function load() { - if (urlContainsSiteConfiguration()) { - if ( - !query.get('storage') || - query.get('storage') === 'temporary' - ) { - setQuery({ 'site-slug': 'temporary' }); - } else { - // @TODO: Create a new permanent site - const randomSlug = Math.random() - .toString(36) - .substring(2, 15); - setQuery({ 'site-slug': randomSlug }); - } - } else { - // Boot the most recently used site - const sites = await listSites(); - if (sites.length > 0) { - setQuery({ 'site-slug': sites[0].slug }); - } else { - setQuery({ 'site-slug': 'temporary' }); - } - } - } - load(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [siteSlug, setLocation]); - - if (!siteSlug || (siteSlug !== 'temporary' && !query.get('storage'))) { - return null; - } - - return children; -} diff --git a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx new file mode 100644 index 0000000000..0249a2011a --- /dev/null +++ b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx @@ -0,0 +1,63 @@ +import { useEffect } from 'react'; +import { resolveBlueprint } from '../../lib/resolve-blueprint'; +import { useCurrentUrl, useSearchParams } from '../../lib/router-hooks'; +import { + addSite, + setActiveSite, + useAppDispatch, + useAppSelector, +} from '../../lib/redux-store'; +import { + createNewSiteInfo, + getSiteInfoBySlug, + SiteStorageType, +} from '../../lib/site-storage'; + +/** + * Ensures the redux store always has an activeSite value. + * It uses the URL as the source of truth and assumes the + * `site-slug` and `storage` query args are always set. + */ +export function EnsurePlaygroundSiteIsSelected({ + children, +}: { + children: React.ReactNode; +}) { + const activeSite = useAppSelector((state) => state.activeSite); + const dispatch = useAppDispatch(); + const [query] = useSearchParams(); + const urlString = useCurrentUrl(); + const siteSlug = query.get('site-slug'); + const storage = query.get('storage')! as SiteStorageType; + + useEffect(() => { + async function ensureSiteIsSelected() { + if (activeSite) { + return; + } + + if (siteSlug !== 'create') { + const siteInfo = await getSiteInfoBySlug(siteSlug!); + dispatch(setActiveSite(siteInfo!)); + return; + } + + const url = new URL(urlString); + const blueprint = await resolveBlueprint(url); + const newSiteInfo = createNewSiteInfo({ + originalBlueprint: blueprint, + storage: storage, + }); + await dispatch(addSite(newSiteInfo)); + dispatch(setActiveSite(newSiteInfo!)); + } + + ensureSiteIsSelected(); + }, [urlString, activeSite, siteSlug, dispatch]); + + if (!activeSite) { + return null; + } + + return children; +} diff --git a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-slug.tsx b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-slug.tsx new file mode 100644 index 0000000000..b0b9491200 --- /dev/null +++ b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-slug.tsx @@ -0,0 +1,78 @@ +import { useEffect } from 'react'; +import { useLocation } from 'wouter'; +import { urlContainsSiteConfiguration } from '../../lib/resolve-blueprint'; +import { listSites, SiteStorageTypes } from '../../lib/site-storage'; +import { useCurrentUrl, useSearchParams } from '../../lib/router-hooks'; + +// @ts-ignore +const opfsSupported = typeof navigator?.storage?.getDirectory !== 'undefined'; + +// Treats the URL as the source of truth. Ensures we're always at a URL +// that contains a site slug. +export function EnsurePlaygroundSiteSlug({ + children, +}: { + children: React.ReactNode; +}) { + const [query, setQuery] = useSearchParams(); + const siteSlug = query.get('site-slug'); + const [, setLocation] = useLocation(); + const urlString = useCurrentUrl(); + + // Ensure the site slug is always present in the URL. + useEffect(() => { + /* + * @TODO: Change the entire mental model of the `storage` parameter. + * For example, `storage=none` + an existing site slug makes + * no sense. We don't load where to load the site from, e.g. + * should it come from OPFS? Local directory? Network? We could + * separate the "load from" and "save" to operations, but they + * make more sense as user interactions than URL parameters. + * Perhaps we only need a single `load-site-from` URL parameter? + */ + // Ensure the optional storage query arg points to a valid storage type. + const storage = query.get('storage'); + if ( + storage && + (!opfsSupported || !SiteStorageTypes.includes(storage as any)) + ) { + setQuery({ storage: 'none' }); + return; + } + + async function ensureSiteSlug() { + // @TODO: Restrict `create` as a system slug that cannot be assigned to a user + // site. + if (siteSlug) { + if (!storage) { + setQuery({ storage: opfsSupported ? 'browser' : 'none' }); + } + return; + } + + if (urlContainsSiteConfiguration(new URL(urlString))) { + if (!storage || storage === 'none') { + setQuery({ 'site-slug': 'create', storage: 'none' }); + } else { + setQuery({ 'site-slug': 'create' }); + } + } else { + // @TODO: Sort sites by most recently used + const sites = await listSites(); + if (sites.length > 0) { + setQuery({ 'site-slug': sites[0].slug }); + } else { + setQuery({ 'site-slug': 'create' }); + } + } + } + ensureSiteSlug(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [siteSlug, setLocation]); + + if (!siteSlug || (siteSlug !== 'create' && !query.get('storage'))) { + return null; + } + + return children; +} diff --git a/packages/playground/website/src/components/ensure-playground-site/index.tsx b/packages/playground/website/src/components/ensure-playground-site/index.tsx new file mode 100644 index 0000000000..c7381873d3 --- /dev/null +++ b/packages/playground/website/src/components/ensure-playground-site/index.tsx @@ -0,0 +1,16 @@ +import { EnsurePlaygroundSiteIsSelected } from './ensure-playground-site-is-selected'; +import { EnsurePlaygroundSiteSlug } from './ensure-playground-site-slug'; + +export function EnsurePlaygroundSite({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx index 53a2c325b9..e57c328120 100644 --- a/packages/playground/website/src/components/layout/index.tsx +++ b/packages/playground/website/src/components/layout/index.tsx @@ -5,31 +5,34 @@ import { SiteManager } from '../site-manager'; import { useRef } from '@wordpress/element'; import { CSSTransition } from 'react-transition-group'; import { __experimentalUseNavigator as useNavigator } from '@wordpress/components'; -import { Blueprint, PlaygroundClient } from '@wp-playground/client'; -import { StorageType } from '../../types'; +import { PlaygroundClient } from '@wp-playground/client'; +import { useAppSelector } from '../../lib/redux-store'; import { PlaygroundConfiguration } from '../playground-configuration-group/form'; export function Layout({ playground, url, iframeRef, - blueprint, - storage, - currentConfiguration, - siteSlug, - setSiteSlug, }: { playground: PlaygroundClient; url: string; iframeRef: React.RefObject; - blueprint: Blueprint; - storage: StorageType; - currentConfiguration: PlaygroundConfiguration; - siteSlug: string | undefined; - setSiteSlug: (siteSlug?: string) => void; }) { const siteViewRef = useRef(null); + const activeSite = useAppSelector((state) => state.activeSite!); + const blueprint = activeSite.originalBlueprint || {}; + const storage = activeSite.storage; + // @TODO: Use SiteMetadata directly + const currentConfiguration: PlaygroundConfiguration = { + storage: storage ?? 'none', + wp: activeSite.wpVersion, + php: activeSite.phpVersion, + withExtensions: activeSite.phpExtensionBundle === 'kitchen-sink', + withNetworking: blueprint?.features?.networking || false, + resetSite: false, + }; + const { goTo, location: { path }, @@ -51,10 +54,7 @@ export function Layout({ unmountOnExit >
- +
diff --git a/packages/playground/website/src/components/playground-configuration-group/form.tsx b/packages/playground/website/src/components/playground-configuration-group/form.tsx index 314e4e139a..cedac6aa11 100644 --- a/packages/playground/website/src/components/playground-configuration-group/form.tsx +++ b/packages/playground/website/src/components/playground-configuration-group/form.tsx @@ -5,19 +5,19 @@ import { SupportedPHPVersion, SupportedPHPVersionsList, } from '@php-wasm/universal'; -import { StorageType } from '../../types'; import { OPFSButton } from './opfs-button'; import Button from '../button'; import { OfflineNotice } from '../offline-notice'; import { PlaygroundReduxState } from '../../lib/redux-store'; import { useSelector } from 'react-redux'; +import { SiteStorageType } from '../../lib/site-storage'; export interface PlaygroundConfiguration { wp: string; php: SupportedPHPVersion; withExtensions: boolean; withNetworking: boolean; - storage: StorageType; + storage: SiteStorageType; resetSite: boolean; } @@ -46,7 +46,9 @@ export function PlaygroundConfigurationForm({ const offline = useSelector((state: PlaygroundReduxState) => state.offline); const [php, setPhp] = useState(initialData.php); - const [storage, setStorage] = useState(initialData.storage); + const [storage, setStorage] = useState( + initialData.storage + ); const [withExtensions, setWithExtensions] = useState( initialData.withExtensions ); @@ -57,7 +59,7 @@ export function PlaygroundConfigurationForm({ const handleStorageChange = async ( event: React.ChangeEvent ) => { - setStorage(event.target.value as any as StorageType); + setStorage(event.target.value as any as SiteStorageType); }; const [resetSite, setResetSite] = useState(initialData.resetSite); @@ -137,7 +139,7 @@ export function PlaygroundConfigurationForm({ id="storage-browser" className={forms.radioInput} onChange={handleStorageChange} - checked={storage === 'browser'} + checked={storage === 'opfs'} />