diff --git a/packages/playground/website/playwright/e2e/website-ui.spec.ts b/packages/playground/website/playwright/e2e/website-ui.spec.ts
index c83d12012a..88462c3dcd 100644
--- a/packages/playground/website/playwright/e2e/website-ui.spec.ts
+++ b/packages/playground/website/playwright/e2e/website-ui.spec.ts
@@ -132,6 +132,57 @@ test('should preserve PHP constants when saving a temporary site to OPFS', async
await expect(wordpress.locator('body')).toContainText('E2E_TEST_VALUE');
});
+test('should rename a saved Playground and persist after reload', async ({
+ website,
+ browserName,
+}) => {
+ test.skip(
+ browserName === 'webkit',
+ `This test relies on OPFS which isn't available in Playwright's flavor of Safari.`
+ );
+
+ await website.goto('./');
+ await website.ensureSiteManagerIsOpen();
+
+ // Save the temporary site to OPFS so rename is available
+ await expect(website.page.getByText('Save')).toBeEnabled();
+ await website.page.getByText('Save').click();
+ await website.page.getByText('Save in this browser').waitFor();
+ await website.page.getByText('Save in this browser').click({ force: true });
+ await expect(website.page.getByLabel('Playground title')).not.toContainText(
+ 'Temporary Playground',
+ {
+ timeout: 90000,
+ }
+ );
+
+ // Open actions menu and trigger Rename
+ await website.page
+ .getByRole('button', { name: 'Additional actions' })
+ .click();
+ await website.page.getByRole('menuitem', { name: 'Rename' }).click();
+
+ const newName = 'My Renamed Playground';
+ const nameInput = website.page.getByLabel('Name');
+ await nameInput.fill('');
+ await nameInput.type(newName);
+ await nameInput.press('Enter');
+
+ await expect(website.page.getByLabel('Playground title')).toContainText(
+ newName
+ );
+
+ // Reload and verify the name persists
+ await website.page.reload();
+ await website.ensureSiteManagerIsOpen();
+ await expect(website.page.getByLabel('Playground title')).toContainText(
+ newName
+ );
+ await expect(
+ website.page.locator('[aria-current="page"]').first()
+ ).toContainText(newName);
+});
+
SupportedPHPVersions.forEach(async (version) => {
test(`should switch PHP version to ${version}`, async ({ website }) => {
await website.goto(`./`);
diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx
index 5affcceebc..21a49077c2 100644
--- a/packages/playground/website/src/components/layout/index.tsx
+++ b/packages/playground/website/src/components/layout/index.tsx
@@ -32,6 +32,7 @@ import {
import { ImportFormModal } from '../import-form-modal';
import { PreviewPRModal } from '../../github/preview-pr';
import { MissingSiteModal } from '../missing-site-modal';
+import { RenameSiteModal } from '../rename-site-modal';
acquireOAuthTokenIfNeeded();
@@ -45,6 +46,7 @@ export const modalSlugs = {
PREVIEW_PR_WP: 'preview-pr-wordpress',
PREVIEW_PR_GUTENBERG: 'preview-pr-gutenberg',
MISSING_SITE_PROMPT: 'missing-site-prompt',
+ RENAME_SITE: 'rename-site',
};
const displayMode = getDisplayModeFromQuery();
@@ -224,6 +226,8 @@ function Modals(blueprint: BlueprintDeclaration) {
);
} else if (currentModal === modalSlugs.MISSING_SITE_PROMPT) {
return ;
+ } else if (currentModal === modalSlugs.RENAME_SITE) {
+ return ;
}
if (query.get('gh-ensure-auth') === 'yes') {
diff --git a/packages/playground/website/src/components/modal/modal-buttons.tsx b/packages/playground/website/src/components/modal/modal-buttons.tsx
index c9bbb0d9de..b0870a527e 100644
--- a/packages/playground/website/src/components/modal/modal-buttons.tsx
+++ b/packages/playground/website/src/components/modal/modal-buttons.tsx
@@ -9,13 +9,17 @@ interface ModalButtonsProps {
onCancel?: () => void;
onSubmit?: (e: any) => void;
}
-export default function ModalButtons({ submitText = 'Submit', areDisabled = false, areBusy, onCancel, onSubmit }: ModalButtonsProps) {
+export default function ModalButtons({
+ submitText = 'Submit',
+ areDisabled = false,
+ areBusy,
+ onCancel,
+ onSubmit,
+}: ModalButtonsProps) {
return (
-
+
- )
+ );
}
diff --git a/packages/playground/website/src/components/rename-site-modal/index.tsx b/packages/playground/website/src/components/rename-site-modal/index.tsx
new file mode 100644
index 0000000000..ed6784a533
--- /dev/null
+++ b/packages/playground/website/src/components/rename-site-modal/index.tsx
@@ -0,0 +1,77 @@
+import React, { 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 { Modal } from '../modal';
+import ModalButtons from '../modal/modal-buttons';
+
+export function RenameSiteModal() {
+ const dispatch = useAppDispatch();
+ const site = useAppSelector((state) =>
+ state.ui.activeSite?.slug
+ ? state.sites.entities[state.ui.activeSite.slug]
+ : undefined
+ );
+
+ const initialName = useMemo(() => site?.metadata?.name ?? '', [site]);
+ const [name, setName] = useState(initialName);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ if (!site) {
+ // Nothing to rename
+ return null;
+ }
+
+ const closeModal = () => dispatch(setActiveModal(null));
+
+ const handleSubmit = async () => {
+ const trimmed = name.trim();
+ if (!trimmed) {
+ return;
+ }
+ try {
+ setIsSubmitting(true);
+ await dispatch(
+ updateSiteMetadata({
+ slug: site.slug,
+ changes: { name: trimmed },
+ }) as any
+ );
+ closeModal();
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/playground/website/src/components/site-manager/site-info-panel/index.tsx b/packages/playground/website/src/components/site-manager/site-info-panel/index.tsx
index 72a3fccb60..d930337970 100644
--- a/packages/playground/website/src/components/site-manager/site-info-panel/index.tsx
+++ b/packages/playground/website/src/components/site-manager/site-info-panel/index.tsx
@@ -29,6 +29,8 @@ import { selectClientInfoBySiteSlug } from '../../../lib/state/redux/slice-clien
import { encodeStringAsBase64 } from '../../../lib/base64';
import { ActiveSiteSettingsForm } from '../site-settings-form/active-site-settings-form';
import { getRelativeDate } from '../../../lib/get-relative-date';
+import { setActiveModal } from '../../../lib/state/redux/slice-ui';
+import { modalSlugs } from '../../layout';
import { removeSite } from '../../../lib/state/redux/slice-sites';
export function SiteInfoPanel({
@@ -222,6 +224,19 @@ export function SiteInfoPanel({
<>
{!isTemporary && (
+