Skip to content

Commit 707a733

Browse files
authored
[Website] Restore the single-click "Edit Settings" flow (#1854)
## Motivation for the change, related issues Restores the easily accessible "Edit settings" button Playground had before merging #1731, and refocuses the user experience on a single, temporary Playground. Multiple Playgrounds are still possible, but now they're less emphasized. As we've learned from @annezazu and other users, the recent [Multiple Playgrounds UI update](#1731) made rapid fire iterations in Playground more difficult. Before #1731, we've had an easily accessible button to update WP and PHP versions. After #1731, that feature required multiple clicks and finding the right button. This PR introduces the following changes: * Add an easily-accessible "Site settings" button for quick PHP/WP version updates * The URL reflects the Query API values used to create the temporary Playground. * Limit the number of temporary Playground sites to exactly one – just like before #1731. The temporary Playground is always there and cannot be deleted. * The only way to create a stored Playground is by saving the temporary Playground. Once you do that, you get a fresh temporary Playground. Kudos to @jarekmorawski for thinking through and designing multiple variations of the user flows ❤️ ## Technical details The implementation is straightforward and focused on rearranging React components and CSS. There's one gotcha in the process of saving temporary site settings. The settings form submission calls `window.history.pushState()` and the `EnsurePlaygroundIsSelected` component watches for the URL changes. However, the user may click the "Update Settings & Reset Playground" button even without changing any form value. Normally, this would mean we can't detect a change and reset the Playground. This PR, thus, adds the `?random=<random string>` parameter to the query string to allow Playground notice the change and recreate the temporary site. ## Visuals ![CleanShot 2024-10-06 at 23 19 12@2x](https://github.com/user-attachments/assets/11e69587-a6ca-4f73-8d51-f15997950e71) ![CleanShot 2024-10-07 at 01 35 12@2x](https://github.com/user-attachments/assets/0e13f94a-adef-4f5a-8fc3-f3f4b9a577c6) Here's the video walkthrough – note I've recorded it before this PR was fully ready for a review: https://github.com/user-attachments/assets/b2f04fa6-d7d5-43ad-93e2-975a2a9cea21 ## Follow up work There are more design elements to consider, e.g. Snackbar notices. @jarekmorawski already designed some improvements and is working more. I would still like to get this PR in and continue iterating based on the feedback we get. ## UI updates checklist - [x] Tested mobile interactions - [x] Resolved accessibility issues reported by Axe Devtools ## Testing plan CI aside, interact with the design proposed in this PR and confirm it feels right.
1 parent cf2f48d commit 707a733

31 files changed

+1338
-1542
lines changed

packages/playground/website/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
</script>
4444
</head>
4545
<body>
46-
<main id="root">
46+
<main id="root" aria-label="WordPress Playground">
4747
<script>
4848
const query = new URLSearchParams(window.location.search);
4949
const shouldLazyLoadPlayground =

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

+92-66
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test('should reflect the URL update from the navigation bar in the WordPress sit
1313
website,
1414
}) => {
1515
await website.goto('./?url=/wp-admin/');
16-
await website.ensureSiteViewIsExpanded();
16+
await website.ensureSiteManagerIsClosed();
1717
await expect(website.page.locator('input[value="/wp-admin/"]')).toHaveValue(
1818
'/wp-admin/'
1919
);
@@ -23,74 +23,98 @@ test('should correctly load /wp-admin without the trailing slash', async ({
2323
website,
2424
}) => {
2525
await website.goto('./?url=/wp-admin');
26-
await website.ensureSiteViewIsExpanded();
26+
await website.ensureSiteManagerIsClosed();
2727
await expect(website.page.locator('input[value="/wp-admin/"]')).toHaveValue(
2828
'/wp-admin/'
2929
);
3030
});
3131

32-
test('should switch between sites', async ({ website }) => {
32+
test('should switch between sites', async ({ website, browserName }) => {
33+
test.skip(
34+
browserName === 'webkit',
35+
`This test relies on OPFS which isn't available in Playwright's flavor of Safari.`
36+
);
37+
3338
await website.goto('./');
3439

3540
await website.ensureSiteManagerIsOpen();
36-
await website.openNewSiteModal();
37-
38-
const newSiteName = await website.page
39-
.locator('input[placeholder="Playground name"]')
40-
.inputValue();
4141

42-
await website.clickCreateInNewSiteModal();
42+
await expect(website.page.getByText('Save')).toBeEnabled();
43+
await website.page.getByText('Save').click();
44+
// We shouldn't need to explicitly call .waitFor(), but the test fails without it.
45+
// Playwright logs that something "intercepts pointer events", that's probably related.
46+
await website.page.getByText('Save in this browser').waitFor();
47+
await website.page.getByText('Save in this browser').click({ force: true });
48+
await expect(
49+
website.page.locator('[aria-current="page"]')
50+
).not.toContainText('Temporary Playground', {
51+
// Saving the site takes a while on CI
52+
timeout: 90000,
53+
});
54+
await expect(website.page.getByLabel('Playground title')).not.toContainText(
55+
'Temporary Playground'
56+
);
4357

44-
await expect(await website.getSiteTitle()).toMatch(newSiteName);
58+
await website.page
59+
.locator('button')
60+
.filter({ hasText: 'Temporary Playground' })
61+
.click();
4562

46-
const sidebarItem = website.page.locator(
47-
'.components-button[class*="_sidebar-item"]:not([class*="_sidebar-item--selected_"])'
63+
await expect(website.page.locator('[aria-current="page"]')).toContainText(
64+
'Temporary Playground'
65+
);
66+
await expect(website.page.getByLabel('Playground title')).toContainText(
67+
'Temporary Playground'
4868
);
49-
const siteName = await sidebarItem
50-
.locator('.components-flex-item[class*="_sidebar-item--site-name"]')
51-
.innerText();
52-
await sidebarItem.click();
53-
54-
await expect(siteName).toMatch(await website.getSiteTitle());
5569
});
5670

5771
SupportedPHPVersions.forEach(async (version) => {
72+
/**
73+
* WordPress 6.6 dropped support for PHP 7.0 and 7.1 and won't load on these versions.
74+
* Therefore, we're skipping the test for these versions.
75+
* @see https://make.wordpress.org/core/2024/04/08/dropping-support-for-php-7-1/
76+
*/
77+
if (['7.0', '7.1'].includes(version)) {
78+
return;
79+
}
80+
5881
test(`should switch PHP version to ${version}`, async ({ website }) => {
59-
/**
60-
* WordPress 6.6 dropped support for PHP 7.0 and 7.1 so we need to skip these versions.
61-
* @see https://make.wordpress.org/core/2024/04/08/dropping-support-for-php-7-1/
62-
*/
63-
if (['7.0', '7.1'].includes(version)) {
64-
return;
65-
}
6682
await website.goto(`./`);
67-
await website.openForkPlaygroundSettings();
68-
await website.selectPHPVersion(version);
69-
await website.clickSaveInForkPlaygroundSettings();
70-
71-
await expect(website.getSiteInfoRowLocator('PHP version')).toHaveText(
72-
`${version} (with extensions)`
83+
await website.ensureSiteManagerIsOpen();
84+
await website.page.getByLabel('PHP version').selectOption(version);
85+
await website.page
86+
.getByText('Apply Settings & Reset Playground')
87+
.click();
88+
await website.ensureSiteManagerIsClosed();
89+
await website.ensureSiteManagerIsOpen();
90+
91+
await expect(website.page.getByLabel('PHP version')).toHaveValue(
92+
version
7393
);
94+
await expect(
95+
website.page.getByLabel('Load PHP extensions')
96+
).toBeChecked();
7497
});
7598

7699
test(`should not load additional PHP ${version} extensions when not requested`, async ({
77100
website,
78101
}) => {
79102
await website.goto('./');
80-
await website.openForkPlaygroundSettings();
81-
await website.selectPHPVersion(version);
82-
83-
// Uncheck the "with extensions" checkbox
84-
const phpExtensionCheckbox = website.page.locator(
85-
'.components-checkbox-control__input[name="withExtensions"]'
86-
);
87-
await phpExtensionCheckbox.uncheck();
88-
89-
await website.clickSaveInForkPlaygroundSettings();
90-
91-
await expect(website.getSiteInfoRowLocator('PHP version')).toHaveText(
103+
await website.ensureSiteManagerIsOpen();
104+
await website.page.getByLabel('PHP version').selectOption(version);
105+
await website.page.getByLabel('Load PHP extensions').uncheck();
106+
await website.page
107+
.getByText('Apply Settings & Reset Playground')
108+
.click();
109+
await website.ensureSiteManagerIsClosed();
110+
await website.ensureSiteManagerIsOpen();
111+
112+
await expect(website.page.getByLabel('PHP version')).toHaveValue(
92113
version
93114
);
115+
await expect(
116+
website.page.getByLabel('Load PHP extensions')
117+
).not.toBeChecked();
94118
});
95119
});
96120

@@ -102,13 +126,19 @@ Object.keys(MinifiedWordPressVersions)
102126
website,
103127
}) => {
104128
await website.goto('./');
105-
await website.openForkPlaygroundSettings();
106-
await website.selectWordPressVersion(version);
107-
await website.clickSaveInForkPlaygroundSettings();
129+
await website.ensureSiteManagerIsOpen();
130+
await website.page
131+
.getByLabel('WordPress version')
132+
.selectOption(version);
133+
await website.page
134+
.getByText('Apply Settings & Reset Playground')
135+
.click();
136+
await website.ensureSiteManagerIsClosed();
137+
await website.ensureSiteManagerIsOpen();
108138

109139
await expect(
110-
website.getSiteInfoRowLocator('WordPress version')
111-
).toHaveText(version);
140+
website.page.getByLabel('WordPress version')
141+
).toHaveValue(version);
112142
});
113143
});
114144

