diff --git a/client/dashboard/app/queries.ts b/client/dashboard/app/queries.ts
index 1d84c30f4b04..c3bccf9aa0cf 100644
--- a/client/dashboard/app/queries.ts
+++ b/client/dashboard/app/queries.ts
@@ -15,6 +15,8 @@ import {
fetchPerformanceInsights,
updateSiteSettings,
restoreSitePlanSoftware,
+ fetchWordPressVersion,
+ updateWordPressVersion,
} from '../data';
import { SITE_FIELDS } from '../data/constants';
import { queryClient } from './query-client';
@@ -70,6 +72,24 @@ export function sitePHPVersionQuery( siteIdOrSlug: string ) {
};
}
+export function siteWordPressVersionQuery( siteSlug: string ) {
+ return {
+ queryKey: [ 'site', siteSlug, 'wp-version' ],
+ queryFn: () => {
+ return fetchWordPressVersion( siteSlug );
+ },
+ };
+}
+
+export function siteWordPressVersionMutation( siteSlug: string ) {
+ return {
+ mutationFn: ( version: string ) => updateWordPressVersion( siteSlug, version ),
+ onSuccess: () => {
+ queryClient.invalidateQueries( { queryKey: [ 'site', siteSlug, 'wp-version' ] } );
+ },
+ };
+}
+
export function domainsQuery() {
return {
queryKey: [ 'domains' ],
diff --git a/client/dashboard/app/router.tsx b/client/dashboard/app/router.tsx
index 20ed133b55f3..2d2d1d9c00c1 100644
--- a/client/dashboard/app/router.tsx
+++ b/client/dashboard/app/router.tsx
@@ -6,6 +6,7 @@ import {
createLazyRoute,
} from '@tanstack/react-router';
import { fetchTwoStep } from '../data';
+import { canUpdateWordPressVersion } from '../sites/settings-wordpress/utils';
import NotFound from './404';
import UnknownError from './500';
import {
@@ -17,6 +18,7 @@ import {
profileQuery,
siteCurrentPlanQuery,
siteEngagementStatsQuery,
+ siteWordPressVersionQuery,
} from './queries';
import { queryClient } from './query-client';
import Root from './root';
@@ -150,6 +152,23 @@ const siteSettingsSubscriptionGiftingRoute = createRoute( {
)
);
+const siteSettingsWordPressRoute = createRoute( {
+ getParentRoute: () => siteRoute,
+ path: 'settings/wordpress',
+ loader: async ( { params: { siteSlug } } ) => {
+ const site = await queryClient.ensureQueryData( siteQuery( siteSlug ) );
+ if ( canUpdateWordPressVersion( site ) ) {
+ return await queryClient.ensureQueryData( siteWordPressVersionQuery( siteSlug ) );
+ }
+ },
+} ).lazy( () =>
+ import( '../sites/settings-wordpress' ).then( ( d ) =>
+ createLazyRoute( 'site-settings-wordpress' )( {
+ component: () => ,
+ } )
+ )
+);
+
const siteSettingsTransferSiteRoute = createRoute( {
getParentRoute: () => siteRoute,
path: 'settings/transfer-site',
@@ -326,6 +345,7 @@ const createRouteTree = ( config: AppConfig ) => {
siteSettingsRoute,
siteSettingsSiteVisibilityRoute,
siteSettingsSubscriptionGiftingRoute,
+ siteSettingsWordPressRoute,
siteSettingsTransferSiteRoute,
] )
);
diff --git a/client/dashboard/data/constants.ts b/client/dashboard/data/constants.ts
index bbd66e332dfa..c031d87625af 100644
--- a/client/dashboard/data/constants.ts
+++ b/client/dashboard/data/constants.ts
@@ -18,6 +18,7 @@ export const SITE_FIELDS = [
'is_coming_soon',
'is_private',
'is_wpcom_atomic',
+ 'is_wpcom_staging_site',
'launch_status',
'site_migration',
'site_owner',
diff --git a/client/dashboard/data/index.ts b/client/dashboard/data/index.ts
index 05a4b12a5885..33f052a5c8d8 100644
--- a/client/dashboard/data/index.ts
+++ b/client/dashboard/data/index.ts
@@ -85,6 +85,26 @@ export const fetchPHPVersion = async ( id: string ): Promise< string | undefined
} );
};
+export const fetchWordPressVersion = async ( siteIdOrSlug: string ): Promise< string > => {
+ return wpcom.req.get( {
+ path: `/sites/${ siteIdOrSlug }/hosting/wp-version`,
+ apiNamespace: 'wpcom/v2',
+ } );
+};
+
+export const updateWordPressVersion = async (
+ siteIdOrSlug: string,
+ version: string
+): Promise< void > => {
+ return wpcom.req.post(
+ {
+ path: `/sites/${ siteIdOrSlug }/hosting/wp-version`,
+ apiNamespace: 'wpcom/v2',
+ },
+ { version }
+ );
+};
+
export const fetchCurrentPlan = async ( siteIdOrSlug: string ): Promise< Plan > => {
const plans: Record< string, Plan > = await wpcom.req.get( {
path: `/sites/${ siteIdOrSlug }/plans`,
diff --git a/client/dashboard/data/types.ts b/client/dashboard/data/types.ts
index d4d97176fa92..7f9d84d1352d 100644
--- a/client/dashboard/data/types.ts
+++ b/client/dashboard/data/types.ts
@@ -91,6 +91,7 @@ export interface Site {
is_coming_soon: boolean;
is_private: boolean;
is_wpcom_atomic: boolean;
+ is_wpcom_staging_site: boolean;
launch_status: string | boolean;
site_migration: {
migration_status: string;
diff --git a/client/dashboard/sites/overview/site-card.tsx b/client/dashboard/sites/overview/site-card.tsx
index 324b4f85f2e5..339876478dda 100644
--- a/client/dashboard/sites/overview/site-card.tsx
+++ b/client/dashboard/sites/overview/site-card.tsx
@@ -12,6 +12,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { sitePHPVersionQuery } from '../../app/queries';
import { TextBlur } from '../../components/text-blur';
import { getSiteStatusLabel } from '../../utils/site-status';
+import { getFormattedWordPressVersion } from '../../utils/wp-version';
import SitePreview from '../site-preview';
import type { Site, Plan } from '../../data/types';
@@ -23,7 +24,9 @@ function PHPVersion( { siteSlug }: { siteSlug: string } ) {
* SiteCard component to display site information in a card format
*/
export default function SiteCard( { site, currentPlan }: { site: Site; currentPlan: Plan } ) {
- const { options, URL: url, is_private, is_wpcom_atomic } = site;
+ const { URL: url, is_private, is_wpcom_atomic } = site;
+ const wpVersion = getFormattedWordPressVersion( site );
+
// If the site is a private A8C site, X-Frame-Options is set to same
// origin.
const iframeDisabled = site.is_a8c && is_private;
@@ -64,11 +67,9 @@ export default function SiteCard( { site, currentPlan }: { site: Site; currentPl
{ getSiteStatusLabel( site ) }
- { ( options?.software_version || is_wpcom_atomic ) && (
+ { ( wpVersion || is_wpcom_atomic ) && (
- { options?.software_version && (
- { options.software_version }
- ) }
+ { wpVersion && { wpVersion } }
{ is_wpcom_atomic && (
diff --git a/client/dashboard/sites/settings-wordpress/index.tsx b/client/dashboard/sites/settings-wordpress/index.tsx
new file mode 100644
index 000000000000..231a4375364d
--- /dev/null
+++ b/client/dashboard/sites/settings-wordpress/index.tsx
@@ -0,0 +1,147 @@
+import { DataForm } from '@automattic/dataviews';
+import { useQuery, useMutation } from '@tanstack/react-query';
+import {
+ Card,
+ CardBody,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+ __experimentalText as Text,
+ Button,
+ Notice,
+} from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+import { createInterpolateElement } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { useState } from 'react';
+import {
+ siteQuery,
+ siteWordPressVersionQuery,
+ siteWordPressVersionMutation,
+} from '../../app/queries';
+import PageLayout from '../../components/page-layout';
+import { getFormattedWordPressVersion } from '../../utils/wp-version';
+import SettingsPageHeader from '../settings-page-header';
+import { canUpdateWordPressVersion } from './utils';
+import type { Field } from '@automattic/dataviews';
+
+export default function WordPressVersionSettings( { siteSlug }: { siteSlug: string } ) {
+ const { data: site } = useQuery( siteQuery( siteSlug ) );
+ const canUpdate = site && canUpdateWordPressVersion( site );
+
+ const { data: version } = useQuery( {
+ ...siteWordPressVersionQuery( siteSlug ),
+ enabled: canUpdate,
+ } );
+ const mutation = useMutation( siteWordPressVersionMutation( siteSlug ) );
+ const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
+
+ const [ formData, setFormData ] = useState< { version: string } >( {
+ version: version ?? '',
+ } );
+
+ if ( ! site ) {
+ return null;
+ }
+
+ const fields: Field< { version: string } >[] = [
+ {
+ id: 'version',
+ label: __( 'WordPress version' ),
+ Edit: 'select',
+ elements: [
+ { value: 'latest', label: getFormattedWordPressVersion( site, 'latest' ) },
+ { value: 'beta', label: getFormattedWordPressVersion( site, 'beta' ) },
+ ],
+ },
+ ];
+
+ const form = {
+ type: 'regular' as const,
+ fields: [ 'version' ],
+ };
+
+ const isDirty = formData.version !== version;
+ const { isPending } = mutation;
+
+ const handleSubmit = ( e: React.FormEvent ) => {
+ e.preventDefault();
+ mutation.mutate( formData.version, {
+ onSuccess: () => {
+ createSuccessNotice( __( 'Settings saved.' ), { type: 'snackbar' } );
+ },
+ onError: () => {
+ createErrorNotice( __( 'Failed to save settings.' ), {
+ type: 'snackbar',
+ } );
+ },
+ } );
+ };
+
+ const renderForm = () => {
+ return (
+
+
+
+
+
+ );
+ };
+
+ const renderNotice = () => {
+ return (
+
+
+ { site.is_wpcom_atomic
+ ? createInterpolateElement(
+ sprintf(
+ // translators: %s: WordPress version, e.g. 6.8
+ __(
+ 'Every WordPress.com site runs the latest WordPress version (%s). For testing purposes, you can switch to the beta version of the next WordPress release on your staging site.'
+ ),
+ getFormattedWordPressVersion( site )
+ ),
+ {
+ // TODO: use correct staging site URL when it's available.
+ // eslint-disable-next-line jsx-a11y/anchor-is-valid
+ a: ,
+ }
+ )
+ : sprintf(
+ // translators: %s: WordPress version, e.g. 6.8
+ __( 'Every WordPress.com site runs the latest WordPress version (%s).' ),
+ getFormattedWordPressVersion( site )
+ ) }
+
+
+ );
+ };
+
+ return (
+
+
+ { canUpdate ? renderForm() : renderNotice() }
+
+ );
+}
diff --git a/client/dashboard/sites/settings-wordpress/summary.tsx b/client/dashboard/sites/settings-wordpress/summary.tsx
new file mode 100644
index 000000000000..39d70699822f
--- /dev/null
+++ b/client/dashboard/sites/settings-wordpress/summary.tsx
@@ -0,0 +1,37 @@
+import { useQuery } from '@tanstack/react-query';
+import { Icon } from '@wordpress/components';
+import { wordpress } from '@wordpress/icons';
+import { siteWordPressVersionQuery } from '../../app/queries';
+import RouterLinkSummaryButton from '../../components/router-link-summary-button';
+import { getFormattedWordPressVersion } from '../../utils/wp-version';
+import { canUpdateWordPressVersion } from './utils';
+import type { Site } from '../../data/types';
+
+export default function WordPressSettingsSummary( { site }: { site: Site } ) {
+ const { data: versionTag } = useQuery( {
+ ...siteWordPressVersionQuery( site.slug ),
+ enabled: canUpdateWordPressVersion( site ),
+ } );
+
+ const wpVersion = getFormattedWordPressVersion( site, versionTag );
+ if ( ! wpVersion ) {
+ return null;
+ }
+
+ const badges = [
+ {
+ text: wpVersion,
+ intent: versionTag === 'beta' ? ( 'warning' as const ) : ( 'success' as const ),
+ },
+ ];
+
+ return (
+ }
+ badges={ badges }
+ />
+ );
+}
diff --git a/client/dashboard/sites/settings-wordpress/utils.ts b/client/dashboard/sites/settings-wordpress/utils.ts
new file mode 100644
index 000000000000..e56d25d53188
--- /dev/null
+++ b/client/dashboard/sites/settings-wordpress/utils.ts
@@ -0,0 +1,5 @@
+import { Site } from '../../data/types';
+
+export function canUpdateWordPressVersion( site: Site ) {
+ return site.is_wpcom_staging_site;
+}
diff --git a/client/dashboard/sites/settings/index.tsx b/client/dashboard/sites/settings/index.tsx
index 9d6fecb841b6..70ba669a716b 100644
--- a/client/dashboard/sites/settings/index.tsx
+++ b/client/dashboard/sites/settings/index.tsx
@@ -10,6 +10,7 @@ import { PageHeader } from '../../components/page-header';
import PageLayout from '../../components/page-layout';
import SiteVisibilitySettingsSummary from '../settings-site-visibility/summary';
import SubscriptionGiftingSettingsSummary from '../settings-subscription-gifting/summary';
+import WordPressSettingsSummary from '../settings-wordpress/summary';
import DangerZone from './danger-zone';
import SiteActions from './site-actions';
@@ -30,6 +31,12 @@ export default function SiteSettings( { siteSlug }: { siteSlug: string } ) {
+ { __( 'Server' ) }
+
+
+
+
+
diff --git a/client/dashboard/utils/wp-version.ts b/client/dashboard/utils/wp-version.ts
new file mode 100644
index 000000000000..f17c134eea71
--- /dev/null
+++ b/client/dashboard/utils/wp-version.ts
@@ -0,0 +1,33 @@
+import { __ } from '@wordpress/i18n';
+import type { Site } from '../data/types';
+
+function getWordPressVersionTagName( versionTag: string ) {
+ if ( versionTag === 'latest' ) {
+ return __( 'Latest' );
+ }
+ if ( versionTag === 'beta' ) {
+ return __( 'Beta' );
+ }
+ return '';
+}
+
+export function getFormattedWordPressVersion(
+ site: Site,
+ versionTag: string | undefined = undefined
+) {
+ let wpVersion = site.options?.software_version;
+ if ( ! wpVersion ) {
+ return '';
+ }
+
+ if ( ! site.is_wpcom_atomic ) {
+ // On Simple sites, the version string has suffix e.g. 6.8.1-alpha-60199
+ wpVersion = wpVersion.split( '-' )[ 0 ];
+ }
+
+ if ( versionTag ) {
+ wpVersion = `${ wpVersion } (${ getWordPressVersionTagName( versionTag ) })`;
+ }
+
+ return wpVersion;
+}