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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 69 additions & 138 deletions client/dashboard/sites/settings-wordpress/index.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,68 @@
import {
siteBySlugQuery,
siteWordPressVersionQuery,
siteWordPressVersionMutation,
wpOrgCoreVersionQuery,
} from '@automattic/api-queries';
import { useQuery, useSuspenseQuery, useMutation } from '@tanstack/react-query';
import {
__experimentalVStack as VStack,
__experimentalText as Text,
Button,
} from '@wordpress/components';
import { DataForm } from '@wordpress/dataviews';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { __experimentalVStack as VStack, __experimentalText as Text } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { useState } from 'react';
import Breadcrumbs from '../../app/breadcrumbs';
import { NavigationBlocker } from '../../app/navigation-blocker';
import { ButtonStack } from '../../components/button-stack';
import { Card, CardBody } from '../../components/card';
import InlineSupportLink from '../../components/inline-support-link';
import Notice from '../../components/notice';
import { PageHeader } from '../../components/page-header';
import PageLayout from '../../components/page-layout';
import { formatWordPressVersion, getFormattedWordPressVersion } from '../../utils/wp-version';
import { getFormattedWordPressVersion } from '../../utils/wp-version';
import { canViewWordPressSettings } from '../features';
import type { Field } from '@wordpress/dataviews';
import { useVersionSwitch } from './use-version-switch';
import { VersionForm } from './version-form';
import { VersionSwitchNotice } from './version-switch-notice';

export default function WordPressSettings( { siteSlug }: { siteSlug: string } ) {
function WordPressSettingsForm( { siteSlug }: { siteSlug: string } ) {
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );
const canView = canViewWordPressSettings( site );

const { data: currentVersion } = useQuery( {
...siteWordPressVersionQuery( site.ID ),
enabled: canView,
} );

const { data: latestVersion } = useQuery( {
...wpOrgCoreVersionQuery(),
enabled: canView,
} );
const { data: betaVersion } = useQuery( {
...wpOrgCoreVersionQuery( 'beta' ),
enabled: canView,
} );

const mutation = useMutation( {
...siteWordPressVersionMutation( site.ID ),
meta: {
snackbar: {
success: __( 'WordPress version saved.' ),
error: __( 'Failed to save WordPress version.' ),
},
},
} );
const { data: currentVersion } = useQuery( siteWordPressVersionQuery( site.ID ) );
const versionSwitch = useVersionSwitch( site );
const { isSwitching, isSwitched, backupState, targetVersion } = versionSwitch;

const [ formData, setFormData ] = useState< { version: string } >( {
version: currentVersion ?? '',
} );
// Resolve the target version tag (e.g. "beta") to a display string (e.g. "7.0-RC2").
const { data: latestVersion = '' } = useQuery( wpOrgCoreVersionQuery() );
const { data: betaVersion = '' } = useQuery( wpOrgCoreVersionQuery( 'beta' ) );

const currentWpVersion = site.options?.software_version ?? '';

const fields: Field< { version: string } >[] = [
{
id: 'version',
label: __( 'WordPress version' ),
Edit: 'select',
elements: [
{
value: 'latest',
label: formatWordPressVersion( latestVersion ?? currentWpVersion, 'latest', true ),
},
{
value: 'beta',
label: formatWordPressVersion( betaVersion ?? currentWpVersion, 'beta', true ),
},
],
},
];

const form = {
layout: { type: 'regular' as const },
fields: [ 'version' ],
};

const isDirty = formData.version !== currentVersion;
const { isPending } = mutation;
return (
<PageLayout
size="small"
header={
<PageHeader
prefix={ <Breadcrumbs length={ 2 } /> }
title="WordPress"
description={ __( 'Manage your WordPress version.' ) }
/>
}
notices={
isSwitching || isSwitched ? (
<VersionSwitchNotice
backupState={ backupState }
targetVersion={ targetVersion === 'beta' ? betaVersion : latestVersion }
currentWpVersion={ site.options?.software_version ?? '' }
isVersionSwitched={ isSwitched }
/>
) : undefined
}
>
<VersionForm
site={ site }
currentVersion={ currentVersion }
versionSwitch={ versionSwitch }
/>
</PageLayout>
);
}

const handleSubmit = ( e: React.FormEvent ) => {
e.preventDefault();
mutation.mutate( formData.version );
};
export default function WordPressSettings( { siteSlug }: { siteSlug: string } ) {
const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) );