@@ -117,43 +147,39 @@ test('should display networking as inactive by default', async ({
117147
}) => {
118148
await website.goto('./');
119149
await website.ensureSiteManagerIsOpen();
120-
await expect(website.getSiteInfoRowLocator('Network access')).toContainText(
121-
'No'
122-
);
150+
await expect(website.page.getByLabel('Network access')).not.toBeChecked();
123151
});
124152

125153
test('should display networking as active when networking is enabled', async ({
126154
website,
127155
}) => {
128156
await website.goto('./?networking=yes');
129157
await website.ensureSiteManagerIsOpen();
130-
await expect(website.getSiteInfoRowLocator('Network access')).toContainText(
131-
'Yes'
132-
);
158+
await expect(website.page.getByLabel('Network access')).toBeChecked();
133159
});
134160

135161
test('should enable networking when requested', async ({ website }) => {
136162
await website.goto('./');
137163

138-
await website.openForkPlaygroundSettings();
139-
await website.setNetworkingEnabled(true);
140-
await website.clickSaveInForkPlaygroundSettings();
164+
await website.ensureSiteManagerIsOpen();
165+
await website.page.getByLabel('Network access').check();
166+
await website.page.getByText('Apply Settings & Reset Playground').click();
167+
await website.ensureSiteManagerIsClosed();
168+
await website.ensureSiteManagerIsOpen();
141169

142-
await expect(website.getSiteInfoRowLocator('Network access')).toContainText(
143-
'Yes'
144-
);
170+
await expect(website.page.getByLabel('Network access')).toBeChecked();
145171
});
146172

