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 ( + + +
+ + + data={ formData } + fields={ fields } + form={ form } + onChange={ ( edits: { version?: string } ) => { + setFormData( ( data ) => ( { ...data, ...edits } ) ); + } } + /> + + + + +
+
+
+ ); + }; + + 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; +}