Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCurrentUrl } from '../../lib/state/url/router-hooks';
import { opfsSiteStorage } from '../../lib/state/opfs/opfs-site-storage';
import {
OPFSSitesLoaded,
selectSiteBySlug,
selectSiteByUrlSlug,
setTemporarySiteSpec,
deriveSiteNameFromSlug,
} from '../../lib/state/redux/slice-sites';
Expand Down Expand Up @@ -42,13 +42,20 @@ export function EnsurePlaygroundSiteIsSelected({
const url = useCurrentUrl();
const requestedSiteSlug = url.searchParams.get('site-slug');
const requestedSiteObject = useAppSelector((state) =>
selectSiteBySlug(state, requestedSiteSlug!)
);
const requestedClientInfo = useAppSelector(
(state) =>
requestedSiteSlug &&
selectClientBySiteSlug(state, requestedSiteSlug)
requestedSiteSlug
? selectSiteByUrlSlug(state, requestedSiteSlug)
: undefined
);
const requestedClientInfo = useAppSelector((state) => {
if (!requestedSiteSlug) {
return undefined;
}
const siteMatch = selectSiteByUrlSlug(state, requestedSiteSlug);
if (!siteMatch) {
return undefined;
}
return selectClientBySiteSlug(state, siteMatch.slug);
});
const [needMissingSitePromptForSlug, setNeedMissingSitePromptForSlug] =
useState<false | string>(false);

Expand Down Expand Up @@ -108,7 +115,7 @@ export function EnsurePlaygroundSiteIsSelected({
}
}

dispatch(setActiveSite(requestedSiteSlug));
dispatch(setActiveSite(requestedSiteObject.slug));
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React, { useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { TextControl } from '@wordpress/components';
import { useAppDispatch, useAppSelector } from '../../lib/state/redux/store';
import { setActiveModal } from '../../lib/state/redux/slice-ui';
import { updateSiteMetadata } from '../../lib/state/redux/slice-sites';
import {
updateSiteMetadata,
deriveSlugFromSiteName,
type SiteInfo,
} from '../../lib/state/redux/slice-sites';
import { PlaygroundRoute, redirectTo } from '../../lib/state/url/router';
import { Modal } from '../modal';
import ModalButtons from '../modal/modal-buttons';

Expand Down Expand Up @@ -32,12 +37,20 @@ export function RenameSiteModal() {
}
try {
setIsSubmitting(true);
const newUrlSlug = deriveSlugFromSiteName(trimmed);
await dispatch(
updateSiteMetadata({
slug: site.slug,
changes: { name: trimmed },
urlSlug: newUrlSlug,
}) as any
);
const updatedSite: SiteInfo = {
...site,
urlSlug: newUrlSlug,
metadata: { ...site.metadata, name: trimmed },
};
redirectTo(PlaygroundRoute.site(updatedSite));
closeModal();
} finally {
setIsSubmitting(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const legacyOpfsPathSymbol = Symbol('legacyOpfsPath');
*/
export interface StoredSiteMetadata extends SiteMetadata {
slug: string;
urlSlug?: string;
}

let opfsSitesRoot: FileSystemDirectoryHandle | undefined = undefined;
Expand Down Expand Up @@ -77,15 +78,21 @@ class OpfsSiteStorage {
);
}

async update(slug: string, metadata: SiteMetadata): Promise<void> {
async update(
slug: string,
metadata: SiteMetadata,
urlSlug?: string
): Promise<void> {
const newSiteDirName = getDirectoryNameForSlug(slug);
if (!(await opfsChildExists(this.root, newSiteDirName))) {
throw new Error(`Site with slug '${slug}' does not exist.`);
}
const existingMetadata = await this.readRawMetadata(newSiteDirName);
const finalUrlSlug = urlSlug ?? existingMetadata?.urlSlug ?? slug;

await opfsWriteFile(
joinPaths(ROOT_PATH, newSiteDirName, SITE_METADATA_FILENAME),
await metadataToStoredFormat(slug, metadata)
await metadataToStoredFormat(slug, metadata, finalUrlSlug)
);
}

Expand Down Expand Up @@ -138,6 +145,29 @@ class OpfsSiteStorage {
const siteDirName = getDirectoryNameForSlug(slug);
await this.root.removeEntry(siteDirName, { recursive: true });
}

private async readRawMetadata(
siteDirName: string
): Promise<(StoredSiteMetadata & { urlSlug?: string }) | undefined> {
try {
const siteDirectory = await this.root.getDirectoryHandle(
siteDirName
);
const siteInfoFileHandle = await siteDirectory.getFileHandle(
SITE_METADATA_FILENAME
);
const file = await siteInfoFileHandle.getFile();
return JSON.parse(await file.text()) as StoredSiteMetadata & {
urlSlug?: string;
};
} catch (error) {
logger.error(
`Error reading raw metadata for site ${siteDirName}:`,
error
);
return undefined;
}
}
}

export const opfsSiteStorage: OpfsSiteStorage | undefined = opfsSitesRoot
Expand All @@ -156,11 +186,13 @@ export function getDirectoryNameForSlug(slug: string) {

async function metadataToStoredFormat(
slug: string,
{ originalBlueprint, ...metadata }: SiteMetadata
{ originalBlueprint, ...metadata }: SiteMetadata,
urlSlug: string = slug
): Promise<string> {
return JSON.stringify(
{
slug,
urlSlug,
originalBlueprint: await getBlueprintDeclaration(originalBlueprint),
...metadata,
},
Expand All @@ -170,7 +202,9 @@ async function metadataToStoredFormat(
}

function storedFormatToMetadata(data: string) {
const { slug, ...metadata } = JSON.parse(data) as StoredSiteMetadata;
const { slug, urlSlug, ...metadata } = JSON.parse(
data
) as StoredSiteMetadata;

/**
* Migrate the legacy runtimeConfiguration data format to the new, flat one.
Expand Down Expand Up @@ -221,6 +255,7 @@ function storedFormatToMetadata(data: string) {

return {
slug,
urlSlug: urlSlug ?? slug,
metadata,
};
}
Expand Down
40 changes: 35 additions & 5 deletions packages/playground/website/src/lib/state/redux/slice-sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { logger } from '@php-wasm/logger';
*/
export interface SiteInfo {
slug: string;
urlSlug?: string;
originalUrlParams?: {
searchParams?: Record<string, string>;
hash?: string;
Expand Down Expand Up @@ -65,12 +66,16 @@ const sitesSlice = createSlice({
action: PayloadAction<{
slug: string;
metadata: Partial<SiteMetadata>;
urlSlug?: string;
}>
) => {
const { slug, metadata } = action.payload;
const { slug, metadata, urlSlug } = action.payload;
const site = state.entities[slug];
if (site) {
site.metadata = { ...site.metadata, ...metadata };
if (urlSlug) {
site.urlSlug = urlSlug;
}
}
},

Expand All @@ -95,7 +100,10 @@ export const OPFSSitesLoaded = (sites: SiteInfo[]) => {
const currentSites = getState().sites.entities;
const allSites = { ...currentSites };
sites.forEach((site) => {
allSites[site.slug] = site;
allSites[site.slug] = {
...site,
urlSlug: site.urlSlug ?? site.slug,
};
});
dispatch(sitesSlice.actions.setSites(allSites));
dispatch(setOPFSSitesLoadingState('loaded'));
Expand Down Expand Up @@ -123,15 +131,18 @@ export function deriveSiteNameFromSlug(slug: string) {
export function updateSiteMetadata({
slug,
changes,
urlSlug,
}: {
slug: string;
changes: Partial<SiteMetadata>;
urlSlug?: string;
}) {
return async (
dispatch: PlaygroundDispatch,
getState: () => PlaygroundReduxState
) => {
const storedSite = selectSiteBySlug(getState(), slug);
const nextUrlSlug = urlSlug ?? storedSite.urlSlug ?? slug;
await dispatch(
updateSite({
slug,
Expand All @@ -140,6 +151,7 @@ export function updateSiteMetadata({
...storedSite.metadata,
...changes,
},
...(urlSlug ? { urlSlug: nextUrlSlug } : {}),
},
})
);
Expand Down Expand Up @@ -176,7 +188,8 @@ export function updateSite({
if (updatedSite.metadata.storage !== 'none') {
await opfsSiteStorage?.update(
updatedSite.slug,
updatedSite.metadata
updatedSite.metadata,
updatedSite.urlSlug ?? updatedSite.slug
);
}
};
Expand All @@ -193,13 +206,19 @@ export function addSite(siteInfo: SiteInfo) {
dispatch: PlaygroundDispatch,
getState: () => PlaygroundReduxState
) => {
const urlSlug = siteInfo.urlSlug ?? siteInfo.slug;
if (siteInfo.metadata.storage === 'none') {
throw new Error(
'Cannot add a temporary site. Use setTemporarySiteSpec instead.'
);
}
await opfsSiteStorage?.create(siteInfo.slug, siteInfo.metadata);
dispatch(sitesSlice.actions.addSite(siteInfo));
dispatch(
sitesSlice.actions.addSite({
...siteInfo,
urlSlug,
})
);
};
}

Expand Down Expand Up @@ -308,8 +327,10 @@ export function setTemporarySiteSpec(
}

// Compute the runtime configuration based on the resolved Blueprint:
const derivedSlug = deriveSlugFromSiteName(siteName);
const newSiteInfo: SiteInfo = {
slug: deriveSlugFromSiteName(siteName),
slug: derivedSlug,
urlSlug: derivedSlug,
originalUrlParams: newSiteUrlParams,
metadata: {
name: siteName,
Expand Down Expand Up @@ -391,6 +412,15 @@ export const {
(state: { sites: ReturnType<typeof sitesSlice.reducer> }) => state.sites
);

export function selectSiteByUrlSlug(
state: { sites: ReturnType<typeof sitesSlice.reducer> },
urlSlug: string
) {
return selectAllSites(state).find(
(site) => (site.urlSlug ?? site.slug) === urlSlug
);
}

export const selectSortedSites = createSelector(
[selectAllSites],
(sites: SiteInfo[]) =>
Expand Down
3 changes: 2 additions & 1 deletion packages/playground/website/src/lib/state/url/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class PlaygroundRoute {
if (site.metadata.storage === 'none') {
return updateUrl(baseUrl, site.originalUrlParams || {});
} else {
const slugForUrl = site.urlSlug ?? site.slug;
const baseParams = new URLSearchParams(baseUrl.split('?')[1]);
const preserveParamsKeys = ['mode', 'networking', 'login', 'url'];
const preserveParams: Record<string, string | null> = {};
Expand All @@ -50,7 +51,7 @@ export class PlaygroundRoute {
}
}
return updateUrl(baseUrl, {
searchParams: { 'site-slug': site.slug, ...preserveParams },
searchParams: { 'site-slug': slugForUrl, ...preserveParams },
hash: '',
});
}
Expand Down