Skip to content

Commit 474d76d

Browse files
authored
Hosting Dashboard V2: Add transfer site action to settings (#103543)
* Hosting Dashboard V2: Add transfer site action to settings * Hosting Dashboard V2: Add transfer site route * Redirect to 404 if the user cannot transfer site * Add ConfirmNewOwnerForm and update styles * Change type of field to email for the validation * Update route * Fix types
1 parent 87b432f commit 474d76d

File tree

8 files changed

+214
-0
lines changed

8 files changed

+214
-0
lines changed

client/dashboard/app/router.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ const siteSettingsSubscriptionGiftingRoute = createRoute( {
150150
)
151151
);
152152

153+
const siteSettingsTransferSiteRoute = createRoute( {
154+
getParentRoute: () => siteRoute,
155+
path: 'settings/transfer-site',
156+
} ).lazy( () =>
157+
import( '../sites/settings-transfer-site' ).then( ( d ) =>
158+
createLazyRoute( 'site-settings-transfer-site' )( {
159+
component: () => <d.default siteSlug={ siteRoute.useParams().siteSlug } />,
160+
} )
161+
)
162+
);
163+
153164
const domainsRoute = createRoute( {
154165
getParentRoute: () => rootRoute,
155166
path: 'domains',
@@ -315,6 +326,7 @@ const createRouteTree = ( config: AppConfig ) => {
315326
siteSettingsRoute,
316327
siteSettingsSiteVisibilityRoute,
317328
siteSettingsSubscriptionGiftingRoute,
329+
siteSettingsTransferSiteRoute,
318330
] )
319331
);
320332
}

