Skip to content

Commit

Permalink
A4A Dev Sites: Add Delete site action for agency dev sites (#94483)
Browse files Browse the repository at this point in the history
* A4A Dev Sites: Add Delete site action for agency dev sites

* Code clean up.

* Add missing property to actionEventNames of jetpack-cloud
  • Loading branch information
mashikag authored Sep 13, 2024
1 parent 066e484 commit f88b7da
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type Props = {
onConfirm?: () => void;
ctaLabel?: string;
closeLabel?: string;
isDisabled?: boolean;
isLoading?: boolean;
isDestructive?: boolean;
};
Expand All @@ -26,6 +27,7 @@ export function A4AConfirmationDialog( {
ctaLabel,
closeLabel,
onClose,
isDisabled,
isLoading,
isDestructive,
}: Props ) {
Expand All @@ -47,7 +49,7 @@ export function A4AConfirmationDialog( {
variant="primary"
isDestructive={ isDestructive }
isBusy={ isLoading }
disabled={ isLoading }
disabled={ isDisabled || isLoading }
onClick={ onConfirm }
>
{ ctaLabel ?? translate( 'Confirm' ) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
.a4a-confirmation-dialog__heading {
padding-block-end: 16px;
@include a4a-font-heading-lg;
}

.a4a-confirmation-dialog .dialog__action-buttons {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;
.a4a-confirmation-dialog {
&__heading {
padding-bottom: 16px;
@include a4a-font-heading-lg;
}

& .dialog__action-buttons {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;

// Something is overriding the opacity of a disabled button, so we need to override it back here
// Feel free to remove the .is-destructive class if you want to apply this to all buttons,
// or to remove the styles entirely if the root issue was fixed
button.components-button.is-destructive:disabled {
opacity: 0.3;
}
}
}
40 changes: 40 additions & 0 deletions client/a8c-for-agencies/data/sites/use-delete-dev-site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
import wpcom from 'calypso/lib/wp';
import { useSelector } from 'calypso/state';
import { getActiveAgencyId } from 'calypso/state/a8c-for-agencies/agency/selectors';

interface APIResponse {
success: boolean;
}

interface APIRequestArgs {
agencyId: number;
siteId: number;
}

function mutationDeleteDevSite( { siteId, agencyId }: APIRequestArgs ): Promise< APIResponse > {
if ( ! agencyId ) {
throw new Error( 'Agency ID is required to assign a license' );
}

return wpcom.req.post( {
method: 'DELETE',
apiNamespace: 'wpcom/v2',
path: `/agency/${ agencyId }/sites/${ siteId }/delete-dev-site`,
} );
}

/**
* Hook to remove a dev site from the agency dashboard, to revoke the dev license, and to delete the WPCOM site.
*/
export default function useDeleteDevSiteMutation( siteId: number, options?: UseMutationOptions ) {
const agencyId = useSelector( getActiveAgencyId );
if ( ! agencyId ) {
throw new Error( 'Agency ID is required to delete a WPCOM dev site' );
}

return useMutation( {
...options,
mutationFn: () => mutationDeleteDevSite( { agencyId, siteId } ),
} );
}
4 changes: 4 additions & 0 deletions client/a8c-for-agencies/data/sites/use-remove-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function mutationRemoveSite( { siteId, agencyId }: Props ): Promise< APIResponse
} );
}

/**
* Hook to remove a site from the agency dashboard.
* Note: This mutation will remove the site from the agency dashboard but the site and its license will still exist.
*/
export default function useRemoveSiteMutation< TContext = unknown >(
options?: UseMutationOptions< APIError, Error, Props, TContext >
): UseMutationResult< APIError, Error, Props, TContext > {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { FormLabel } from '@automattic/components';
import { useTranslate } from 'i18n-calypso';
import { useState } from 'react';
import { A4AConfirmationDialog } from 'calypso/a8c-for-agencies/components/a4a-confirmation-dialog';
import useDeleteDevSiteMutation from 'calypso/a8c-for-agencies/data/sites/use-delete-dev-site';
import FormFieldset from 'calypso/components/forms/form-fieldset';
import FormTextInput from 'calypso/components/forms/form-text-input';
import { useDispatch } from 'calypso/state';
import { errorNotice } from 'calypso/state/notices/actions';
import './style.scss';

type Props = {
siteId: number;
siteDomain: string;
onClose: () => void;
onSiteDeleted?: () => void;
busy?: boolean;
};

export function DevSiteDeleteConfirmationDialog( {
siteId,
siteDomain,
onSiteDeleted,
onClose,
busy,
}: Props ) {
const dispatch = useDispatch();
const translate = useTranslate();
const [ isDisabled, setIsDisabled ] = useState( true ); // Disabled by default - user needs to type the site name to enable the button
const title = translate( 'Delete site' );

const { mutate: deleteDevSite, isPending: isDeleting } = useDeleteDevSiteMutation( siteId, {
onSuccess: () => {
onSiteDeleted?.();
},
onError: () => {
dispatch( errorNotice( translate( 'An error occurred while deleting the site.' ) ) );
},
} );

const handleDeleteConfirmationInputChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
const value = event.target.value;
setIsDisabled( value !== siteDomain );
};

return (
<A4AConfirmationDialog
className="dev-site-delete-confirmation-dialog"
title={ title }
onClose={ onClose }
onConfirm={ deleteDevSite }
ctaLabel={ translate( 'Delete site' ) }
isLoading={ busy || isDeleting }
isDisabled={ isDisabled }
isDestructive
>
<p>
{ translate( 'Are you sure you want to delete the site {{b}}%(siteDomain)s{{/b}}?', {
args: { siteDomain },
components: {
b: <b />,
},
comment: '%(siteDomain)s is the site domain',
} ) }
</p>
<p>
{ translate(
'Deletion is {{strong}}irreversible and will permanently remove all site content{{/strong}} — posts, pages, media, users, authors, domains, purchased upgrades, and premium themes.',
{
components: {
strong: <strong />,
},
}
) }
</p>

<FormFieldset>
<FormLabel htmlFor="site-delete-confirmation-input">
{ translate(
'Type {{strong}}%(siteDomain)s{{/strong}} below to confirm you want to delete the site:',
{
components: {
strong: <strong />,
},
args: { siteDomain },
comment: '%(siteDomain)s is the site domain',
}
) }
</FormLabel>
<FormTextInput
name="site-delete-confirmation-input"
autoCapitalize="off"
aria-required="true"
onChange={ handleDeleteConfirmationInputChange }
/>
</FormFieldset>
</A4AConfirmationDialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.dev-site-delete-confirmation-dialog {
&.dialog.card {
max-width: 60%;
}

p {
font-size: 1rem;
margin-bottom: 1rem;
}

fieldset {
margin-top: 2rem;

label {
font-size: 1rem;
font-weight: 500;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const actionEventNames: ActionEventNames = {
large_screen: 'calypso_a4a_sites_dataview_prepare_for_launch_large_screen',
small_screen: 'calypso_a4a_sites_dataview_prepare_for_launch_small_screen',
},
delete_site: {
large_screen: 'calypso_a4a_sites_dataview_delete_large_screen',
small_screen: 'calypso_a4a_sites_dataview_delete_small_screen',
},
};

// Returns event name based on the action type
Expand Down
79 changes: 55 additions & 24 deletions client/a8c-for-agencies/sections/sites/site-actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
import { useState, useRef, useCallback } from 'react';
import useRemoveSiteMutation from 'calypso/a8c-for-agencies/data/sites/use-remove-site';
import { DevSiteDeleteConfirmationDialog } from 'calypso/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog';
import useSiteActions from 'calypso/a8c-for-agencies/sections/sites/site-actions/use-site-actions';
import { SiteRemoveConfirmationDialog } from 'calypso/a8c-for-agencies/sections/sites/site-remove-confirmation-dialog';
import PopoverMenu from 'calypso/components/popover-menu';
import PopoverMenuItem from 'calypso/components/popover-menu/item';
import { useDispatch } from 'calypso/state';
import { successNotice } from 'calypso/state/notices/actions';
import useSiteActions from './use-site-actions';
import type { AllowedActionTypes, SiteNode } from '../types';

import './style.scss';
Expand All @@ -32,8 +33,10 @@ export default function SiteActions( {
const dispatch = useDispatch();

const [ isOpen, setIsOpen ] = useState( false );
const [ showDeleteDevSiteDialog, setShowDeleteDevSiteDialog ] = useState( false );
const [ showRemoveSiteDialog, setShowRemoveSiteDialog ] = useState( false );
const [ isPendingRefetch, setIsPendingRefetch ] = useState( false );
const { a4a_site_id: siteId, url: siteDomain } = site.value || { siteDomain: '' };

const buttonActionRef = useRef< HTMLButtonElement | null >( null );

Expand All @@ -46,8 +49,13 @@ export default function SiteActions( {
}, [] );

const onSelectAction = useCallback( ( action: AllowedActionTypes ) => {
if ( action === 'remove_site' ) {
setShowRemoveSiteDialog( true );
switch ( action ) {
case 'delete_site':
setShowDeleteDevSiteDialog( true );
break;
case 'remove_site':
setShowRemoveSiteDialog( true );
break;
}
}, [] );

Expand All @@ -59,28 +67,42 @@ export default function SiteActions( {
onSelect: onSelectAction,
} );

const { mutate: removeSite, isPending } = useRemoveSiteMutation();
const { mutate: removeSite, isPending: isRemovingSite } = useRemoveSiteMutation();

const onRemoveSite = useCallback( () => {
if ( site.value?.a4a_site_id ) {
removeSite(
{ siteId: site.value?.a4a_site_id },
{
onSuccess: () => {
setIsPendingRefetch( true );
// Add 1 second delay to refetch sites to give time for site profile to be reindexed properly.
setTimeout( () => {
onRefetchSite?.()?.then( () => {
setIsPendingRefetch( false );
setShowRemoveSiteDialog( false );
dispatch( successNotice( translate( 'The site has been successfully removed.' ) ) );
} );
}, 1000 );
},
}
);
if ( ! siteId ) {
return;
}
}, [ dispatch, onRefetchSite, removeSite, site.value?.a4a_site_id, translate ] );

removeSite(
{ siteId },
{
onSuccess: () => {
setIsPendingRefetch( true );
// Add 1 second delay to refetch sites to give time for site profile to be reindexed properly.
setTimeout( () => {
onRefetchSite?.()?.then( () => {
setIsPendingRefetch( false );
setShowRemoveSiteDialog( false );
dispatch( successNotice( translate( 'The site has been successfully removed.' ) ) );
} );
}, 1000 );
},
}
);
}, [ dispatch, onRefetchSite, removeSite, siteId, translate ] );

const onDeleteSite = useCallback( () => {
setIsPendingRefetch( true );
// Add 1 second delay to refetch sites to give time for site profile to be reindexed properly.
setTimeout( () => {
onRefetchSite?.()?.then( () => {
setIsPendingRefetch( false );
setShowDeleteDevSiteDialog( false );
dispatch( successNotice( translate( 'The site has been successfully deleted.' ) ) );
} );
}, 1000 );
}, [ dispatch, onRefetchSite, translate ] );

return (
<>
Expand Down Expand Up @@ -121,10 +143,19 @@ export default function SiteActions( {

{ showRemoveSiteDialog && (
<SiteRemoveConfirmationDialog
siteName={ site.value?.url || '' }
siteName={ siteDomain }
onClose={ () => setShowRemoveSiteDialog( false ) }
onConfirm={ onRemoveSite }
busy={ isPending || isPendingRefetch }
busy={ isRemovingSite || isPendingRefetch }
/>
) }
{ showDeleteDevSiteDialog && (
<DevSiteDeleteConfirmationDialog
siteId={ siteId || 0 }
siteDomain={ siteDomain }
onClose={ () => setShowDeleteDevSiteDialog( false ) }
onSiteDeleted={ onDeleteSite }
busy={ isPendingRefetch }
/>
) }
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ export default function useSiteActions( {
const isWPCOMSimpleSite = ! isJetpack && ! isA4AClient;
const isWPCOMSite = isWPCOMSimpleSite || isWPCOMAtomicSite;

const canRemove = useSelector( ( state: A4AStore ) =>
const hasRemoveManagedSitesCapability = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_remove_managed_sites' )
);

// Whether to enable the Remove site action. The action will remove the site from the A4A dashboard but the site and its license will still exist.
const canRemove = ! isDevSite && hasRemoveManagedSitesCapability;

// Whether to enable the Delete site action. The action will remove the site from the A4A dashboard and delete the site and its license.
const canDelete = isDevSite && hasRemoveManagedSitesCapability;

return useMemo( () => {
if ( ! siteValue ) {
return [];
Expand Down Expand Up @@ -170,8 +176,16 @@ export default function useSiteActions( {
className: 'is-error',
isEnabled: canRemove,
},
{
name: translate( 'Delete site' ),
onClick: () => handleClickMenuItem( 'delete_site' ),
icon: 'trash',
className: 'is-error',
isEnabled: canDelete,
},
];
}, [
canDelete,
canRemove,
dispatch,
isDevSite,
Expand Down
Loading

0 comments on commit f88b7da

Please sign in to comment.