Repository: Automattic/studio
Pull request #1011: Edit WordPress version in site details modal
Author: login: sejas
Created: 2025-03-05T16:06:27Z | Merged: 2025-03-06T20:53:22Z

<!--
Link a related issue to this PR. If the PR does not immediately resolve the issue,
for example, it requires a separate deployment to production, avoid
using the "Fixes" keyword and use "Related to" instead.
-->

## Related issues

- Fixes https://github.com/Automattic/dotcom-forge/issues/10536

## Proposed Changes

- Add the ability to change WordPress version in the settings modal

## Testing Instructions

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

- Run

**Change WordPress version**

- Run `STUDIO_WP_VERSIONS=true npm start`
- Go to settings tab
- Click Edit, next to Site Details
- Select a different WordPress version
- Save
- Wait ~1 minute
- Start the site if it was not started before
- Open wp-admin
- Observe the WordPress version installed is what you selected before.
- Repeat with beta versions, downgrading, upgrading and confirm it works as expected.

**Change name and PHP version**

- Confirm editing the name and PHP version are working as expected.

https://github.com/user-attachments/assets/3855c96c-0e14-49cc-a5c1-24a60676c8ee

**Error message**

We display an error message when the wp version update fails. An easy way to reproduce is updating a beta in a WP site in "Spanish": https://github.com/Automattic/dotcom-forge/issues/10568

- Change your languages to Spanish or other language
- Select your a beta version 
- Save
- Observe the alert and error message. The error message says: `Failed download`
- Change the WP version to a valid version
- Confirm it works as expected, and the error message disappears when you click save.


https://github.com/user-attachments/assets/176c0f98-fca7-41fa-9913-7fb8bbd77b65


## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Have you checked for TypeScript, React or other console errors?



Diff:
diff --git a/src/components/content-tab-settings.tsx b/src/components/content-tab-settings.tsx
index 65350b8b16..31d1e8c519 100644
--- a/src/components/content-tab-settings.tsx
+++ b/src/components/content-tab-settings.tsx
@@ -31,7 +31,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
 	// Empty strings account for legacy sites lacking a stored password.
 	const storedPassword = decodePassword( selectedSite.adminPassword ?? '' );
 	const password = storedPassword === '' ? 'password' : storedPassword;
