From f88b7dace21ee2b7e1d17050bab8af8dc2dbf8f6 Mon Sep 17 00:00:00 2001 From: Maciej Grabowski Date: Fri, 13 Sep 2024 13:17:23 +0200 Subject: [PATCH] A4A Dev Sites: Add Delete site action for agency dev sites (#94483) * A4A Dev Sites: Add Delete site action for agency dev sites * Code clean up. * Add missing property to actionEventNames of jetpack-cloud --- .../a4a-confirmation-dialog/index.tsx | 4 +- .../a4a-confirmation-dialog/style.scss | 28 ++++-- .../data/sites/use-delete-dev-site.ts | 40 ++++++++ .../data/sites/use-remove-site.ts | 4 + .../index.tsx | 99 +++++++++++++++++++ .../style.scss | 19 ++++ .../site-actions/get-action-event-name.ts | 4 + .../sections/sites/site-actions/index.tsx | 79 ++++++++++----- .../sites/site-actions/use-site-actions.ts | 16 ++- .../site-actions/get-action-event-name.ts | 4 + .../agency-dashboard/sites-overview/types.ts | 3 +- 11 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 client/a8c-for-agencies/data/sites/use-delete-dev-site.ts create mode 100644 client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/index.tsx create mode 100644 client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/style.scss diff --git a/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx b/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx index aa1fd330b29d8..22e6799e52d83 100644 --- a/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx +++ b/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx @@ -14,6 +14,7 @@ export type Props = { onConfirm?: () => void; ctaLabel?: string; closeLabel?: string; + isDisabled?: boolean; isLoading?: boolean; isDestructive?: boolean; }; @@ -26,6 +27,7 @@ export function A4AConfirmationDialog( { ctaLabel, closeLabel, onClose, + isDisabled, isLoading, isDestructive, }: Props ) { @@ -47,7 +49,7 @@ export function A4AConfirmationDialog( { variant="primary" isDestructive={ isDestructive } isBusy={ isLoading } - disabled={ isLoading } + disabled={ isDisabled || isLoading } onClick={ onConfirm } > { ctaLabel ?? translate( 'Confirm' ) } diff --git a/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss b/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss index abb089bec3795..dad45cff292d2 100644 --- a/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss +++ b/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss @@ -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; + } + } } diff --git a/client/a8c-for-agencies/data/sites/use-delete-dev-site.ts b/client/a8c-for-agencies/data/sites/use-delete-dev-site.ts new file mode 100644 index 0000000000000..59d2d9445c0a8 --- /dev/null +++ b/client/a8c-for-agencies/data/sites/use-delete-dev-site.ts @@ -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 } ), + } ); +} diff --git a/client/a8c-for-agencies/data/sites/use-remove-site.ts b/client/a8c-for-agencies/data/sites/use-remove-site.ts index 386e205d31779..13f659e8f751c 100644 --- a/client/a8c-for-agencies/data/sites/use-remove-site.ts +++ b/client/a8c-for-agencies/data/sites/use-remove-site.ts @@ -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 > { diff --git a/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/index.tsx b/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/index.tsx new file mode 100644 index 0000000000000..811e459bdea38 --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/index.tsx @@ -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 ( + +

+ { translate( 'Are you sure you want to delete the site {{b}}%(siteDomain)s{{/b}}?', { + args: { siteDomain }, + components: { + b: , + }, + comment: '%(siteDomain)s is the site domain', + } ) } +

+

+ { 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: , + }, + } + ) } +

+ + + + { translate( + 'Type {{strong}}%(siteDomain)s{{/strong}} below to confirm you want to delete the site:', + { + components: { + strong: , + }, + args: { siteDomain }, + comment: '%(siteDomain)s is the site domain', + } + ) } + + + +
+ ); +} diff --git a/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/style.scss b/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/style.scss new file mode 100644 index 0000000000000..dda6cf31395c7 --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/dev-site-delete-confirmation-dialog/style.scss @@ -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; + } + } +} diff --git a/client/a8c-for-agencies/sections/sites/site-actions/get-action-event-name.ts b/client/a8c-for-agencies/sections/sites/site-actions/get-action-event-name.ts index e1c4addbc71f9..b0d0180861b56 100644 --- a/client/a8c-for-agencies/sections/sites/site-actions/get-action-event-name.ts +++ b/client/a8c-for-agencies/sections/sites/site-actions/get-action-event-name.ts @@ -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 diff --git a/client/a8c-for-agencies/sections/sites/site-actions/index.tsx b/client/a8c-for-agencies/sections/sites/site-actions/index.tsx index 1451c973da462..4fdd7f0204043 100644 --- a/client/a8c-for-agencies/sections/sites/site-actions/index.tsx +++ b/client/a8c-for-agencies/sections/sites/site-actions/index.tsx @@ -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'; @@ -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 ); @@ -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; } }, [] ); @@ -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 ( <> @@ -121,10 +143,19 @@ export default function SiteActions( { { showRemoveSiteDialog && ( setShowRemoveSiteDialog( false ) } onConfirm={ onRemoveSite } - busy={ isPending || isPendingRefetch } + busy={ isRemovingSite || isPendingRefetch } + /> + ) } + { showDeleteDevSiteDialog && ( + setShowDeleteDevSiteDialog( false ) } + onSiteDeleted={ onDeleteSite } + busy={ isPendingRefetch } /> ) } diff --git a/client/a8c-for-agencies/sections/sites/site-actions/use-site-actions.ts b/client/a8c-for-agencies/sections/sites/site-actions/use-site-actions.ts index 6ed06d7a19b4d..627ffe61d7416 100644 --- a/client/a8c-for-agencies/sections/sites/site-actions/use-site-actions.ts +++ b/client/a8c-for-agencies/sections/sites/site-actions/use-site-actions.ts @@ -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 []; @@ -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, diff --git a/client/jetpack-cloud/sections/agency-dashboard/sites-overview/site-actions/get-action-event-name.ts b/client/jetpack-cloud/sections/agency-dashboard/sites-overview/site-actions/get-action-event-name.ts index 46579a9e12327..33289f765b584 100644 --- a/client/jetpack-cloud/sections/agency-dashboard/sites-overview/site-actions/get-action-event-name.ts +++ b/client/jetpack-cloud/sections/agency-dashboard/sites-overview/site-actions/get-action-event-name.ts @@ -46,6 +46,10 @@ const actionEventNames: ActionEventNames = { large_screen: 'calypso_jetpack_agency_dashboard_prepare_for_launch_large_screen', small_screen: 'calypso_jetpack_agency_dashboard_prepare_for_launch_small_screen', }, + delete_site: { + large_screen: 'calypso_jetpack_agency_dashboard_delete_large_screen', + small_screen: 'calypso_jetpack_agency_dashboard_delete_small_screen', + }, }; // Returns event name based on the action type diff --git a/client/jetpack-cloud/sections/agency-dashboard/sites-overview/types.ts b/client/jetpack-cloud/sections/agency-dashboard/sites-overview/types.ts index 739998c8bd70b..d1948b398d028 100644 --- a/client/jetpack-cloud/sections/agency-dashboard/sites-overview/types.ts +++ b/client/jetpack-cloud/sections/agency-dashboard/sites-overview/types.ts @@ -227,7 +227,8 @@ export type AllowedActionTypes = | 'change_domain' | 'hosting_configuration' | 'remove_site' - | 'prepare_for_launch'; + | 'prepare_for_launch' + | 'delete_site'; export type ActionEventNames = { [ key in AllowedActionTypes ]: { small_screen: string; large_screen: string };