Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState } from 'react';
import { Notice, __experimentalText as Text } from '@wordpress/components';
import { useAppDispatch, useAppSelector } from '../../lib/state/redux/store';
import {
setActiveModal,
setSiteSlugToDelete,
} from '../../lib/state/redux/slice-ui';
import { useSitesAPI } from '../../lib/state/redux/site-management-api-middleware';
import { Modal } from '../modal';
import ModalButtons from '../modal/modal-buttons';
import css from '../modal/style.module.css';

export function DeleteSiteModal() {
const dispatch = useAppDispatch();
const sitesAPI = useSitesAPI();
const siteSlugToDelete = useAppSelector(
(state) => state.ui.siteSlugToDelete
);
const site = useAppSelector((state) =>
siteSlugToDelete ? state.sites.entities[siteSlugToDelete] : undefined
);

const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

if (!site) {
return null;
}

const closeModal = () => {
dispatch(setActiveModal(null));
dispatch(setSiteSlugToDelete(undefined));
};

const handleSubmit = async () => {
try {
setIsSubmitting(true);
setError(null);
await sitesAPI.delete(site.slug);
closeModal();
} catch (e) {
setError(
e instanceof Error
? e.message
: 'Deleting failed. Please try again.'
);
} finally {
setIsSubmitting(false);
}
};

return (
<Modal
title="Delete Playground"
contentLabel='This is a dialog window which overlays the main content of the page. The modal begins with a heading 2 called "Delete Playground". Pressing the Close button will close the modal and bring you back to where you were on the page.'
onRequestClose={closeModal}
small
>
<form
className={css.modalForm}
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Text>
Are you sure you want to delete the site &ldquo;
{site.metadata.name}&rdquo;? This action cannot be undone.
</Text>
{error ? (
<Notice status="error" isDismissible={false}>
{error}
</Notice>
) : null}
<ModalButtons
submitText="Delete"
areBusy={isSubmitting}
onCancel={closeModal}
/>
</form>
</Modal>
);
}
3 changes: 3 additions & 0 deletions packages/playground/website/src/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../playground-viewport';
import { MissingSiteModal } from '../missing-site-modal';
import { RenameSiteModal } from '../rename-site-modal';
import { DeleteSiteModal } from '../delete-site-modal';
import { SaveSiteModal } from '../save-site-modal';
import { modalSlugs } from '../../lib/state/redux/slice-ui';
import { GitHubPrivateRepoAuthModal } from '../github-private-repo-auth-modal';
Expand Down Expand Up @@ -167,6 +168,8 @@ function Modals() {
return <MissingSiteModal />;
} else if (currentModal === modalSlugs.RENAME_SITE) {
return <RenameSiteModal />;
} else if (currentModal === modalSlugs.DELETE_SITE) {
return <DeleteSiteModal />;
} else if (currentModal === modalSlugs.SAVE_SITE) {
return <SaveSiteModal />;
} else if (currentModal === modalSlugs.GITHUB_PRIVATE_REPO_AUTH) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
max-width: 350px;
}

.modal-form {
display: flex;
flex-direction: column;
gap: 12px;
}