client/dashboard/data/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const SITE_FIELDS = [
2020
'is_wpcom_atomic',
2121
'launch_status',
2222
'site_migration',
23+
'site_owner',
2324
'options',
2425
'jetpack',
2526
'jetpack_modules',

client/dashboard/data/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Profile {
99
}
1010

1111
export interface User {
12+
ID: number;
1213
username: string;
1314
display_name: string;
1415
avatar_URL?: string;
@@ -94,6 +95,7 @@ export interface Site {
9495
site_migration: {
9596
migration_status: string;
9697
} | null;
98+
site_owner: number;
9799
jetpack: boolean;
98100
jetpack_modules: string[];
99101
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useAuth } from '../../app/auth';
2+
import type { Site } from '../../data/types';
3+
4+
export function useCanTransferSite( { site }: { site?: Site } ) {
5+
const { user } = useAuth();
6+
7+
// TODO: The following types of the site are not allowed to transfer the ownership:
8+
// * NonAtomicJetpackSite
9+
// * P2 Hub
10+
// * WP For Teams
11+
// * VIP Site
12+
// * Staging site
13+
// We may need to handle this via endpoint somewhere. See canCurrentUserStartSiteOwnerTransfer.
14+
return site?.site_owner === user.ID;
15+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { DataForm, isItemValid } from '@automattic/dataviews';
2+
import {
3+
__experimentalHStack as HStack,
4+
__experimentalVStack as VStack,
5+
__experimentalText as Text,
6+
Button,
7+
} from '@wordpress/components';
8+
import { createInterpolateElement } from '@wordpress/element';
9+
import { sprintf, __ } from '@wordpress/i18n';
10+
import { useState } from 'react';
11+
import type { Field } from '@automattic/dataviews';
12+
13+
type ConfirmNewOwnerFormData = {
14+
email: string;
15+
};
16+
17+
const fields: Field< ConfirmNewOwnerFormData >[] = [
18+
{
19+
id: 'email',
20+
label: __( 'Email' ),
21+
type: 'email' as const,
22+
},
23+
];
24+
25+
const form = {
26+
type: 'regular' as const,
27+
fields: [ 'email' ],
28+
};
29+
30+
export function ConfirmNewOwnerForm( {
31+
siteSlug,
32+
handleSubmit,
33+
}: {
34+
siteSlug: string;
35+
handleSubmit: ( event: React.FormEvent ) => void;
36+
} ) {
37+
const [ formData, setFormData ] = useState( {
38+
email: '',
39+
} );
40+
41+
const isSaveDisabled = ! isItemValid( formData, fields, form );
42+
43+
return (
44+
<VStack spacing={ 1 }>
45+
<VStack style={ { padding: '8px 0' } }>
46+
<Text size="15px" weight={ 500 } lineHeight="32px">
47+
{ __( 'Confirm new owner' ) }
48+
</Text>
49+
<Text lineHeight="20px">
50+
{ createInterpolateElement(
51+
sprintf(
52+
/* translators: %(siteSlug)s - the current site slug */
53+
__(
54+
"Ready to transfer <strong>%(siteSlug)s</strong> and its associated purchases? Simply enter the new owner's email below, or choose an existing user to start the transfer process."
55+
),
56+
{
57+
siteSlug,
58+
}
59+
),
60+
{
61+
strong: <strong />,
62+
}
63+
) }
64+
</Text>
65+
</VStack>
66+
<form onSubmit={ handleSubmit }>
67+
<VStack spacing={ 4 } style={ { padding: '8px 0' } }>
68+
<DataForm< ConfirmNewOwnerFormData >
69+
data={ formData }
70+
fields={ fields }
71+
form={ form }
72+
onChange={ ( edits: Partial< ConfirmNewOwnerFormData > ) => {
73+
setFormData( ( data ) => ( { ...data, ...edits } ) );
74+
} }
75+
/>
76+
<HStack justify="flex-start">
77+
<Button variant="primary" type="submit" disabled={ isSaveDisabled }>
78+
{ __( 'Continue' ) }
79+
</Button>
80+
</HStack>
81+
</VStack>
82+
</form>
83+
</VStack>
84+
);
85+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { notFound } from '@tanstack/react-router';
3+
import { Card, CardBody, ExternalLink } from '@wordpress/components';
4+
import { createInterpolateElement } from '@wordpress/element';
5+
import { __ } from '@wordpress/i18n';
6+
import { siteQuery } from '../../app/queries';
7+
import PageLayout from '../../components/page-layout';
8+
import { useCanTransferSite } from '../hooks/use-can-transfer-site';
9+
import SettingsPageHeader from '../settings-page-header';
10+
import { ConfirmNewOwnerForm } from './confirm-new-owner-form';
11+
12+
export default function SettingsTransferSite( { siteSlug }: { siteSlug: string } ) {
13+
const { data: site } = useQuery( siteQuery( siteSlug ) );
14+
const canTransferSite = useCanTransferSite( { site } );
15+
16+
// TODO: Integrate with the API.
17+
const handleConfirmNewOwner = ( event: React.FormEvent ) => {
18+
event.preventDefault();
19+
};
20+
21+
if ( ! site ) {
22+
return null;
23+
}
24+
25+
if ( ! canTransferSite ) {
26+
throw notFound();
27+
}
28+
29+
return (
30+
<PageLayout
31+
size="small"
32+
header={
33+
<SettingsPageHeader
34+
title={ __( 'Transfer site' ) }
35+
description={ createInterpolateElement(
36+
__(
37+
'Transfer this site to a new or existing site member with just a few clicks. <learnMoreLink />.'
38+
),
39+
{
40+
learnMoreLink: <ExternalLink href="#learn-more">{ __( 'Learn more' ) }</ExternalLink>,
41+
}
42+
) }
43+
/>
44+
}
45+
>
46+
<Card>
47+
<CardBody>
48+
<ConfirmNewOwnerForm siteSlug={ siteSlug } handleSubmit={ handleConfirmNewOwner } />
49+
</CardBody>
50+
</Card>
51+
</PageLayout>
52+
);
53+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { __experimentalHeading as Heading } from '@wordpress/components';
2+
import { __ } from '@wordpress/i18n';
3+
import { ActionList } from '../../components/action-list';
4+
import RouterLinkButton from '../../components/router-link-button';
5+
import { useCanTransferSite } from '../hooks/use-can-transfer-site';
6+
import type { Site } from '../../data/types';
7+
8+
const TransferSite = ( { site }: { site: Site } ) => {
9+
const { slug } = site;
10+
11+
return (
12+
<ActionList.ActionItem
13+
title={ __( 'Transfer site' ) }
14+
description={ __( 'Transfer ownership of this site to another WordPress.com user.' ) }
15+
actions={
16+
<RouterLinkButton
17+
variant="secondary"
18+
size="compact"
19+
to={ `/sites/${ slug }/settings/transfer-site` }
20+
>
21+
{ __( 'Transfer' ) }
22+
</RouterLinkButton>
23+
}
24+
/>
25+
);
26+
};
27+
28+
export default function DangerZone( { site }: { site: Site } ) {
29+
const canTransferSite = useCanTransferSite( { site } );
30+
const actions = [ canTransferSite && <TransferSite key="transfer-site" site={ site } /> ].filter(
31+
Boolean
32+
);
33+
34+
if ( ! actions.length ) {
35+
return null;
36+
}
37+
38+
return (
39+
<>
40+
<Heading>{ __( 'Danger zone' ) }</Heading>
41+
<ActionList>{ actions }</ActionList>
42+
</>
43+
);
44+
}

client/dashboard/sites/settings/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PageHeader } from '../../components/page-header';
1010
import PageLayout from '../../components/page-layout';
1111
import SiteVisibilitySettingsSummary from '../settings-site-visibility/summary';
1212
import SubscriptionGiftingSettingsSummary from '../settings-subscription-gifting/summary';
13+
import DangerZone from './danger-zone';
1314
import SiteActions from './site-actions';
1415

1516
export default function SiteSettings( { siteSlug }: { siteSlug: string } ) {
@@ -30,6 +31,7 @@ export default function SiteSettings( { siteSlug }: { siteSlug: string } ) {
3031
</VStack>
3132
</Card>
3233
<SiteActions site={ site } />
34+
<DangerZone site={ site } />
3335
</PageLayout>
3436
);
3537
}

0 commit comments

Comments
 (0)