-
Notifications
You must be signed in to change notification settings - Fork 2k
Dashboard v2: Implement WP version settings #103560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find any docs to this endpoint. Can you share if you have a link that I missed? Does it just return Can we get this info and the WP version from a single place? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a whole project for this feature: pesfPa-qt-p2. Yes, the endpoint just return either ![]() I'm really hesitant to change the endpoint (it would break v1 or whatnot) or the core design of this feature for now... porting features to v2 and changing the functionalities should be different activities. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agreed, but we should track what we can improve and/or simplify. I might be missing context, but I think we should consider getting WP info from a single endpoint (probably remove this one and update the calls of v1). Additionally, not for this PR, we might want to revisit the design for this and add some help text about the options. |
||
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`, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Card> | ||
<CardBody> | ||
<form onSubmit={ handleSubmit }> | ||
<VStack spacing={ 4 }> | ||
<DataForm< { version: string } > | ||
data={ formData } | ||
fields={ fields } | ||
form={ form } | ||
onChange={ ( edits: { version?: string } ) => { | ||
setFormData( ( data ) => ( { ...data, ...edits } ) ); | ||
} } | ||
/> | ||
<HStack justify="flex-start"> | ||
<Button | ||
variant="primary" | ||
type="submit" | ||
isBusy={ isPending } | ||
disabled={ isPending || ! isDirty } | ||
> | ||
{ __( 'Save' ) } | ||
</Button> | ||
</HStack> | ||
</VStack> | ||
</form> | ||
</CardBody> | ||
</Card> | ||
); | ||
}; | ||
|
||
const renderNotice = () => { | ||
return ( | ||
<Notice isDismissible={ false }> | ||
<Text> | ||
{ 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 <a>your staging site</a>.' | ||
), | ||
getFormattedWordPressVersion( site ) | ||
), | ||
{ | ||
// TODO: use correct staging site URL when it's available. | ||
// eslint-disable-next-line jsx-a11y/anchor-is-valid | ||
a: <a href="#" />, | ||
} | ||
) | ||
: sprintf( | ||
// translators: %s: WordPress version, e.g. 6.8 | ||
__( 'Every WordPress.com site runs the latest WordPress version (%s).' ), | ||
getFormattedWordPressVersion( site ) | ||
) } | ||
</Text> | ||
</Notice> | ||
); | ||
}; | ||
|
||
return ( | ||
<PageLayout size="small"> | ||
<SettingsPageHeader title="WordPress" /> | ||
{ canUpdate ? renderForm() : renderNotice() } | ||
</PageLayout> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<RouterLinkSummaryButton | ||
to={ `/sites/${ site.slug }/settings/wordpress` } | ||
title="WordPress" | ||
density="medium" | ||
decoration={ <Icon icon={ wordpress } /> } | ||
badges={ badges } | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Site } from '../../data/types'; | ||
|
||
export function canUpdateWordPressVersion( site: Site ) { | ||
return site.is_wpcom_staging_site; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@youknowriad @ellatrix not sure if this is a good pattern (business logic in the router file)... but I'm not sure what the alternative is.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's fine to call API on component level 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me this is a good approach, it ensure the loader works properly. What happens if we call the API without the check? Does the endpoint fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it fails and then the dashboard shows this instead

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also ties back to my suggestion to use
beforeLoad
to protect this route. Because if the route was protected usingbeforeLoad
there won't be any need to do this check here.I've been thinking and maybe the right approach is going to be this
I would love for this to be explored separately as a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand, in this case, the parent
siteSettingsRoute
would need to fetch everything needed for the setting child routes even though we only open one of them (e.g. WordPress version)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, all we need is the "site" object and the "site" object is already loaded as part of the parent route's loader.