-	const wpVersion = useGetWpVersion( selectedSite );
+	const [ wpVersion, refreshWpVersion ] = useGetWpVersion( selectedSite );
 	return (
 		<div className="p-8">
 			<table className="mb-2 m-w-full" cellPadding={ 0 } cellSpacing={ 0 }>
@@ -40,7 +40,9 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
 						<th colSpan={ 2 } className="pb-4 ltr:text-left rtl:text-right">
 							<h3 className="text-black text-sm font-semibold">
 								{ __( 'Site details' ) }
-								{ wpVersionsEnabled && <EditSiteDetails /> }
+								{ wpVersionsEnabled && (
+									<EditSiteDetails currentWpVersion={ wpVersion } onSave={ refreshWpVersion } />
+								) }
 							</h3>
 						</th>
 					</tr>
diff --git a/src/components/error-information.tsx b/src/components/error-information.tsx
new file mode 100644
index 0000000000..c7898610f9
--- /dev/null
+++ b/src/components/error-information.tsx
@@ -0,0 +1,16 @@
+import { cx } from 'src/lib/cx';
+import { ErrorIcon } from './error-icon';
+
+type ErrorInformationProps = {
+	children: React.ReactNode;
+	className?: string;
+};
+
+export function ErrorInformation( { children, className }: ErrorInformationProps ) {
+	return (
+		<div className={ cx( 'flex items-center gap-1 text-xs text-red-500', className ) }>
+			<ErrorIcon />
+			{ children }
+		</div>
+	);
+}
diff --git a/src/components/tests/content-tab-assistant.test.tsx b/src/components/tests/content-tab-assistant.test.tsx
index 9afc56c88b..e80540b725 100644
--- a/src/components/tests/content-tab-assistant.test.tsx
+++ b/src/components/tests/content-tab-assistant.test.tsx
@@ -117,7 +117,7 @@ describe( 'ContentTabAssistant', () => {
 			showMessageBox: jest.fn().mockResolvedValue( { response: 0, checkboxChecked: false } ),
 			executeWPCLiInline: jest.fn().mockResolvedValue( { stdout: '', stderr: 'Error' } ),
 		} );
-		( useGetWpVersion as jest.Mock ).mockReturnValue( '6.4.3' );
+		( useGetWpVersion as jest.Mock ).mockReturnValue( [ '6.4.3', jest.fn() ] );
 	} );
 
 	it( 'renders placeholder text input', () => {
diff --git a/src/components/tests/content-tab-settings.test.tsx b/src/components/tests/content-tab-settings.test.tsx
index a39b3916c5..b8c6bc4f03 100644
--- a/src/components/tests/content-tab-settings.test.tsx
+++ b/src/components/tests/content-tab-settings.test.tsx
@@ -29,7 +29,7 @@ describe( 'ContentTabSettings', () => {
 	const generateProposedSitePath = jest.fn();
 	beforeEach( () => {
 		jest.clearAllMocks();
-		( useGetWpVersion as jest.Mock ).mockReturnValue( '7.7.7' );
+		( useGetWpVersion as jest.Mock ).mockReturnValue( [ '7.7.7', jest.fn() ] );
 		( getIpcApi as jest.Mock ).mockReturnValue( {
 			copyText,
 			openLocalPath,
diff --git a/src/components/text-control.tsx b/src/components/text-control.tsx
index e7de4344b1..c9927245a8 100644
--- a/src/components/text-control.tsx
+++ b/src/components/text-control.tsx
@@ -12,6 +12,8 @@ const TextControlComponent = ( props: TextControlProps ) => {
 			__nextHasNoMarginBottom={ true }
 			className={ cx(
 				'[&_input]:!px-4 [&_input]:!py-3 [&_input]:!rounded-sm [&_input]:!self-stretch [&_input]:!align-center [&_input]:!gap-1 [&_input]:!flex',
+				props.disabled &&
+					'[&_input]:!bg-a8c-gray-100 [&_input]:!text-a8c-gray-600 [&_input]:!border-a8c-gray-400',
 				props.className
 			) }
 		/>
diff --git a/src/hooks/use-get-wp-version.ts b/src/hooks/use-get-wp-version.ts
index e3b2607eb0..7d37de5c21 100644
--- a/src/hooks/use-get-wp-version.ts
+++ b/src/hooks/use-get-wp-version.ts
@@ -1,10 +1,13 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { getIpcApi } from 'src/lib/get-ipc-api';
 
 export function useGetWpVersion( site: SiteDetails ) {
 	const [ wpVersion, setWpVersion ] = useState( '-' );
-	useEffect( () => {
+	const refreshWpVersion = useCallback( () => {
 		getIpcApi().getWpVersion( site.id ).then( setWpVersion );
-	}, [ site.id, site.running ] );
-	return wpVersion;
+	}, [ site.id ] );
+	useEffect( () => {
+		refreshWpVersion();
+	}, [ site.running, refreshWpVersion ] );
+	return [ wpVersion, refreshWpVersion ] as const;
 }
diff --git a/src/index.css b/src/index.css
index 2e5e6f163e..dcb4ebcf0d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -271,4 +271,8 @@ blockquote {
 
 .error-message {
 	color: #D63638;
+}
+
+.error-select-control .components-input-control__backdrop {
+	border-color: theme( 'colors.red.500' )!important;
 }
\ No newline at end of file
diff --git a/src/modules/site-settings/edit-site-details.tsx b/src/modules/site-settings/edit-site-details.tsx
index 122b502197..7e45821c90 100644
--- a/src/modules/site-settings/edit-site-details.tsx
+++ b/src/modules/site-settings/edit-site-details.tsx
@@ -1,79 +1,118 @@
 import { SupportedPHPVersion, SupportedPHPVersions } from '@php-wasm/universal';
 import { SelectControl } from '@wordpress/components';
 import { useI18n } from '@wordpress/react-i18n';
-import { FormEvent, useCallback, useEffect, useState } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import stripAnsi from 'strip-ansi';
 import Button from 'src/components/button';
+import { ErrorInformation } from 'src/components/error-information';
 import Modal from 'src/components/modal';
 import TextControlComponent from 'src/components/text-control';
 import { useSiteDetails } from 'src/hooks/use-site-details';
+import { cx } from 'src/lib/cx';
+import { getIpcApi } from 'src/lib/get-ipc-api';
 import { useRootSelector } from 'src/stores';
 import { wordpressVersionsSelectors } from 'src/stores/wordpress-versions-slice';
 import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';
 
-export default function EditSiteDetails() {
+type EditSiteDetailsProps = {
+	currentWpVersion: string;
+	onSave: () => void;
+};
+
+export default function EditSiteDetails( { currentWpVersion, onSave }: EditSiteDetailsProps ) {
 	const { __ } = useI18n();
 	const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
-	const [ editSiteError, setEditSiteError ] = useState( '' );
+	const [ isChangeWpError, setIsChangeWpError ] = useState( '' );
 	const [ showModal, setShowModal ] = useState( false );
 	const [ isEditingSite, setIsEditingSite ] = useState( false );
+	const closeModal = useCallback( () => {
+		if ( isEditingSite ) {
+			return;
+		}
+		setShowModal( false );
+	}, [ isEditingSite ] );
 	const [ siteName, setSiteName ] = useState( selectedSite?.name ?? '' );
 	const [ selectedPhpVersion, setSelectedPhpVersion ] = useState< SupportedPHPVersion >(
 		( selectedSite?.phpVersion as SupportedPHPVersion ) ?? DEFAULT_PHP_VERSION
 	);
-	const [ selectedWpVersion, setSelectedWpVersion ] = useState( 'latest' );
-
+	const [ selectedWpVersion, setSelectedWpVersion ] = useState( currentWpVersion );
 	const wordpressVersions = useRootSelector( wordpressVersionsSelectors.selectWordPressVersions );
+	const wordpressVersionOptions = wordpressVersions.map( ( version ) => ( {
+		label: version.label,
+		value: version.value,
+	} ) );
+	if ( ! wordpressVersionOptions.some( ( version ) => version.value === currentWpVersion ) ) {
+		wordpressVersionOptions.push( { label: currentWpVersion, value: currentWpVersion } );
+	}
 
-	useEffect( () => {
-		if ( selectedSite && showModal ) {
-			setSiteName( selectedSite.name );
-			setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+	const resetFormState = useCallback( () => {
+		if ( ! selectedSite ) {
+			return;
 		}
-	}, [ selectedSite, showModal ] );
+		setSiteName( selectedSite.name );
+		setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+		setSelectedWpVersion( currentWpVersion );
+		setIsChangeWpError( '' );
+	}, [ currentWpVersion, selectedSite ] );
 
-	const resetLocalState = useCallback( () => {
-		setShowModal( false );
-		setSiteName( '' );
-		setSelectedPhpVersion( DEFAULT_PHP_VERSION );
-		setSelectedWpVersion( 'latest' );
-		setEditSiteError( '' );
-	}, [] );
+	const onSiteEdit = async ( event: FormEvent ) => {
+		event.preventDefault();
+		if ( ! selectedSite?.id ) {
+			return;
+		}
+		setIsEditingSite( true );
+		setIsChangeWpError( '' );
+		try {
+			const running = selectedSite.running;
 
-	const onSiteEdit = useCallback(
-		async ( event: FormEvent ) => {
-			event.preventDefault();
-			if ( ! selectedSite ) {
-				return;
+			const hasWpVersionChanged = selectedWpVersion !== currentWpVersion;
+			const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
+			const needsRestart = running && ( hasWpVersionChanged || hasPhpVersionChanged );
+			if ( needsRestart ) {
+				await stopServer( selectedSite.id );
 			}
-			setIsEditingSite( true );
-			try {
-				const running = selectedSite.running;
-				await updateSite( {
-					...selectedSite,
-					name: siteName,
-					phpVersion: selectedPhpVersion,
-				} );
-				if ( running && selectedSite.phpVersion !== selectedPhpVersion ) {
-					await stopServer( selectedSite.id );
-					await startServer( selectedSite.id );
+
+			if ( hasWpVersionChanged ) {
+				try {
+					const result = await getIpcApi().executeWPCLiInline( {
+						siteId: selectedSite.id,
+						args: `core update --version=${ selectedWpVersion } --force`,
+						skipPluginsAndThemes: true,
+					} );
+					if ( result.exitCode !== 0 ) {
+						throw new Error( result.stderr );
+					}
+				} catch ( wpError ) {
+					console.error( 'Error changing WordPress version:', wpError );
+					const errorMessage = stripAnsi( ( wpError as Error )?.message );
+					setIsChangeWpError( __( 'Error changing WordPress version' ) );
+					getIpcApi().showErrorMessageBox( {
+						title: __( 'Error changing WordPress version' ),
+						message: errorMessage,
+					} );
+					setSelectedWpVersion( currentWpVersion );
+					setIsEditingSite( false );
+					return;
 				}
-				setShowModal( false );
-				resetLocalState();
-			} catch ( e ) {
-				setEditSiteError( ( e as Error )?.message );
 			}
-			setIsEditingSite( false );
-		},
-		[
-			updateSite,
-			selectedSite,
-			siteName,
-			selectedPhpVersion,
-			resetLocalState,
-			startServer,
-			stopServer,
-		]
-	);
+
+			await updateSite( {
+				...selectedSite,
+				name: siteName,
+				phpVersion: selectedPhpVersion,
+			} );
+
+			if ( needsRestart ) {
+				await startServer( selectedSite.id );
+			}
+			onSave();
+			closeModal();
+			resetFormState();
+		} catch ( e ) {
+			setIsChangeWpError( ( e as Error )?.message );
+		}
+		setIsEditingSite( false );
+	};
 
 	return (
 		<>
@@ -83,22 +122,28 @@ export default function EditSiteDetails() {
 					title={ __( 'Edit site' ) }
 					isDismissible
 					focusOnMount="firstContentElement"
-					onRequestClose={ resetLocalState }
+					onRequestClose={ closeModal }
+					className={ cx(
+						isEditingSite &&
+							'[&_[aria-label="Close"]_svg]:opacity-50 [&_[aria-label="Close"]]:cursor-not-allowed'
+					) }
 				>
 					<form onSubmit={ onSiteEdit }>
 						<div className="flex flex-col gap-6">
 							<label className="flex flex-col gap-1.5 leading-4">
 								<span className="font-semibold">{ __( 'Site name' ) }</span>
 								<TextControlComponent
+									disabled={ isEditingSite }
 									onChange={ setSiteName }
 									value={ siteName }
 								></TextControlComponent>
 							</label>
 
-							<div className="flex flex-row gap-x-6">
+							<div className="flex flex-row gap-x-6 pb-2">
 								<label className="flex flex-1 flex-col gap-1.5 leading-4">
 									<span className="font-semibold">{ __( 'PHP version' ) }</span>
 									<SelectControl
+										disabled={ isEditingSite }
 										value={ selectedPhpVersion }
 										options={ SupportedPHPVersions.map( ( version ) => ( {
 											label: version,
@@ -106,23 +151,31 @@ export default function EditSiteDetails() {
 										} ) ) }
 										onChange={ ( version ) => setSelectedPhpVersion( version ) }
 										__next40pxDefaultSize
+										__nextHasNoMarginBottom
 									/>
 								</label>
 
 								<label className="flex flex-1 flex-col gap-1.5 leading-4">
 									<span className="font-semibold">{ __( 'WordPress version' ) }</span>
 									<SelectControl
+										className={ cx( isChangeWpError && 'error-select-control' ) }
+										disabled={ isEditingSite }
 										value={ selectedWpVersion }
-										options={ wordpressVersions }
-										onChange={ ( version ) => setSelectedWpVersion( version ) }
+										options={ wordpressVersionOptions }
+										onChange={ setSelectedWpVersion }
 										__next40pxDefaultSize
+										__nextHasNoMarginBottom
 									/>
 								</label>
 							</div>
 						</div>
 
+						{ isChangeWpError && (
+							<ErrorInformation className="mt-4">{ isChangeWpError }</ErrorInformation>
+						) }
+
 						<div className="flex flex-row justify-end gap-x-5 mt-6">
-							<Button onClick={ resetLocalState } disabled={ isEditingSite } variant="tertiary">
+							<Button onClick={ closeModal } disabled={ isEditingSite } variant="tertiary">
 								{ __( 'Cancel' ) }
 							</Button>
 							<Button
@@ -133,9 +186,9 @@ export default function EditSiteDetails() {
 									isEditingSite ||
 										! selectedSite ||
 										( selectedSite?.name === siteName &&
-											selectedSite?.phpVersion === selectedPhpVersion ) ||
-										! siteName.trim() ||
-										editSiteError
+											selectedSite?.phpVersion === selectedPhpVersion &&
+											currentWpVersion === selectedWpVersion ) ||
+										! siteName.trim()
 								) }
 							>
 								{ isEditingSite ? __( 'Saving…' ) : __( 'Save' ) }
@@ -149,6 +202,7 @@ export default function EditSiteDetails() {
 				className="!mx-4 shrink-0"
 				onClick={ () => {
 					setShowModal( true );
+					resetFormState();
 				} }
 				label={ __( 'Edit site' ) }
 				variant="link"
diff --git a/src/modules/site-settings/tests/edit-site-details.test.tsx b/src/modules/site-settings/tests/edit-site-details.test.tsx
new file mode 100644
index 0000000000..d26487f225
--- /dev/null
+++ b/src/modules/site-settings/tests/edit-site-details.test.tsx
@@ -0,0 +1,269 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import EditSiteDetails from 'src/modules/site-settings/edit-site-details';
+
+// Mock the hooks and dependencies
+const mockUpdateSite = jest.fn();
+const mockStopServer = jest.fn();
+const mockStartServer = jest.fn();
+const mockExecuteWPCLiInline = jest.fn();
+const mockShowErrorMessageBox = jest.fn();
+
+jest.mock( 'src/hooks/use-site-details', () => ( {
+	useSiteDetails: () => ( {
+		selectedSite: {
+			id: 'site-123',
+			name: 'Test Site',
+			phpVersion: '8.0',
+			running: true,
+		},
+		updateSite: mockUpdateSite,
+		stopServer: mockStopServer,
+		startServer: mockStartServer,
+	} ),
+} ) );
+
+jest.mock( 'src/lib/get-ipc-api', () => ( {
+	getIpcApi: () => ( {
+		executeWPCLiInline: mockExecuteWPCLiInline,
+		showErrorMessageBox: mockShowErrorMessageBox,
+	} ),
+} ) );
+
+jest.mock( '@wordpress/react-i18n', () => ( {
+	useI18n: () => ( {
+		__: ( text: string ) => text,
+	} ),
+} ) );
+
+jest.mock( 'src/stores', () => ( {
+	useRootSelector: jest.fn().mockImplementation( () => {
+		// Mock the WordPress versions selector
+		return [
+			{ label: '6.4', value: '6.4' },
+			{ label: '6.3', value: '6.3' },
+			{ label: '6.2', value: '6.2' },
+		];
+	} ),
+} ) );
+
+describe( 'EditSiteDetails', () => {
+	const defaultProps = {
+		currentWpVersion: '6.3',
+		onSave: jest.fn(),
+	};
+
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockExecuteWPCLiInline.mockResolvedValue( { exitCode: 0 } );
+	} );
+
+	it( 'should render the edit button', () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		expect( screen.getByRole( 'button', { name: 'Edit site' } ) ).toBeInTheDocument();
+	} );
+
+	it( 'should open the modal when edit button is clicked', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
+		expect( screen.getByLabelText( 'Site name' ) ).toHaveValue( 'Test Site' );
+		expect( screen.getByLabelText( 'PHP version' ) ).toHaveValue( '8.0' );
+		expect( screen.getByLabelText( 'WordPress version' ) ).toHaveValue( '6.3' );
+	} );
+
+	it( 'should close the modal when cancel button is clicked', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+		await user.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
+
+		expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'should disable the save button when no changes are made', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		expect( screen.getByRole( 'button', { name: 'Save' } ) ).toBeDisabled();
+	} );
+
+	it( 'should enable the save button when site name is changed', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const siteNameInput = screen.getByLabelText( 'Site name' );
+		await user.clear( siteNameInput );
+		await user.type( siteNameInput, 'New Site Name' );
+
+		expect( screen.getByRole( 'button', { name: 'Save' } ) ).toBeEnabled();
+	} );
+
+	it( 'should enable the save button when PHP version is changed', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const phpVersionSelect = screen.getByLabelText( 'PHP version' );
+		await user.selectOptions( phpVersionSelect, '8.2' );
+
+		expect( screen.getByRole( 'button', { name: 'Save' } ) ).toBeEnabled();
+	} );
+
+	it( 'should enable the save button when WordPress version is changed', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
+		await user.selectOptions( wpVersionSelect, '6.4' );
+
+		expect( screen.getByRole( 'button', { name: 'Save' } ) ).toBeEnabled();
+	} );
+
+	it( 'should disable the save button when site name is empty', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const siteNameInput = screen.getByLabelText( 'Site name' );
+		await user.clear( siteNameInput );
+
+		expect( screen.getByRole( 'button', { name: 'Save' } ) ).toBeDisabled();
+	} );
+
+	it( 'should update site when save button is clicked with changed site name', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const siteNameInput = screen.getByLabelText( 'Site name' );
+		await user.clear( siteNameInput );
+		await user.type( siteNameInput, 'New Site Name' );
+
+		await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
+
+		expect( mockUpdateSite ).toHaveBeenCalledWith( {
+			id: 'site-123',
+			name: 'New Site Name',
+			phpVersion: '8.0',
+			running: true,
+		} );
+		expect( defaultProps.onSave ).toHaveBeenCalled();
+	} );
+
+	it( 'should update site and restart server when PHP version is changed', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const phpVersionSelect = screen.getByLabelText( 'PHP version' );
+		await user.selectOptions( phpVersionSelect, '8.2' );
+
+		await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
+
+		expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' );
+		expect( mockUpdateSite ).toHaveBeenCalledWith( {
+			id: 'site-123',
+			name: 'Test Site',
+			phpVersion: '8.2',
+			running: true,
+		} );
+		expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' );
+		expect( defaultProps.onSave ).toHaveBeenCalled();
+	} );
+
+	it( 'should update WordPress version when changed', async () => {
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
+		await user.selectOptions( wpVersionSelect, '6.4' );
+
+		await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
+
+		expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' );
+		expect( mockExecuteWPCLiInline ).toHaveBeenCalledWith( {
+			siteId: 'site-123',
+			args: 'core update --version=6.4 --force',
+			skipPluginsAndThemes: true,
+		} );
+		expect( mockUpdateSite ).toHaveBeenCalledWith( {
+			id: 'site-123',
+			name: 'Test Site',
+			phpVersion: '8.0',
+			running: true,
+		} );
+		expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' );
+		expect( defaultProps.onSave ).toHaveBeenCalled();
+	} );
+
+	it( 'should show error when WordPress version update fails', async () => {
+		mockExecuteWPCLiInline.mockResolvedValue( { exitCode: 1, stderr: 'Update failed' } );
+
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
+		await user.selectOptions( wpVersionSelect, '6.4' );
+
+		await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
+
+		await waitFor( () => {
+			expect( mockShowErrorMessageBox ).toHaveBeenCalledWith( {
+				title: 'Error changing WordPress version',
+				message: 'Update failed',
+			} );
+		} );
+
+		expect( screen.getByText( 'Error changing WordPress version' ) ).toBeInTheDocument();
+	} );
+
+	it( 'should disable form controls when site is being edited', async () => {
+		// Mock the updateSite to delay completion
+		mockUpdateSite.mockImplementation(
+			() => new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+		);
+
+		render( <EditSiteDetails { ...defaultProps } /> );
+		const user = userEvent.setup();
+
+		await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) );
+
+		const siteNameInput = screen.getByLabelText( 'Site name' );
+		await user.clear( siteNameInput );
+		await user.type( siteNameInput, 'New Site Name' );
+
+		const saveButton = screen.getByRole( 'button', { name: 'Save' } );
+		await user.click( saveButton );
+
+		// Check that controls are disabled during save
+		expect( screen.getByRole( 'button', { name: 'Saving…' } ) ).toBeInTheDocument();
+		expect( screen.getByLabelText( 'Site name' ) ).toBeDisabled();
+		expect( screen.getByLabelText( 'PHP version' ) ).toBeDisabled();
+		expect( screen.getByLabelText( 'WordPress version' ) ).toBeDisabled();
+		expect( screen.getByRole( 'button', { name: 'Cancel' } ) ).toBeDisabled();
+
+		// Wait for the update to complete
+		await waitFor( () => {
+			expect( mockUpdateSite ).toHaveBeenCalled();
+		} );
+	} );
+} );
diff --git a/tailwind.config.js b/tailwind.config.js
index f22eafb906..9caebfa03b 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -133,6 +133,9 @@ a8cToTailwindColors[ `${ PREFIX }-blueberry-5` ] = '#F7F8FE'; // WordPress Blue
 a8cToTailwindColors[ `${ PREFIX }-blueberry` ] = '#3858E9'; // WordPress Blue
 a8cToTailwindColors[ `${ PREFIX }-blueberry-70` ] = '#1d35b4'; // WordPress Blue 70
 a8cToTailwindColors[ `${ PREFIX }-gray-700` ] = '#757575'; // Gray 700
+a8cToTailwindColors[ `${ PREFIX }-gray-400` ] = '#CCC'; // Gray 400
+a8cToTailwindColors[ `${ PREFIX }-gray-600` ] = '#949494'; // Gray 600
+a8cToTailwindColors[ `${ PREFIX }-gray-100` ] = '#f0f0f0'; // Gray 100
 
 module.exports = {
 	content: [ './src/**/*.{html,ejs,js,jsx,ts,tsx}' ],


Reviews:
user: login: ivan-ottinger
body: The proposed changes look good. I found them clean and easy to follow.

Thank you for including detailed test instructions together with screen recordings.

The switching of the WordPress version works well, including the site name and PHP version. I did not observe any regressions. Error reporting works as expected too.

---

> We display an error message when the wp version update fails. An easy way to reproduce is updating a beta in a WP site in "Spanish": https://github.com/Automattic/dotcom-forge/issues/10568

When it comes to testing with different languages, one thing I did not realize initially is that the site itself has to be created in that other language first (or the WordPress language needs to be set first to that other language).

Otherwise (if the language setting in WordPress is already in English), even if the Studio UI is in other language, the WordPress version switch to beta will go through without any issues (since it is switching to English beta version).

---

One small thing we could consider to include in this PR: Updating the styling of the site name field and the closing `X` to ensure it is clear they are disabled during the update process:

![Markup on 2025-03-06 at 10:16:02](https://github.com/user-attachments/assets/c129a2d3-9c14-4eec-bde4-bdb61f662a1f)

---

Anyways, great work! 👌🏼🙂 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:32:03Z
user: login: katinthehatsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:36:55Z
user: login: katinthehatsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:48:30Z
user: login: katinthehatsite
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:48:46Z
user: login: sejas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:05:41Z
user: login: sejas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:05:48Z
user: login: sejas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:07:02Z
user: login: sejas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:16:23Z

Review comments:
diff_hunk: @@ -83,13 +120,14 @@ export default function EditSiteDetails() {
 					title={ __( 'Edit site' ) }
 					isDismissible
 					focusOnMount="firstContentElement"
-					onRequestClose={ resetLocalState }
+					onRequestClose={ closeModal }
 				>
 					<form onSubmit={ onSiteEdit }>
 						<div className="flex flex-col gap-6">
 							<label className="flex flex-col gap-1.5 leading-4">
 								<span className="font-semibold">{ __( 'Site name' ) }</span>
 								<TextControlComponent
+									disabled={ isEditingSite }
path: src/modules/site-settings/edit-site-details.tsx
user: login: katinthehatsite
body: Let's update the styles to also look greyed out on this one so that it corresponds to the design of PHP and WordPress versions fields
created_at: 2025-03-06T09:36:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,79 +1,116 @@
 import { SupportedPHPVersion, SupportedPHPVersions } from '@php-wasm/universal';
 import { SelectControl } from '@wordpress/components';
 import { useI18n } from '@wordpress/react-i18n';
-import { FormEvent, useCallback, useEffect, useState } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import stripAnsi from 'strip-ansi';
 import Button from 'src/components/button';
 import Modal from 'src/components/modal';
 import TextControlComponent from 'src/components/text-control';
 import { useSiteDetails } from 'src/hooks/use-site-details';
+import { getIpcApi } from 'src/lib/get-ipc-api';
 import { useRootSelector } from 'src/stores';
 import { wordpressVersionsSelectors } from 'src/stores/wordpress-versions-slice';
 import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';
 
-export default function EditSiteDetails() {
+type EditSiteDetailsProps = {
+	currentWpVersion: string;
+	onSave: () => void;
+};
+
+export default function EditSiteDetails( { currentWpVersion, onSave }: EditSiteDetailsProps ) {
 	const { __ } = useI18n();
 	const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
 	const [ editSiteError, setEditSiteError ] = useState( '' );
 	const [ showModal, setShowModal ] = useState( false );
 	const [ isEditingSite, setIsEditingSite ] = useState( false );
+	const closeModal = useCallback( () => {
+		if ( isEditingSite ) {
+			return;
+		}
+		setShowModal( false );
+	}, [ isEditingSite ] );
 	const [ siteName, setSiteName ] = useState( selectedSite?.name ?? '' );
 	const [ selectedPhpVersion, setSelectedPhpVersion ] = useState< SupportedPHPVersion >(
 		( selectedSite?.phpVersion as SupportedPHPVersion ) ?? DEFAULT_PHP_VERSION
 	);
-	const [ selectedWpVersion, setSelectedWpVersion ] = useState( 'latest' );
-
+	const [ selectedWpVersion, setSelectedWpVersion ] = useState( currentWpVersion );
 	const wordpressVersions = useRootSelector( wordpressVersionsSelectors.selectWordPressVersions );
+	const wordpressVersionOptions = wordpressVersions.map( ( version ) => ( {
+		label: version.label,
+		value: version.value,
+	} ) );
+	if ( ! wordpressVersionOptions.some( ( version ) => version.value === currentWpVersion ) ) {
+		wordpressVersionOptions.push( { label: currentWpVersion, value: currentWpVersion } );
+	}
 
-	useEffect( () => {
-		if ( selectedSite && showModal ) {
-			setSiteName( selectedSite.name );
-			setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+	const resetFormState = useCallback( () => {
+		if ( ! selectedSite ) {
+			return;
 		}
-	}, [ selectedSite, showModal ] );
+		setSiteName( selectedSite.name );
+		setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+		setSelectedWpVersion( currentWpVersion );
+		setEditSiteError( '' );
+	}, [ currentWpVersion, selectedSite ] );
 
-	const resetLocalState = useCallback( () => {
-		setShowModal( false );
-		setSiteName( '' );
-		setSelectedPhpVersion( DEFAULT_PHP_VERSION );
-		setSelectedWpVersion( 'latest' );
+	const onSiteEdit = async ( event: FormEvent ) => {
+		event.preventDefault();
+		if ( ! selectedSite?.id ) {
+			return;
+		}
+		setIsEditingSite( true );
 		setEditSiteError( '' );
-	}, [] );
+		try {
+			const running = selectedSite.running;
 
-	const onSiteEdit = useCallback(
-		async ( event: FormEvent ) => {
-			event.preventDefault();
-			if ( ! selectedSite ) {
-				return;
+			const hasWpVersionChanged = selectedWpVersion !== currentWpVersion;
+			const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
+			const needsRestart = running && ( hasWpVersionChanged || hasPhpVersionChanged );
+			if ( needsRestart ) {
+				await stopServer( selectedSite.id );
 			}
-			setIsEditingSite( true );
-			try {
-				const running = selectedSite.running;
-				await updateSite( {
-					...selectedSite,
-					name: siteName,
-					phpVersion: selectedPhpVersion,
-				} );
-				if ( running && selectedSite.phpVersion !== selectedPhpVersion ) {
-					await stopServer( selectedSite.id );
-					await startServer( selectedSite.id );
+
+			if ( hasWpVersionChanged ) {
+				try {
+					const result = await getIpcApi().executeWPCLiInline( {
+						siteId: selectedSite.id,
+						args: `core update --version=${ selectedWpVersion } --force`,
+						skipPluginsAndThemes: true,
+					} );
+					if ( result.exitCode !== 0 ) {
+						throw new Error( result.stderr );
+					}
+				} catch ( wpError ) {
+					console.error( 'Error updating WordPress version:', wpError );
+					const errorMessage = stripAnsi( ( wpError as Error )?.message );
+					setEditSiteError( __( 'Error updating WordPress version' ) );
path: src/modules/site-settings/edit-site-details.tsx
user: login: katinthehatsite
body: ```suggestion
					setEditSiteError( __( 'Error changing WordPress version.' ) );
```
created_at: 2025-03-06T09:48:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,79 +1,116 @@
 import { SupportedPHPVersion, SupportedPHPVersions } from '@php-wasm/universal';
 import { SelectControl } from '@wordpress/components';
 import { useI18n } from '@wordpress/react-i18n';
-import { FormEvent, useCallback, useEffect, useState } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import stripAnsi from 'strip-ansi';
 import Button from 'src/components/button';
 import Modal from 'src/components/modal';
 import TextControlComponent from 'src/components/text-control';
 import { useSiteDetails } from 'src/hooks/use-site-details';
+import { getIpcApi } from 'src/lib/get-ipc-api';
 import { useRootSelector } from 'src/stores';
 import { wordpressVersionsSelectors } from 'src/stores/wordpress-versions-slice';
 import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';
 
-export default function EditSiteDetails() {
+type EditSiteDetailsProps = {
+	currentWpVersion: string;
+	onSave: () => void;
+};
+
+export default function EditSiteDetails( { currentWpVersion, onSave }: EditSiteDetailsProps ) {
 	const { __ } = useI18n();
 	const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
 	const [ editSiteError, setEditSiteError ] = useState( '' );
 	const [ showModal, setShowModal ] = useState( false );
 	const [ isEditingSite, setIsEditingSite ] = useState( false );
+	const closeModal = useCallback( () => {
+		if ( isEditingSite ) {
+			return;
+		}
+		setShowModal( false );
+	}, [ isEditingSite ] );
 	const [ siteName, setSiteName ] = useState( selectedSite?.name ?? '' );
 	const [ selectedPhpVersion, setSelectedPhpVersion ] = useState< SupportedPHPVersion >(
 		( selectedSite?.phpVersion as SupportedPHPVersion ) ?? DEFAULT_PHP_VERSION
 	);
-	const [ selectedWpVersion, setSelectedWpVersion ] = useState( 'latest' );
-
+	const [ selectedWpVersion, setSelectedWpVersion ] = useState( currentWpVersion );
 	const wordpressVersions = useRootSelector( wordpressVersionsSelectors.selectWordPressVersions );
+	const wordpressVersionOptions = wordpressVersions.map( ( version ) => ( {
+		label: version.label,
+		value: version.value,
+	} ) );
+	if ( ! wordpressVersionOptions.some( ( version ) => version.value === currentWpVersion ) ) {
+		wordpressVersionOptions.push( { label: currentWpVersion, value: currentWpVersion } );
+	}
 
-	useEffect( () => {
-		if ( selectedSite && showModal ) {
-			setSiteName( selectedSite.name );
-			setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+	const resetFormState = useCallback( () => {
+		if ( ! selectedSite ) {
+			return;
 		}
-	}, [ selectedSite, showModal ] );
+		setSiteName( selectedSite.name );
+		setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+		setSelectedWpVersion( currentWpVersion );
+		setEditSiteError( '' );
+	}, [ currentWpVersion, selectedSite ] );
 
-	const resetLocalState = useCallback( () => {
-		setShowModal( false );
-		setSiteName( '' );
-		setSelectedPhpVersion( DEFAULT_PHP_VERSION );
-		setSelectedWpVersion( 'latest' );
+	const onSiteEdit = async ( event: FormEvent ) => {
+		event.preventDefault();
+		if ( ! selectedSite?.id ) {
+			return;
+		}
+		setIsEditingSite( true );
 		setEditSiteError( '' );
-	}, [] );
+		try {
+			const running = selectedSite.running;
 
-	const onSiteEdit = useCallback(
-		async ( event: FormEvent ) => {
-			event.preventDefault();
-			if ( ! selectedSite ) {
-				return;
+			const hasWpVersionChanged = selectedWpVersion !== currentWpVersion;
+			const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
+			const needsRestart = running && ( hasWpVersionChanged || hasPhpVersionChanged );
+			if ( needsRestart ) {
+				await stopServer( selectedSite.id );
 			}
-			setIsEditingSite( true );
-			try {
-				const running = selectedSite.running;
-				await updateSite( {
-					...selectedSite,
-					name: siteName,
-					phpVersion: selectedPhpVersion,
-				} );
-				if ( running && selectedSite.phpVersion !== selectedPhpVersion ) {
-					await stopServer( selectedSite.id );
-					await startServer( selectedSite.id );
+
+			if ( hasWpVersionChanged ) {
+				try {
+					const result = await getIpcApi().executeWPCLiInline( {
+						siteId: selectedSite.id,
+						args: `core update --version=${ selectedWpVersion } --force`,
+						skipPluginsAndThemes: true,
+					} );
+					if ( result.exitCode !== 0 ) {
+						throw new Error( result.stderr );
+					}
+				} catch ( wpError ) {
+					console.error( 'Error updating WordPress version:', wpError );
+					const errorMessage = stripAnsi( ( wpError as Error )?.message );
+					setEditSiteError( __( 'Error updating WordPress version' ) );
+					getIpcApi().showErrorMessageBox( {
+						title: __( 'Error updating WordPress version' ),
path: src/modules/site-settings/edit-site-details.tsx
user: login: katinthehatsite
body: ```suggestion
						title: __( 'Error changing WordPress version' ),
```
created_at: 2025-03-06T09:48:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,79 +1,116 @@
 import { SupportedPHPVersion, SupportedPHPVersions } from '@php-wasm/universal';
 import { SelectControl } from '@wordpress/components';
 import { useI18n } from '@wordpress/react-i18n';
-import { FormEvent, useCallback, useEffect, useState } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import stripAnsi from 'strip-ansi';
 import Button from 'src/components/button';
 import Modal from 'src/components/modal';
 import TextControlComponent from 'src/components/text-control';
 import { useSiteDetails } from 'src/hooks/use-site-details';
+import { getIpcApi } from 'src/lib/get-ipc-api';
 import { useRootSelector } from 'src/stores';
 import { wordpressVersionsSelectors } from 'src/stores/wordpress-versions-slice';
 import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';
 
-export default function EditSiteDetails() {
+type EditSiteDetailsProps = {
+	currentWpVersion: string;
+	onSave: () => void;
+};
+
+export default function EditSiteDetails( { currentWpVersion, onSave }: EditSiteDetailsProps ) {
 	const { __ } = useI18n();
 	const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
 	const [ editSiteError, setEditSiteError ] = useState( '' );
 	const [ showModal, setShowModal ] = useState( false );
 	const [ isEditingSite, setIsEditingSite ] = useState( false );
+	const closeModal = useCallback( () => {
+		if ( isEditingSite ) {
+			return;
+		}
+		setShowModal( false );
+	}, [ isEditingSite ] );
 	const [ siteName, setSiteName ] = useState( selectedSite?.name ?? '' );
 	const [ selectedPhpVersion, setSelectedPhpVersion ] = useState< SupportedPHPVersion >(
 		( selectedSite?.phpVersion as SupportedPHPVersion ) ?? DEFAULT_PHP_VERSION
 	);
-	const [ selectedWpVersion, setSelectedWpVersion ] = useState( 'latest' );
-
+	const [ selectedWpVersion, setSelectedWpVersion ] = useState( currentWpVersion );
 	const wordpressVersions = useRootSelector( wordpressVersionsSelectors.selectWordPressVersions );
+	const wordpressVersionOptions = wordpressVersions.map( ( version ) => ( {
+		label: version.label,
+		value: version.value,
+	} ) );
+	if ( ! wordpressVersionOptions.some( ( version ) => version.value === currentWpVersion ) ) {
+		wordpressVersionOptions.push( { label: currentWpVersion, value: currentWpVersion } );
+	}
 
-	useEffect( () => {
-		if ( selectedSite && showModal ) {
-			setSiteName( selectedSite.name );
-			setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+	const resetFormState = useCallback( () => {
+		if ( ! selectedSite ) {
+			return;
 		}
-	}, [ selectedSite, showModal ] );
+		setSiteName( selectedSite.name );
+		setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+		setSelectedWpVersion( currentWpVersion );
+		setEditSiteError( '' );
+	}, [ currentWpVersion, selectedSite ] );
 
-	const resetLocalState = useCallback( () => {
-		setShowModal( false );
-		setSiteName( '' );
-		setSelectedPhpVersion( DEFAULT_PHP_VERSION );
-		setSelectedWpVersion( 'latest' );
+	const onSiteEdit = async ( event: FormEvent ) => {
+		event.preventDefault();
+		if ( ! selectedSite?.id ) {
+			return;
+		}
+		setIsEditingSite( true );
 		setEditSiteError( '' );
-	}, [] );
+		try {
+			const running = selectedSite.running;
 
-	const onSiteEdit = useCallback(
-		async ( event: FormEvent ) => {
-			event.preventDefault();
-			if ( ! selectedSite ) {
-				return;
+			const hasWpVersionChanged = selectedWpVersion !== currentWpVersion;
+			const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
+			const needsRestart = running && ( hasWpVersionChanged || hasPhpVersionChanged );
+			if ( needsRestart ) {
+				await stopServer( selectedSite.id );
 			}
-			setIsEditingSite( true );
-			try {
-				const running = selectedSite.running;
-				await updateSite( {
-					...selectedSite,
-					name: siteName,
-					phpVersion: selectedPhpVersion,
-				} );
-				if ( running && selectedSite.phpVersion !== selectedPhpVersion ) {
-					await stopServer( selectedSite.id );
-					await startServer( selectedSite.id );
+
+			if ( hasWpVersionChanged ) {
+				try {
+					const result = await getIpcApi().executeWPCLiInline( {
+						siteId: selectedSite.id,
+						args: `core update --version=${ selectedWpVersion } --force`,
+						skipPluginsAndThemes: true,
+					} );
+					if ( result.exitCode !== 0 ) {
+						throw new Error( result.stderr );
+					}
+				} catch ( wpError ) {
+					console.error( 'Error updating WordPress version:', wpError );
+					const errorMessage = stripAnsi( ( wpError as Error )?.message );
+					setEditSiteError( __( 'Error updating WordPress version' ) );
+					getIpcApi().showErrorMessageBox( {
+						title: __( 'Error updating WordPress version' ),
path: src/modules/site-settings/edit-site-details.tsx
user: login: sejas
body: Done! https://github.com/Automattic/studio/pull/1011/commits/361f1e8f7f0d5bcf60f50bdba6e7c4aa5f4deb98
created_at: 2025-03-06T14:05:41Z
author_association: MEMBER
diff_hunk: @@ -1,79 +1,116 @@
 import { SupportedPHPVersion, SupportedPHPVersions } from '@php-wasm/universal';
 import { SelectControl } from '@wordpress/components';
 import { useI18n } from '@wordpress/react-i18n';
-import { FormEvent, useCallback, useEffect, useState } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import stripAnsi from 'strip-ansi';
 import Button from 'src/components/button';
 import Modal from 'src/components/modal';
 import TextControlComponent from 'src/components/text-control';
 import { useSiteDetails } from 'src/hooks/use-site-details';
+import { getIpcApi } from 'src/lib/get-ipc-api';
 import { useRootSelector } from 'src/stores';
 import { wordpressVersionsSelectors } from 'src/stores/wordpress-versions-slice';
 import { DEFAULT_PHP_VERSION } from 'vendor/wp-now/src/constants';
 
-export default function EditSiteDetails() {
+type EditSiteDetailsProps = {
+	currentWpVersion: string;
+	onSave: () => void;
+};
+
+export default function EditSiteDetails( { currentWpVersion, onSave }: EditSiteDetailsProps ) {
 	const { __ } = useI18n();
 	const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
 	const [ editSiteError, setEditSiteError ] = useState( '' );
 	const [ showModal, setShowModal ] = useState( false );
 	const [ isEditingSite, setIsEditingSite ] = useState( false );
+	const closeModal = useCallback( () => {
+		if ( isEditingSite ) {
+			return;
+		}
+		setShowModal( false );
+	}, [ isEditingSite ] );
 	const [ siteName, setSiteName ] = useState( selectedSite?.name ?? '' );
 	const [ selectedPhpVersion, setSelectedPhpVersion ] = useState< SupportedPHPVersion >(
 		( selectedSite?.phpVersion as SupportedPHPVersion ) ?? DEFAULT_PHP_VERSION
 	);
-	const [ selectedWpVersion, setSelectedWpVersion ] = useState( 'latest' );
-
+	const [ selectedWpVersion, setSelectedWpVersion ] = useState( currentWpVersion );
 	const wordpressVersions = useRootSelector( wordpressVersionsSelectors.selectWordPressVersions );
+	const wordpressVersionOptions = wordpressVersions.map( ( version ) => ( {
+		label: version.label,
+		value: version.value,
+	} ) );
+	if ( ! wordpressVersionOptions.some( ( version ) => version.value === currentWpVersion ) ) {
+		wordpressVersionOptions.push( { label: currentWpVersion, value: currentWpVersion } );
+	}
 
-	useEffect( () => {
-		if ( selectedSite && showModal ) {
-			setSiteName( selectedSite.name );
-			setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+	const resetFormState = useCallback( () => {
+		if ( ! selectedSite ) {
+			return;
 		}
-	}, [ selectedSite, showModal ] );
+		setSiteName( selectedSite.name );
+		setSelectedPhpVersion( selectedSite.phpVersion as SupportedPHPVersion );
+		setSelectedWpVersion( currentWpVersion );
+		setEditSiteError( '' );
+	}, [ currentWpVersion, selectedSite ] );
 
-	const resetLocalState = useCallback( () => {
-		setShowModal( false );
-		setSiteName( '' );
-		setSelectedPhpVersion( DEFAULT_PHP_VERSION );
-		setSelectedWpVersion( 'latest' );
+	const onSiteEdit = async ( event: FormEvent ) => {
+		event.preventDefault();
+		if ( ! selectedSite?.id ) {
+			return;
+		}
+		setIsEditingSite( true );
 		setEditSiteError( '' );
-	}, [] );
+		try {
+			const running = selectedSite.running;
 
-	const onSiteEdit = useCallback(
-		async ( event: FormEvent ) => {
-			event.preventDefault();
-			if ( ! selectedSite ) {
-				return;
+			const hasWpVersionChanged = selectedWpVersion !== currentWpVersion;
+			const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
+			const needsRestart = running && ( hasWpVersionChanged || hasPhpVersionChanged );
+			if ( needsRestart ) {
+				await stopServer( selectedSite.id );
 			}
-			setIsEditingSite( true );
-			try {
-				const running = selectedSite.running;
-				await updateSite( {
-					...selectedSite,
-					name: siteName,
-					phpVersion: selectedPhpVersion,
-				} );
-				if ( running && selectedSite.phpVersion !== selectedPhpVersion ) {
-					await stopServer( selectedSite.id );
-					await startServer( selectedSite.id );
+
+			if ( hasWpVersionChanged ) {
+				try {
+					const result = await getIpcApi().executeWPCLiInline( {
+						siteId: selectedSite.id,
+						args: `core update --version=${ selectedWpVersion } --force`,
+						skipPluginsAndThemes: true,
+					} );
+					if ( result.exitCode !== 0 ) {
+						throw new Error( result.stderr );
+					}
+				} catch ( wpError ) {
+					console.error( 'Error updating WordPress version:', wpError );
+					const errorMessage = stripAnsi( ( wpError as Error )?.message );
+					setEditSiteError( __( 'Error updating WordPress version' ) );
path: src/modules/site-settings/edit-site-details.tsx
user: login: sejas
body: Done https://github.com/Automattic/studio/pull/1011/commits/361f1e8f7f0d5bcf60f50bdba6e7c4aa5f4deb98
created_at: 2025-03-06T14:05:47Z
author_association: MEMBER
diff_hunk: @@ -83,13 +120,14 @@ export default function EditSiteDetails() {
 					title={ __( 'Edit site' ) }
 					isDismissible
 					focusOnMount="firstContentElement"
-					onRequestClose={ resetLocalState }
+					onRequestClose={ closeModal }
 				>
 					<form onSubmit={ onSiteEdit }>
 						<div className="flex flex-col gap-6">
 							<label className="flex flex-col gap-1.5 leading-4">
 								<span className="font-semibold">{ __( 'Site name' ) }</span>
 								<TextControlComponent
+									disabled={ isEditingSite }
path: src/modules/site-settings/edit-site-details.tsx
user: login: sejas
body: Done! https://github.com/Automattic/studio/pull/1011/commits/0ef921261adb21035f76e372d9be39d5296df2ad https://github.com/Automattic/studio/pull/1011/commits/7f5533ff50ecbeb26b4b352e5a76b27f700b41d5
created_at: 2025-03-06T14:07:02Z
author_association: MEMBER
diff_hunk: @@ -271,4 +271,8 @@ blockquote {
 
 .error-message {
 	color: #D63638;
+}
+
+.error-select-control .components-input-control__backdrop {
+	border-color: theme( 'colors.red.500' )!important;
path: src/index.css
user: login: sejas
body: I had to add a CSS style because I couldn't target the WP component directly using Tailwind.

✅ `className="[&_.components-input-control\_\_backdrop]:!border-red-500"`
❌ `className={ cx( error && '[&_.components-input-control\_\_backdrop]:!border-red-500')}`
❌ `className={ cx( error && '[&_.components-input-control__backdrop]:!border-red-500')}`
❌ `className={ '[&_.components-input-control\_\_backdrop]:!border-red-500' }`
❌ `className={ '[&_.components-input-control__backdrop]:!border-red-500' }`
created_at: 2025-03-06T14:16:23Z
author_association: MEMBER

PR comments:
user: login: katinthehatsite
created_at: 2025-03-06T09:47:45Z
body: Let's also slightly adjust the design of the error in terms of spacing and size. We can use the following warning as an example for styling:

<img width="472" alt="Screenshot 2025-03-06 at 10 46 31 AM" src="https://github.com/user-attachments/assets/44b86723-88a3-46bd-8057-103bcdfb58e4" />

We can also add the warning icon there. 

Finally, let's change the wording to `Error changing WP version` as the user might not only update it to higher version but also might downgrade. 
author_association: CONTRIBUTOR
user: login: sejas
created_at: 2025-03-06T14:13:22Z
body: @katinthehatsite , @ivan-ottinger , I've applied your feedback and updated the styles when there is an error and when editing is in progress. Let me know what you think.

<img width="1012" alt="Screenshot 2025-03-06 at 13 54 59" src="https://github.com/user-attachments/assets/c663c3e3-b114-48d3-8ae7-78966484c7d3" />


<img width="1012" alt="Screenshot 2025-03-06 at 13 22 02" src="https://github.com/user-attachments/assets/b9aa3db4-cac8-4a4b-8e76-c623168a4333" />


author_association: MEMBER
user: login: ivan-ottinger
created_at: 2025-03-06T14:37:51Z
body: > @katinthehatsite , @ivan-ottinger , I've applied your feedback and updated the styles when there is an error and when editing is in progress. Let me know what you think.

Nice! The changes look good:

| Updating | Error |
|--------|--------|
| ![Markup on 2025-03-06 at 15:33:12](https://github.com/user-attachments/assets/3c0731fa-4477-403f-bf15-f84552e71f2d) | ![Markup on 2025-03-06 at 15:34:16](https://github.com/user-attachments/assets/d782f032-8677-432a-bc8e-7fd1252a321d) | 

author_association: CONTRIBUTOR
user: login: youknowriad
created_at: 2025-03-07T04:51:05Z
body: What's the reason for hiding this behind an environment variable?
author_association: CONTRIBUTOR
user: login: youknowriad
created_at: 2025-03-07T05:01:57Z
body: Also, why "edit-site-details" is not using "site-form",
Also, why don't we have "php version" and "wordpress version" selectors as part of the site creation form?
author_association: CONTRIBUTOR
user: login: wojtekn
created_at: 2025-03-07T08:16:39Z
body: > What's the reason for hiding this behind an environment variable?

We will enable this when the feature is finished, and we are confident about its quality. See https://github.com/orgs/Automattic/projects/543/views/39

> Also, why "edit-site-details" is not using "site-form",

We can call it technical debt. The former was initially added to edit the site name only, but it has now been repurposed. We can think about merging them at some point, especially when we allow to edit the local domain and path.

> Also, why don't we have "php version" and "wordpress version" selectors as part of the site creation form?

See https://github.com/Automattic/dotcom-forge/issues/10531
author_association: CONTRIBUTOR
user: login: fredrikekelund
created_at: 2025-03-07T09:18:30Z
body: This PR made the backup and local path inputs grey in the `Add site` modal. I'm assuming that was unintentional?

<img width="1332" alt="Screenshot 2025-03-07 at 10 17 40" src="https://github.com/user-attachments/assets/f037fe78-8a29-4a06-9443-a2503ebfdddb" />

author_association: CONTRIBUTOR
user: login: katinthehatsite
created_at: 2025-03-07T09:22:11Z
body: @fredrikekelund yeah this looks like not intentional. Let's open an issue and fix it today. I do not think this is released to users as it should have been merged after Studio release. Do you want to open the issue or want me to do it?

Alternatively, feel free to submit a PR with the fix.
author_association: CONTRIBUTOR
user: login: wojtekn
created_at: 2025-03-07T09:23:32Z
body: @fredrikekelund @katinthehatsite it looks okay in 1.3.7. I reproduced it in the trunk, though.
author_association: CONTRIBUTOR
user: login: katinthehatsite
created_at: 2025-03-07T09:46:40Z
body: Added the issue here: https://github.com/Automattic/dotcom-forge/issues/10579
author_association: CONTRIBUTOR