Skip to content

Commit 6a59a02

Browse files
authored
[Website] Renaming stored Playgrounds (#2486)
## Motivation for the change, related issues A limited version of #2124 that only enables renaming stored Playgrounds. It does not enforce choosing a name when the Playground is first saved – it reduces the size of the PR and makes testing and reviewing easier (the original PR is larger and breaks the E2E tests). ## Testing Instructions (or ideally a Blueprint) This PR ships an E2E test. For manual testing: - Checkout this branch - Start the dev server npm run dev - Go to the Playground manager - Click save on a temporary site - Click on the kebab menu of that site and click Rename - Rename the site and confirm the changes are saved - Refresh the page and confirm the new name is still there
1 parent 56fb7c2 commit 6a59a02

File tree

5 files changed

+164
-6
lines changed

5 files changed

+164
-6
lines changed

packages/playground/website/playwright/e2e/website-ui.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,63 @@ test('should preserve PHP constants when saving a temporary site to OPFS', async
132132
await expect(wordpress.locator('body')).toContainText('E2E_TEST_VALUE');
133133
});
134134

135+
test('should rename a saved Playground and persist after reload', async ({
136+
website,
137+
browserName,
138+
}) => {
139+
test.skip(
140+
browserName === 'webkit',
141+
`This test relies on OPFS which isn't available in Playwright's flavor of Safari.`
142+
);
143+
144+
await website.goto('./');
145+
await website.ensureSiteManagerIsOpen();
146+
147+
// Save the temporary site to OPFS so rename is available
148+
await expect(website.page.getByText('Save')).toBeEnabled();
149+
await website.page.getByText('Save').click();
150+
await website.page.getByText('Save in this browser').waitFor();
151+
await website.page.getByText('Save in this browser').click({ force: true });
152+
await expect(website.page.getByLabel('Playground title')).not.toContainText(
153+
'Temporary Playground',
154+
{
155+
timeout: 90000,
156+
}
157+
);
158+
159+
// Open actions menu and trigger Rename
160+
await website.page
161+
.getByRole('button', { name: 'Additional actions' })
162+
.click();
163+
await website.page.getByRole('menuitem', { name: 'Rename' }).click();
164+
165+
const newName = 'My Renamed Playground';
166+
const dialog = website.page.getByRole('dialog', {
167+
name: 'Rename Playground',
168+
});
169+
const nameInput = dialog.getByRole('textbox', { name: 'Name' });
170+
await nameInput.fill('');
171+
await nameInput.type(newName);
172+
await nameInput.press('Enter');
173+
174+
await expect(website.page.getByLabel('Playground title')).toContainText(
175+
newName
176+
);
177+
178+
// Wait for the dialog to be closed
179+
await expect(dialog).not.toBeVisible();
180+
181+
// Reload and verify the name persists
182+
await website.page.reload();
183+
await website.ensureSiteManagerIsOpen();
184+
await expect(website.page.getByLabel('Playground title')).toContainText(
185+
newName
186+
);
187+
await expect(
188+
website.page.locator('[aria-current="page"]').first()
189+
).toContainText(newName);
190+
});
191+
135192
SupportedPHPVersions.forEach(async (version) => {
136193
test(`should switch PHP version to ${version}`, async ({ website }) => {
137194
await website.goto(`./`);

packages/playground/website/src/components/layout/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import { ImportFormModal } from '../import-form-modal';
3333
import { PreviewPRModal } from '../../github/preview-pr';
3434
import { MissingSiteModal } from '../missing-site-modal';
35+
import { RenameSiteModal } from '../rename-site-modal';
3536

3637
acquireOAuthTokenIfNeeded();
3738

@@ -45,6 +46,7 @@ export const modalSlugs = {
4546
PREVIEW_PR_WP: 'preview-pr-wordpress',
4647
PREVIEW_PR_GUTENBERG: 'preview-pr-gutenberg',
4748
MISSING_SITE_PROMPT: 'missing-site-prompt',
49+
RENAME_SITE: 'rename-site',
4850
};
4951

5052
const displayMode = getDisplayModeFromQuery();
@@ -224,6 +226,8 @@ function Modals(blueprint: BlueprintDeclaration) {
224226
);
225227
} else if (currentModal === modalSlugs.MISSING_SITE_PROMPT) {
226228
return <MissingSiteModal />;
229+
} else if (currentModal === modalSlugs.RENAME_SITE) {
230+
return <RenameSiteModal />;
227231
}
228232

229233
if (query.get('gh-ensure-auth') === 'yes') {

packages/playground/website/src/components/modal/modal-buttons.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ interface ModalButtonsProps {
99
onCancel?: () => void;
1010
onSubmit?: (e: any) => void;
1111
}
12-
export default function ModalButtons({ submitText = 'Submit', areDisabled = false, areBusy, onCancel, onSubmit }: ModalButtonsProps) {
12+
export default function ModalButtons({
13+
submitText = 'Submit',
14+
areDisabled = false,
15+
areBusy,
16+
onCancel,
17+
onSubmit,
18+
}: ModalButtonsProps) {
1319
return (
14-
<Flex
15-
justify="end"
16-
className={css.modalButtons}
17-
>
20+
<Flex justify="end" className={css.modalButtons}>
1821
<Button
22+
type="button"
1923
isBusy={areBusy}
2024
disabled={areDisabled}
2125
variant="link"
@@ -24,6 +28,7 @@ export default function ModalButtons({ submitText = 'Submit', areDisabled = fals
2428
Cancel
2529
</Button>
2630
<Button
31+
type="submit"
2732
isBusy={areBusy}
2833
disabled={areDisabled}
2934
variant="primary"
@@ -32,5 +37,5 @@ export default function ModalButtons({ submitText = 'Submit', areDisabled = fals
3237
{submitText}
3338
</Button>
3439
</Flex>
35-
)
40+
);
3641
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { TextControl } from '@wordpress/components';
3+
import { useAppDispatch, useAppSelector } from '../../lib/state/redux/store';
4+
import { setActiveModal } from '../../lib/state/redux/slice-ui';
5+
import { updateSiteMetadata } from '../../lib/state/redux/slice-sites';
6+
import { Modal } from '../modal';
7+
import ModalButtons from '../modal/modal-buttons';
8+
9+
export function RenameSiteModal() {
10+
const dispatch = useAppDispatch();
11+
const site = useAppSelector((state) =>
12+
state.ui.activeSite?.slug
13+
? state.sites.entities[state.ui.activeSite.slug]
14+
: undefined
15+
);
16+
17+
const initialName = useMemo(() => site?.metadata?.name ?? '', [site]);
18+
const [name, setName] = useState<string>(initialName);
19+
const [isSubmitting, setIsSubmitting] = useState(false);
20+
21+
if (!site) {
22+
// Nothing to rename
23+
return null;
24+
}
25+
26+
const closeModal = () => dispatch(setActiveModal(null));
27+
28+
const handleSubmit = async () => {
29+
const trimmed = name.trim();
30+
if (!trimmed) {
31+
return;
32+
}
33+
try {
34+
setIsSubmitting(true);
35+
await dispatch(
36+
updateSiteMetadata({
37+
slug: site.slug,
38+
changes: { name: trimmed },
39+
}) as any
40+
);
41+
closeModal();
42+
} finally {
43+
setIsSubmitting(false);
44+
}
45+
};
46+
47+
return (
48+
<Modal
49+
title="Rename Playground"
50+
contentLabel='This is a dialog window which overlays the main content of the page. The modal begins with a heading 2 called "Rename Playground". Pressing the Close button will close the modal and bring you back to where you were on the page.'
51+
onRequestClose={closeModal}
52+
small
53+
>
54+
<form
55+
onSubmit={(e) => {
56+
e.preventDefault();
57+
handleSubmit();
58+
}}
59+
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
60+
>
61+
<TextControl
62+
__nextHasNoMarginBottom
63+
label="Name"
64+
value={name}
65+
onChange={(val: string) => setName(val)}
66+
autoFocus
67+
/>
68+
<ModalButtons
69+
submitText="Rename"
70+
areDisabled={!name.trim()}
71+
areBusy={isSubmitting}
72+
onCancel={closeModal}
73+
/>
74+
</form>
75+
</Modal>
76+
);
77+
}

packages/playground/website/src/components/site-manager/site-info-panel/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { selectClientInfoBySiteSlug } from '../../../lib/state/redux/slice-clien
2929
import { encodeStringAsBase64 } from '../../../lib/base64';
3030
import { ActiveSiteSettingsForm } from '../site-settings-form/active-site-settings-form';
3131
import { getRelativeDate } from '../../../lib/get-relative-date';
32+
import { setActiveModal } from '../../../lib/state/redux/slice-ui';
33+
import { modalSlugs } from '../../layout';
3234
import { removeSite } from '../../../lib/state/redux/slice-sites';
3335

3436
export function SiteInfoPanel({
@@ -222,6 +224,19 @@ export function SiteInfoPanel({
222224
<>
223225
{!isTemporary && (
224226
<MenuGroup>
227+
<MenuItem
228+
aria-label="Rename this Playground"
229+
onClick={() => {
230+
dispatch(
231+
setActiveModal(
232+
modalSlugs.RENAME_SITE
233+
)
234+
);
235+
onClose();
236+
}}
237+
>
238+
Rename
239+
</MenuItem>
225240
<MenuItem
226241
aria-label="Delete this Playground"
227242
className={css.danger}

0 commit comments

Comments
 (0)