if ( ! canView ) {
return (
<PageLayout
size="small"
header={
<PageHeader
prefix={ <Breadcrumbs length={ 2 } /> }
title="WordPress"
description={ __( 'Manage your WordPress version.' ) }
/>
}
>
<Notice>
<VStack>
<Text as="p">
{ sprintf(
// translators: %s: WordPress version, e.g. 6.8
__( 'Every WordPress.com site runs the latest WordPress version (%s).' ),
getFormattedWordPressVersion( site )
) }
</Text>
{ site.is_wpcom_atomic && (
<Text as="p">
{ createInterpolateElement(
__(
'Switch to a staging site to test a beta version of the next WordPress release. <learnMoreLink />'
),
{
learnMoreLink: <InlineSupportLink supportContext="switch-to-staging-site" />,
}
) }
</Text>
) }
</VStack>
</Notice>
</PageLayout>
);
if ( canViewWordPressSettings( site ) ) {
return <WordPressSettingsForm siteSlug={ siteSlug } />;
}

return (
Expand All @@ -141,33 +76,29 @@ export default function WordPressSettings( { siteSlug }: { siteSlug: string } )
/>
}
>
<Card>
<CardBody>
<form onSubmit={ handleSubmit }>
<VStack spacing={ 4 }>
<NavigationBlocker shouldBlock={ isDirty } />
<DataForm< { version: string } >
data={ formData }
fields={ fields }
form={ form }
onChange={ ( edits: { version?: string } ) => {
setFormData( ( data ) => ( { ...data, ...edits } ) );
} }
/>
<ButtonStack justify="flex-start">
<Button
variant="primary"
type="submit"
isBusy={ isPending }
disabled={ isPending || ! isDirty }
>
{ __( 'Save' ) }
</Button>
</ButtonStack>
</VStack>
</form>
</CardBody>
</Card>
<Notice>
<VStack>
<Text as="p">
{ sprintf(
// translators: %s: WordPress version, e.g. 6.8
__( 'Every WordPress.com site runs the latest WordPress version (%s).' ),
getFormattedWordPressVersion( site )
) }
</Text>
{ site.is_wpcom_atomic && (
<Text as="p">
{ createInterpolateElement(
__(
'Switch to a staging site to test a beta version of the next WordPress release. <learnMoreLink />'
),
{
learnMoreLink: <InlineSupportLink supportContext="switch-to-staging-site" />,
}
) }
</Text>
) }
</VStack>
</Notice>
</PageLayout>
);
}
86 changes: 86 additions & 0 deletions client/dashboard/sites/settings-wordpress/use-version-switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
queryClient,
siteBackupsQuery,
siteBySlugQuery,
sitePendingWordPressVersionQuery,
siteWordPressVersionQuery,
siteWordPressVersionMutation,
} from '@automattic/api-queries';
import { useQuery, useMutation } from '@tanstack/react-query';
import { usePrevious } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from 'react';
import { useBackupState } from '../backups/use-backup-state';
import type { BackupState } from '../backups/use-backup-state';
import type { Site } from '@automattic/api-core';

export interface VersionSwitchState {
backupState: BackupState;
/** The pending version tag while switching, or the last one after switch completes. */
targetVersion: string;
isSwitching: boolean;
isSwitched: boolean;
mutation: ReturnType< typeof useMutation< void, Error, string > >;
}

export function useVersionSwitch( site: Site ): VersionSwitchState {
const backupState = useBackupState( site.ID );

// Check if there's a pending version switch.
const { data: pendingVersion } = useQuery( sitePendingWordPressVersionQuery( site.ID ) );
const isSwitching = !! pendingVersion;
const wasSwitching = usePrevious( isSwitching );
const [ isSwitched, setIsSwitched ] = useState( false );
const [ targetVersion, setTargetVersion ] = useState( '' );

// Remember the pending version so we can show it in the success notice.
useEffect( () => {
if ( pendingVersion ) {
setTargetVersion( pendingVersion );
}
}, [ pendingVersion ] );

// Track the transition from switching to not switching.
useEffect( () => {
if ( wasSwitching && ! isSwitching ) {
setIsSwitched( true );
queryClient.invalidateQueries( siteWordPressVersionQuery( site.ID ) );
queryClient.invalidateQueries( siteBySlugQuery( site.slug ) );
}
}, [ wasSwitching, isSwitching, site.ID, site.slug ] );

// Poll backups while a version switch is in progress.
useQuery( {
...siteBackupsQuery( site.ID ),
refetchInterval: isSwitching ? 3000 : false,
enabled: isSwitching,
} );

// After backup completes, poll pending version until it clears.
useQuery( {
...sitePendingWordPressVersionQuery( site.ID ),
refetchInterval: isSwitching && backupState.hasRecentlyCompleted ? 5000 : false,
} );

const mutation = useMutation( {
...siteWordPressVersionMutation( site.ID ),
onSuccess: () => {
backupState.setEnqueued( true );
setIsSwitched( false );
queryClient.invalidateQueries( sitePendingWordPressVersionQuery( site.ID ) );
},
meta: {
snackbar: {
error: __( 'Failed to save WordPress version.' ),
},
},
} );
Comment on lines +106 to +114
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Spreading siteWordPressVersionMutation and then declaring onSuccess at the same level silently replaces the mutation's own onSuccess, so queryClient.invalidateQueries( siteWordPressVersionQuery( siteId ) ) defined there is never called.

❌ Incorrect - overrides mutation option callbacks
onSuccess: () => setShowSuccessMessage( true ), // Breaks cache updates!

(api-queries README)

The component-specific side-effects should be passed as a callback to mutate(). Since the callbacks reference hook-internal state, expose a switchVersion wrapper from this hook instead of the raw mutation:

Suggested change
const mutation = useMutation( {
...siteWordPressVersionMutation( site.ID ),
onSuccess: () => {
backupState.setEnqueued( true );
setIsSwitched( false );
queryClient.invalidateQueries( sitePendingWordPressVersionQuery( site.ID ) );
},
meta: {
snackbar: {
error: __( 'Failed to save WordPress version.' ),
},
},
} );
const mutation = useMutation( {
...siteWordPressVersionMutation( site.ID ),
meta: {
snackbar: {
error: __( 'Failed to save WordPress version.' ),
},
},
} );
const switchVersion = ( version: string ) => {
mutation.mutate( version, {
onSuccess: () => {
backupState.setEnqueued( true );
setIsSwitched( false );
},
} );
};

Then update VersionSwitchState to expose switchVersion (and isPending from mutation.isPending) instead of the raw mutation, and call versionSwitch.switchVersion( formData.version ) from VersionForm.


return {
backupState,
targetVersion,
isSwitching,
isSwitched,
mutation,
};
}
Loading
Loading