147173
test('should disable networking when requested', async ({ website }) => {
148174
await website.goto('./?networking=yes');
149175

150-
await website.openForkPlaygroundSettings();
151-
await website.setNetworkingEnabled(false);
152-
await website.clickSaveInForkPlaygroundSettings();
176+
await website.ensureSiteManagerIsOpen();
177+
await website.page.getByLabel('Network access').uncheck();
178+
await website.page.getByText('Apply Settings & Reset Playground').click();
179+
await website.ensureSiteManagerIsClosed();
180+
await website.ensureSiteManagerIsOpen();
153181

154-
await expect(website.getSiteInfoRowLocator('Network access')).toContainText(
155-
'No'
156-
);
182+
await expect(website.page.getByLabel('Network access')).not.toBeChecked();
157183
});
158184

159185
test('should display PHP output even when a fatal error is hit', async ({

packages/playground/website/playwright/website-page.ts

+4-71
Original file line numberDiff line numberDiff line change
@@ -25,92 +25,25 @@ export class WebsitePage {
2525
}
2626

2727
async ensureSiteManagerIsOpen() {
28-
const siteManagerHeading = this.page.getByText('Your Playgrounds');
29-
if (!(await siteManagerHeading.isVisible({ timeout: 5000 }))) {
28+
const siteManagerHeading = this.page.locator('.main-sidebar');
29+
if (await siteManagerHeading.isHidden({ timeout: 5000 })) {
3030
await this.page.getByLabel('Open Site Manager').click();
3131
}
3232
await expect(siteManagerHeading).toBeVisible();
3333
}
3434

35-
async ensureSiteViewIsExpanded() {
35+
async ensureSiteManagerIsClosed() {
3636
const openSiteButton = this.page.locator('div[title="Open site"]');
3737
if (await openSiteButton.isVisible({ timeout: 5000 })) {
3838
await openSiteButton.click();
3939
}
40-
41-
const siteManagerHeading = this.page.getByText('Your Playgrounds');
40+
const siteManagerHeading = this.page.locator('.main-sidebar');
4241
await expect(siteManagerHeading).not.toBeVisible();
4342
}
4443

45-
async openNewSiteModal() {
46-
const addPlaygroundButton = this.page.locator(
47-
'button.components-button',
48-
{
49-
hasText: 'Add Playground',
50-
}
51-
);
52-
await addPlaygroundButton.click();
53-
}
54-
55-
async clickCreateInNewSiteModal() {
56-
const createTempPlaygroundButton = this.page.locator(
57-
'button.components-button',
58-
{
59-
hasText: 'Create a temporary Playground',
60-
}
61-
);
62-
await createTempPlaygroundButton.click();
63-
}
64-
6544
async getSiteTitle(): Promise<string> {
6645
return await this.page
6746
.locator('h1[class*="_site-info-header-details-name"]')
6847
.innerText();
6948
}
70-
71-
async openForkPlaygroundSettings() {
72-
await this.ensureSiteManagerIsOpen();
73-
const editSettingsButton = this.page.locator(
74-
'button.components-button',
75-
{
76-
hasText: 'Create a similar Playground',
77-
}
78-
);
79-
await editSettingsButton.click({ timeout: 5000 });
80-
}
81-
82-
async selectPHPVersion(version: string) {
83-
const phpVersionSelect = this.page.locator('select[name=phpVersion]');
84-
await phpVersionSelect.selectOption(version);
85-
}
86-
87-
async clickSaveInForkPlaygroundSettings() {
88-
const saveSettingsButton = this.page.locator(
89-
'button.components-button.is-primary',
90-
{
91-
hasText: 'Create',
92-
}
93-
);
94-
await saveSettingsButton.click();
95-
}
96-
97-
async selectWordPressVersion(version: string) {
98-
const wordpressVersionSelect = this.page.locator(
99-
'select[name=wpVersion]'
100-
);
101-
await wordpressVersionSelect.selectOption(version);
102-
}
103-
104-
getSiteInfoRowLocator(key: string) {
105-
return this.page.getByLabel(key);
106-
}
107-
108-
async setNetworkingEnabled(enabled: boolean) {
109-
const checkbox = this.page.locator('input[name="withNetworking"]');
110-
if (enabled) {
111-
await checkbox.check();
112-
} else {
113-
await checkbox.uncheck();
114-
}
115-
}
11649
}

packages/playground/website/src/components/browser-chrome/index.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
useActiveSite,
1010
} from '../../lib/state/redux/store';
1111
import { SyncLocalFilesButton } from '../sync-local-files-button';
12+
import { Dropdown, Icon } from '@wordpress/components';
13+
import { cog } from '@wordpress/icons';
14+
import Button from '../button';
15+
import { ActiveSiteSettingsForm } from '../site-manager/site-settings-form';
1216

1317
interface BrowserChromeProps {
1418
children?: React.ReactNode;
@@ -57,6 +61,45 @@ export default function BrowserChrome({
5761
</div>
5862

5963
<div className={css.toolbarButtons}>
64+
<Dropdown
65+
className="my-container-class-name"
66+
contentClassName="my-dropdown-content-classname"
67+
popoverProps={{ placement: 'bottom-start' }}
68+
renderToggle={({ isOpen, onToggle }) => (
69+
<Button
70+
variant="browser-chrome"
71+
aria-label="Edit Playground settings"
72+
onClick={onToggle}
73+
aria-expanded={isOpen}
74+
style={{
75+
padding: '0 10px',
76+
fill: '#FFF',
77+
alignItems: 'center',
78+
display: 'flex',
79+
}}
80+
>
81+
<Icon icon={cog} />
82+
</Button>
83+
)}
84+
renderContent={({ onClose }) => (
85+
<div
86+
style={{
87+
width: 400,
88+
maxWidth: '100vw',
89+
padding: 0,
90+
}}
91+
>
92+
<div className={css.headerSection}>
93+
<h2 style={{ margin: 0 }}>
94+
Playground settings
95+
</h2>
96+
</div>
97+
<ActiveSiteSettingsForm
98+
onSubmit={onClose}
99+
/>
100+
</div>
101+
)}
102+
/>
60103
{activeSite?.metadata?.storage === 'local-fs' ? (
61104
<SyncLocalFilesButton />
62105
) : null}

0 commit comments

Comments
 (0)