Skip to content
4 changes: 4 additions & 0 deletions packages/playground/website/src/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
Expand Down Expand Up @@ -222,6 +224,8 @@ function Modals(blueprint: BlueprintDeclaration) {
}}
/>
);
} else if (currentModal === modalSlugs.RENAME_SITE) {
return <RenameSiteModal />;
} else if (currentModal === modalSlugs.MISSING_SITE_PROMPT) {
return <MissingSiteModal />;
}
Expand Down
18 changes: 10 additions & 8 deletions packages/playground/website/src/components/modal/modal-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +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 (
<Flex
justify="end"
className={css.modalButtons}
>
<Flex justify="end" gap={4} className={css.modalButtons}>
<Button
isBusy={areBusy}
disabled={areDisabled}
disabled={areDisabled || areBusy}
variant="link"
onClick={onCancel}
>
Expand All @@ -32,5 +34,5 @@ export default function ModalButtons({ submitText = 'Submit', areDisabled = fals
{submitText}
</Button>
</Flex>
)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useDispatch } from 'react-redux';
import SiteNameForm from '../site-name-form';
import { Modal } from '../modal';
import { setActiveModal } from '../../lib/state/redux/slice-ui';
import type { PlaygroundDispatch } from '../../lib/state/redux/store';
import { useActiveSite } from '../../lib/state/redux/store';
import { updateSiteMetadata } from '../../lib/state/redux/slice-sites';
import { useState } from 'react';

export const RenameSiteModal = () => {
const dispatch: PlaygroundDispatch = useDispatch();
const [isUpdating, setIsUpdating] = useState(false);

const activeSite = useActiveSite();

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

async function handleSubmit(newName: string) {
if (!activeSite || !activeSite.slug) {
return null;
}
setIsUpdating(true);
await dispatch(
updateSiteMetadata({
slug: activeSite.slug,
changes: {
name: newName,
},
})
);
setIsUpdating(false);
closeModal();
}

return (
<Modal
title="Rename Playground"
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 Cancel button will close
the modal and bring you back to where you were on the page.'
onRequestClose={closeModal}
>
<SiteNameForm
onClose={closeModal}
onSubmit={handleSubmit}
isBusy={isUpdating}
siteName={activeSite?.metadata.name ?? ''}
autoFocusNameInput={true}
/>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useDispatch } from 'react-redux';
import SiteNameForm from '../site-name-form';
import { Modal } from '../modal';
import type { PlaygroundDispatch } from '../../lib/state/redux/store';
import { useActiveSite } from '../../lib/state/redux/store';
import { updateSiteMetadata } from '../../lib/state/redux/slice-sites';
import { useState } from 'react';
import type { SiteStorageType } from '../../lib/site-metadata';
import { persistTemporarySite } from '../../lib/state/redux/persist-temporary-site';

interface SaveSiteModalProps {
storageType: Extract<SiteStorageType, 'opfs' | 'local-fs'>;
onClose: () => void;
}

export const SaveSiteModal = ({ storageType, onClose }: SaveSiteModalProps) => {
const dispatch: PlaygroundDispatch = useDispatch();
const [isSaving, setIsSaving] = useState(false);

const activeSite = useActiveSite();

const closeModal = () => {
onClose();
};

async function handleSubmit(newName: string) {
if (!activeSite || !activeSite.slug) {
return null;
}
setIsSaving(true);
await dispatch(
updateSiteMetadata({
slug: activeSite.slug,
changes: {
name: newName,
},
})
);
await dispatch(persistTemporarySite(activeSite.slug, storageType));
setIsSaving(false);
closeModal();
}

return (
<Modal
title="Save Playground"
contentLabel='This is a dialog window which overlays the main content of the
page. The modal begins with a heading 2 called "Save
Playground". Pressing the Cancel button will close
the modal and bring you back to where you were on the page.'
onRequestClose={closeModal}
>
<SiteNameForm
onClose={closeModal}
onSubmit={handleSubmit}
isBusy={isSaving}
siteName={activeSite?.metadata.name ?? ''}
autoFocusNameInput={true}
/>
</Modal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { usePlaygroundClientInfo } from '../../../lib/use-playground-client';
import { OfflineNotice } from '../../offline-notice';
import { DownloadAsZipMenuItem } from '../../toolbar-buttons/download-as-zip';
import { GithubExportMenuItem } from '../../toolbar-buttons/github-export-menu-item';
import { RenameMenuItem } from '../../toolbar-buttons/rename-menu-item';
import { ReportError } from '../../toolbar-buttons/report-error';
import { TemporarySiteNotice } from '../temporary-site-notice';
import type { SiteInfo } from '../../../lib/state/redux/slice-sites';
Expand Down Expand Up @@ -222,6 +223,9 @@ export function SiteInfoPanel({
<>
{!isTemporary && (
<MenuGroup>
<RenameMenuItem
onClose={onClose}
/>
<MenuItem
aria-label="Delete this Playground"
className={css.danger}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAppSelector, useAppDispatch } from '../../../lib/state/redux/store';
import { useAppSelector } from '../../../lib/state/redux/store';
import {
DropdownMenu,
DropdownMenuItem,
Expand All @@ -7,11 +7,12 @@ import {
// @ts-ignore
} from '@wordpress/components/build/dropdown-menu-v2/index.js';
import css from './style.module.css';
import { persistTemporarySite } from '../../../lib/state/redux/persist-temporary-site';
import { selectClientInfoBySiteSlug } from '../../../lib/state/redux/slice-clients';
import { useLocalFsAvailability } from '../../../lib/hooks/use-local-fs-availability';
import { isOpfsAvailable } from '../../../lib/state/opfs/opfs-site-storage';
import type { SiteStorageType } from '../../../lib/site-metadata';
import { useState } from 'react';
import { SaveSiteModal } from '../../save-site-modal';
import type { PersistedSiteStorageType } from '../../../lib/site-metadata';

export function SitePersistButton({
siteSlug,
Expand All @@ -20,34 +21,40 @@ export function SitePersistButton({
}: {
siteSlug: string;
children: React.ReactNode;
storage?: Extract<SiteStorageType, 'opfs' | 'local-fs'> | null;
storage?: PersistedSiteStorageType | null;
}) {
const [selectedStorageType, setSelectedStorageType] =
useState<PersistedSiteStorageType | null>(null);
const clientInfo = useAppSelector((state) =>
selectClientInfoBySiteSlug(state, siteSlug)
);
const localFsAvailability = useLocalFsAvailability(clientInfo?.client);
const dispatch = useAppDispatch();

const persistSiteClick = (storageType: PersistedSiteStorageType) => {
setSelectedStorageType(storageType);
};

if (selectedStorageType) {
return (
<SaveSiteModal
storageType={selectedStorageType}
onClose={() => setSelectedStorageType(null)}
/>
);
}

if (!clientInfo?.opfsSync || clientInfo.opfsSync?.status === 'error') {
let button = null;
if (storage) {
button = (
<div
onClick={() =>
dispatch(persistTemporarySite(siteSlug, storage))
}
>
{children}
</div>
<div onClick={() => persistSiteClick(storage)}>{children}</div>
);
} else {
button = (
<DropdownMenu trigger={children}>
<DropdownMenuItem
disabled={!isOpfsAvailable}
onClick={() =>
dispatch(persistTemporarySite(siteSlug, 'opfs'))
}
onClick={() => persistSiteClick('opfs')}
>
<DropdownMenuItemLabel>
Save in this browser
Expand All @@ -62,9 +69,7 @@ export function SitePersistButton({
</DropdownMenuItem>
<DropdownMenuItem
disabled={localFsAvailability !== 'available'}
onClick={() =>
dispatch(persistTemporarySite(siteSlug, 'local-fs'))
}
onClick={() => persistSiteClick('local-fs')}
>
<DropdownMenuItemLabel>
Save in a local directory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { useState } from 'react';
import ModalButtons from '../modal/modal-buttons';
import { TextControl } from '@wordpress/components';

interface SiteNameFormProps {
onClose: () => void;
onSubmit: (newName: string) => void;
isBusy: boolean;
siteName: string;
autoFocusNameInput?: boolean;
}

export default function SiteNameForm({
onClose,
onSubmit,
isBusy,
siteName,
autoFocusNameInput = false,
}: SiteNameFormProps) {
const [newName, setNewName] = useState<string>(siteName);

function submitOnEnter(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
onSubmit(newName);
}
}

return (
<>
<TextControl
label="Playground name"
placeholder="My Playground"
value={newName}
onChange={setNewName}
onKeyDown={submitOnEnter}
autoFocus={autoFocusNameInput}
/>

<ModalButtons
areDisabled={!newName}
onCancel={onClose}
onSubmit={() => onSubmit(newName)}
submitText="Save"
areBusy={isBusy}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MenuItem } from '@wordpress/components';
import { setActiveModal } from '../../lib/state/redux/slice-ui';
import type { PlaygroundDispatch } from '../../lib/state/redux/store';
import { useDispatch } from 'react-redux';
import { modalSlugs } from '../layout';

interface Props {
onClose: () => void;
disabled?: boolean;
}
export function RenameMenuItem({ onClose, disabled }: Props) {
const dispatch: PlaygroundDispatch = useDispatch();
return (
<MenuItem
aria-label="Rename this Playground"
disabled={disabled}
onClick={() => {
dispatch(setActiveModal(modalSlugs.RENAME_SITE));
onClose();
}}
>
Rename
</MenuItem>
);
}
4 changes: 4 additions & 0 deletions packages/playground/website/src/lib/site-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { resolveBlueprintFromURL } from './state/url/resolve-blueprint-from-url'
*/
export const SiteStorageTypes = ['opfs', 'local-fs', 'none'] as const;
export type SiteStorageType = (typeof SiteStorageTypes)[number];
export type PersistedSiteStorageType = Extract<
SiteStorageType,
'opfs' | 'local-fs'
>;

/**
* The site logo data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function getSqliteDriverModuleDetails(
url: string;
} {
switch (version) {

case 'develop':
/** @ts-ignore */
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"develop": "develop",
"v2.1.16": "v2.1.16"
}
"develop": "develop",
"v2.1.16": "v2.1.16"
}
Loading