.modal-buttons {
margin-top: 1.5rem;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { TextControl } from '@wordpress/components';
import { Notice, TextControl } from '@wordpress/components';
import { useAppDispatch, useAppSelector } from '../../lib/state/redux/store';
import {
setActiveModal,
Expand All @@ -8,6 +8,7 @@ import {
import { useSitesAPI } from '../../lib/state/redux/site-management-api-middleware';
import { Modal } from '../modal';
import ModalButtons from '../modal/modal-buttons';
import css from '../modal/style.module.css';

export function RenameSiteModal() {
const dispatch = useAppDispatch();
Expand All @@ -22,6 +23,7 @@ export function RenameSiteModal() {
const initialName = useMemo(() => site?.metadata?.name ?? '', [site]);
const [name, setName] = useState<string>(initialName);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

if (!site || site.metadata.storage === 'none') {
// Nothing to rename
Expand All @@ -40,8 +42,15 @@ export function RenameSiteModal() {
}
try {
setIsSubmitting(true);
setError(null);
await sitesAPI.rename(trimmed);
closeModal();
} catch (e) {
setError(
e instanceof Error
? e.message
: 'Renaming failed. Please try again.'
);
} finally {
setIsSubmitting(false);
}
Expand All @@ -59,7 +68,7 @@ export function RenameSiteModal() {
e.preventDefault();
handleSubmit();
}}
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
className={css.modalForm}
>
<TextControl
__nextHasNoMarginBottom
Expand All @@ -70,6 +79,11 @@ export function RenameSiteModal() {
maxLength={80}
autoFocus
/>
{error ? (
<Notice status="error" isDismissible={false}>
{error}
</Notice>
) : null}
<ModalButtons
submitText="Rename"
areDisabled={!name.trim()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
BaseControl,
TextControl,
RadioControl,
Notice,
} from '@wordpress/components';
import { Modal } from '../modal';
import ModalButtons from '../modal/modal-buttons';
Expand All @@ -30,11 +31,6 @@ const helpTextStyle: CSSProperties = {
marginTop: 8,
};

const errorTextStyle: CSSProperties = {
color: '#d63638',
marginTop: 8,
};

export function SaveSiteModal() {
const dispatch = useAppDispatch();
const sitesAPI = useSitesAPI();
Expand Down Expand Up @@ -261,7 +257,11 @@ export function SaveSiteModal() {
// Don't close modal here - useEffect will close it when save completes
} catch (error) {
logger.error(error);
setSubmitError('Saving failed. Please try again.');
setSubmitError(
error instanceof Error
? error.message
: 'Saving failed. Please try again.'
);
setIsSubmitting(false);
}
};
Expand Down Expand Up @@ -370,7 +370,9 @@ export function SaveSiteModal() {
</Button>
</div>
{directoryError ? (
<p style={errorTextStyle}>{directoryError}</p>
<Notice status="error" isDismissible={false}>
{directoryError}
</Notice>
) : null}
</BaseControl>
)}
Expand All @@ -389,16 +391,18 @@ export function SaveSiteModal() {
</p>
</div>
)}
{submitError ? (
<Notice status="error" isDismissible={false}>
{submitError}
</Notice>
) : null}
<ModalButtons
submitText="Save"
onCancel={handleRequestClose}
areDisabled={saveDisabled}
areBusy={false}
style={{ marginTop: 0 }}
/>
{submitError ? (
<p style={errorTextStyle}>{submitError}</p>
) : null}
</form>
</Modal>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
setSiteManagerOpen,
setSiteManagerSection,
setSiteSlugToRename,
setSiteSlugToDelete,
} from '../../lib/state/redux/slice-ui';
import { useSitesAPI } from '../../lib/state/redux/site-management-api-middleware';
import { WordPressIcon } from '@wp-playground/components';
Expand Down Expand Up @@ -247,14 +248,10 @@ export function SavedPlaygroundsOverlay({
return `data:${logo.mime};base64,${logo.data}`;
};

const handleDeleteSite = async (site: SiteInfo, closeMenu: () => void) => {
const proceed = window.confirm(
`Are you sure you want to delete the site '${site.metadata.name}'?`
);
if (proceed) {
await sitesAPI.delete(site.slug);
closeMenu();
}
const handleDeleteSite = (site: SiteInfo, closeMenu: () => void) => {
dispatch(setSiteSlugToDelete(site.slug));
modalDispatch(setActiveModal(modalSlugs.DELETE_SITE));
closeMenu();
};
Comment on lines +251 to 255
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletion modal state is being split across two dispatchers (dispatch vs modalDispatch). If these target different Redux stores/providers, the modal can open without ui.siteSlugToDelete set (or vice versa), causing the UI to render no modal content and leaving activeModal stuck. Use the same dispatcher for both setSiteSlugToDelete(...) and setActiveModal(...) so the slug and modal state are always updated atomically in the same store.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bgrgicak Valid point for consideration? Thoughts? Not a blocker of course.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these target different Redux stores/providers, the modal can open without ui.siteSlugToDelete set (or vice versa)

Both dispatches use the same store.


const handleRenameSite = (site: SiteInfo, closeMenu: () => void) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ import {
modalSlugs,
setActiveModal,
setSiteManagerOpen,
setSiteManagerSection,
setSiteSlugToDelete,
setSiteSlugToRename,
} from '../../../lib/state/redux/slice-ui';
import { useAppDispatch, useAppSelector } from '../../../lib/state/redux/store';
import { useSitesAPI } from '../../../lib/state/redux/site-management-api-middleware';
import { usePlaygroundClientInfo } from '../../../lib/use-playground-client';
import { SiteLogs } from '../../log-modal';
import { OfflineNotice } from '../../offline-notice';
Expand Down Expand Up @@ -85,8 +84,6 @@ export function SiteInfoPanel({
}) {
const offline = useAppSelector((state) => state.ui.offline);
const dispatch = useAppDispatch();
const sitesAPI = useSitesAPI();

// Load the last active tab for this site
const [initialTabName] = useState(() => {
const lastTab = getSiteLastTab(site.slug);
Expand All @@ -103,16 +100,10 @@ export function SiteInfoPanel({

const isTemporary = site.metadata.storage === 'none';

const removeSiteAndCloseMenu = async (onClose: () => void) => {
// TODO: Replace with HTML-based dialog
const proceed = window.confirm(
`Are you sure you want to delete the site '${site.metadata.name}'?`
);
if (proceed) {
await sitesAPI.delete(site.slug);
dispatch(setSiteManagerSection('sidebar'));
onClose();
}
const removeSiteAndCloseMenu = (onClose: () => void) => {
dispatch(setSiteSlugToDelete(site.slug));
dispatch(setActiveModal(modalSlugs.DELETE_SITE));
onClose();
};
const clientInfo = useAppSelector((state) =>
selectClientInfoBySiteSlug(state, site.slug)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export function persistTemporarySite(
skipRenameModal?: boolean;
} = {}
) {
// @TODO: Handle errors
return async (
dispatch: typeof store.dispatch,
getState: () => PlaygroundReduxState
Expand Down Expand Up @@ -135,21 +134,15 @@ export function persistTemporarySite(
} else if (storageType === 'local-fs') {
let dirHandle = options.localFsHandle;
if (!dirHandle) {
try {
// Request permission to access the directory.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker
dirHandle = await (window as any).showDirectoryPicker({
// By specifying an ID, the browser can remember different directories
// for different IDs.If the same ID is used for another picker, the
// picker opens in the same directory.
id: 'playground-directory',
mode: 'readwrite',
});
} catch (e) {
// No directory selected but log the error just in case.
logger.error(e);
return;
}
// Request permission to access the directory.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker
dirHandle = await (window as any).showDirectoryPicker({
// By specifying an ID, the browser can remember different directories
// for different IDs.If the same ID is used for another picker, the
// picker opens in the same directory.
id: 'playground-directory',
mode: 'readwrite',
});
}
await saveDirectoryHandle(siteSlug, dirHandle!);

Expand Down
Loading
Loading