Skip to content

Commit b0e12c8

Browse files
authored
Studio: Delete all active demo sites (#131)
* Studio: Delete all active demo sites * Update: Fix some linting errors * Update: wording at delete all confirmation dialog * Update: fix linting errors * Update: Add a new hook to fetch snaphsots from server * Update: Add tests * Fix: failing tests * Update: rewording message * Update: add notification after deleting all sites * Update: Fix lint errors
1 parent 8d53364 commit b0e12c8

6 files changed

Lines changed: 225 additions & 11 deletions

File tree

‎src/components/user-settings.tsx‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { DropdownMenu, Icon, MenuGroup, MenuItem, Spinner } from '@wordpress/com
22
import { sprintf } from '@wordpress/i18n';
33
import { moreVertical, trash } from '@wordpress/icons';
44
import { useI18n } from '@wordpress/react-i18n';
5-
import { useCallback, useState } from 'react';
5+
import { useCallback, useState, useEffect } from 'react';
66
import { WPCOM_PROFILE_URL } from '../constants';
77
import { useAuth } from '../hooks/use-auth';
88
import { useDeleteSnapshot } from '../hooks/use-delete-snapshot';
9+
import { useFetchSnapshots } from '../hooks/use-fetch-snaphsots';
910
import { useIpcListener } from '../hooks/use-ipc-listener';
1011
import { useOffline } from '../hooks/use-offline';
11-
import { useSiteDetails } from '../hooks/use-site-details';
1212
import { useSiteUsage } from '../hooks/use-site-usage';
1313
import { cx } from '../lib/cx';
1414
import { getIpcApi } from '../lib/get-ipc-api';
@@ -142,9 +142,10 @@ const SnapshotInfo = ( {
142142

143143
export default function UserSettings() {
144144
const { __ } = useI18n();
145+
const [ deletedAllSnapshots, setDeletedAllSnapshots ] = useState( false );
145146
const { isAuthenticated, authenticate, logout, user } = useAuth();
146-
const { snapshots } = useSiteDetails();
147147
const { siteLimit, siteCount, isLoading: isLoadingSiteUsage } = useSiteUsage();
148+
const { allSnapshots, isLoading: isLoadingAllSnapshots } = useFetchSnapshots();
148149
const { deleteAllSnapshots, loadingDeletingAllSnapshots } = useDeleteSnapshot( {
149150
displayAlert: true,
150151
} );
@@ -161,12 +162,39 @@ export default function UserSettings() {
161162
setNeedsToOpenUserSettings( ! needsToOpenUserSettings );
162163
} );
163164

165+
useEffect( () => {
166+
if ( deletedAllSnapshots && ! loadingDeletingAllSnapshots ) {
167+
setDeletedAllSnapshots( false );
168+
getIpcApi().showNotification( {
169+
title: __( 'Delete Successful' ),
170+
body: __( 'All demo sites have been deleted.' ),
171+
} );
172+
}
173+
}, [ __, loadingDeletingAllSnapshots, deletedAllSnapshots ] );
174+
164175
const onRemoveSnapshots = useCallback( async () => {
165-
if ( snapshots?.length === 0 ) {
176+
if ( ! allSnapshots || allSnapshots.length === 0 ) {
166177
return;
167178
}
168-
await deleteAllSnapshots( snapshots );
169-
}, [ snapshots, deleteAllSnapshots ] );
179+
180+
const CANCEL_BUTTON_INDEX = 0;
181+
const DELETE_BUTTON_INDEX = 1;
182+
183+
const { response } = await getIpcApi().showMessageBox( {
184+
type: 'warning',
185+
message: __( 'Delete all demo sites' ),
186+
detail: __(
187+
'All demo sites that exist for your WordPress.com account, along with all posts, pages, comments, and media, will be lost.'
188+
),
189+
buttons: [ __( 'Cancel' ), __( 'Delete all' ) ],
190+
cancelId: CANCEL_BUTTON_INDEX,
191+
} );
192+
193+
if ( response === DELETE_BUTTON_INDEX ) {
194+
await deleteAllSnapshots( allSnapshots );
195+
setDeletedAllSnapshots( true );
196+
}
197+
}, [ allSnapshots, deleteAllSnapshots, __ ] );
170198

171199
return (
172200
<>
@@ -201,8 +229,9 @@ export default function UserSettings() {
201229
isDisabled={
202230
siteCount === 0 ||
203231
loadingDeletingAllSnapshots ||
232+
isLoadingAllSnapshots ||
204233
isLoadingSiteUsage ||
205-
snapshots.length === 0 ||
234+
allSnapshots?.length === 0 ||
206235
isOffline
207236
}
208237
siteCount={ siteCount }
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// To run tests, execute `npm run test -- src/hooks/tests/use-fetch-snapshots.test.ts` from the root directory
2+
import * as Sentry from '@sentry/electron/renderer';
3+
import { waitFor, renderHook } from '@testing-library/react';
4+
import { useOffline } from '../../hooks/use-offline';
5+
import { useAuth } from '../use-auth';
6+
import { useFetchSnapshots } from '../use-fetch-snaphsots';
7+
import { useSiteUsage } from '../use-site-usage';
8+
9+
jest.mock( '@sentry/electron/renderer' );
10+
jest.mock( '../use-auth' );
11+
jest.mock( '../use-site-usage' );
12+
13+
describe( 'useFetchSnapshots', () => {
14+
// Mock data and responses
15+
const clientReqGet = jest.fn();
16+
17+
beforeEach( () => {
18+
jest.resetAllMocks();
19+
( useSiteUsage as jest.Mock ).mockReturnValue( { siteCount: 1 } );
20+
} );
21+
it( 'sets initial state correctly when client is not available', async () => {
22+
( useAuth as jest.Mock ).mockImplementation( () => ( {
23+
client: null,
24+
} ) );
25+
const { result } = renderHook( () => useFetchSnapshots() );
26+
27+
expect( result.current.isLoading ).toBe( false );
28+
expect( result.current.allSnapshots ).toBe( null );
29+
} );
30+
it( 'handles fetch snapshots failure', async () => {
31+
const error = new Error( 'Failed to fetch' );
32+
( useAuth as jest.Mock ).mockReturnValue( {
33+
client: {
34+
req: {
35+
get: clientReqGet.mockRejectedValue( error ),
36+
},
37+
},
38+
} );
39+
40+
const { result } = renderHook( () => useFetchSnapshots() );
41+
42+
await waitFor( () => {
43+
expect( result.current.isLoading ).toBe( false );
44+
expect( Sentry.captureException ).toHaveBeenCalledWith( error );
45+
expect( result.current.allSnapshots ).toBe( null );
46+
} );
47+
} );
48+
it( 'fetches snapshots on mount when client is available', async () => {
49+
( useAuth as jest.Mock ).mockReturnValue( {
50+
client: {
51+
req: {
52+
get: clientReqGet.mockResolvedValue( {
53+
sites: [
54+
{
55+
atomic_site_id: 12345,
56+
},
57+
{
58+
atomic_site_id: 67890,
59+
},
60+
{
61+
atomic_site_id: 13579,
62+
},
63+
],
64+
} ),
65+
},
66+
},
67+
} );
68+
69+
const { result } = renderHook( () => useFetchSnapshots() );
70+
71+
await waitFor( () => {
72+
expect( result.current.isLoading ).toBe( false );
73+
expect( clientReqGet ).toHaveBeenCalled();
74+
expect( result.current.allSnapshots ).toStrictEqual( [
75+
{ atomicSiteId: 12345 },
76+
{ atomicSiteId: 67890 },
77+
{ atomicSiteId: 13579 },
78+
] );
79+
} );
80+
} );
81+
82+
it( 'should not check for snapshots when lacking an internet connection', () => {
83+
( useAuth as jest.Mock ).mockReturnValue( {
84+
client: {
85+
req: {
86+
get: clientReqGet.mockResolvedValue( { sites: [] } ),
87+
},
88+
},
89+
} );
90+
( useOffline as jest.Mock ).mockReturnValue( true );
91+
renderHook( () => useSiteUsage() );
92+
93+
expect( clientReqGet ).not.toHaveBeenCalled();
94+
} );
95+
} );

‎src/hooks/tests/use-site-usage.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// To run tests, execute `npm run test -- src/hooks/use-site-usage.test.ts` from the root directory
1+
// To run tests, execute `npm run test -- src/hooks/tests/use-site-usage.test.ts` from the root directory
22
import * as Sentry from '@sentry/electron/renderer';
33
import { waitFor, renderHook } from '@testing-library/react';
44
import { LIMIT_OF_ZIP_SITES_PER_USER } from '../../constants';
@@ -53,7 +53,10 @@ describe( 'useSiteUsage', () => {
5353
( useAuth as jest.Mock ).mockReturnValue( {
5454
client: {
5555
req: {
56-
get: clientReqGet.mockResolvedValue( { site_count: 3, site_limit: 15 } ),
56+
get: clientReqGet.mockResolvedValue( {
57+
site_count: 3,
58+
site_limit: 15,
59+
} ),
5760
},
5861
},
5962
} );

‎src/hooks/use-fetch-snaphsots.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as Sentry from '@sentry/electron/renderer';
2+
import { useCallback, useEffect, useState } from 'react';
3+
import { useAuth } from './use-auth';
4+
import { useOffline } from './use-offline';
5+
import { useSiteUsage } from './use-site-usage';
6+
7+
interface FetchSnapshotResponse {
8+
sites: { atomic_site_id: number }[];
9+
}
10+
11+
export function useFetchSnapshots() {
12+
const [ allSnapshots, setAllSnapshots ] = useState< Pick< Snapshot, 'atomicSiteId' >[] | null >(
13+
null
14+
);
15+
const { siteCount } = useSiteUsage();
16+
const [ isLoading, setIsLoading ] = useState( false );
17+
const { client } = useAuth();
18+
const isOffline = useOffline();
19+
20+
const fetchAllSnapshots = useCallback( async () => {
21+
if ( ! client?.req || isOffline ) {
22+
return null;
23+
}
24+
setIsLoading( true );
25+
try {
26+
const response: FetchSnapshotResponse = await client.req.get( {
27+
path: '/jurassic-ninja/list',
28+
apiNamespace: 'wpcom/v2',
29+
} );
30+
const sites: Pick< Snapshot, 'atomicSiteId' >[] =
31+
response.sites?.map( ( { atomic_site_id } ) => ( { atomicSiteId: atomic_site_id } ) ) ?? [];
32+
return sites;
33+
} catch ( error ) {
34+
Sentry.captureException( error );
35+
} finally {
36+
setIsLoading( false );
37+
}
38+
}, [ client?.req, isOffline ] );
39+
40+
useEffect( () => {
41+
if ( ! client ) {
42+
return;
43+
}
44+
const fetchSnapshots = async () => {
45+
const sites = await fetchAllSnapshots();
46+
if ( ! sites ) {
47+
return;
48+
}
49+
setAllSnapshots( sites );
50+
setIsLoading( false );
51+
};
52+
fetchSnapshots();
53+
}, [ client, fetchAllSnapshots, siteCount ] );
54+
55+
return { fetchAllSnapshots, allSnapshots, isLoading };
56+
}

‎src/hooks/use-site-details.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { __ } from '@wordpress/i18n';
33
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
44
import { getIpcApi } from '../lib/get-ipc-api';
55
import { useDeleteSnapshot } from './use-delete-snapshot';
6+
import { useFetchSnapshots } from './use-fetch-snaphsots';
67

78
interface SiteDetailsContext {
89
selectedSite: SiteDetails | null;
@@ -70,6 +71,7 @@ function useSelectedSite( firstSiteId: string | null ) {
7071
}
7172

7273
function useSnapshots() {
74+
const { allSnapshots, isLoading: loadingServerSnapshots } = useFetchSnapshots();
7375
const [ initiated, setInitiated ] = useState( false );
7476
const [ snapshots, setSnapshots ] = useState< Snapshot[] >( [] );
7577

@@ -83,6 +85,25 @@ function useSnapshots() {
8385
} );
8486
}, [] );
8587

88+
const clearFloatingSnapshots = useCallback( function clearFloatingSnapshots(
89+
allSnapshots: Pick< Snapshot, 'atomicSiteId' >[]
90+
) {
91+
const siteIds = allSnapshots.map( ( snapshot ) => snapshot.atomicSiteId );
92+
if ( ! siteIds.length ) {
93+
setSnapshots( [] );
94+
return;
95+
}
96+
setSnapshots( ( snapshots ) =>
97+
snapshots.filter( ( snapshot ) => siteIds.includes( snapshot.atomicSiteId ) )
98+
);
99+
}, [] );
100+
101+
useEffect( () => {
102+
if ( initiated && ! loadingServerSnapshots && allSnapshots ) {
103+
clearFloatingSnapshots( allSnapshots );
104+
}
105+
}, [ allSnapshots, clearFloatingSnapshots, initiated, loadingServerSnapshots ] );
106+
86107
// Save snapshots to storage when they change
87108
useEffect( () => {
88109
if ( ! initiated ) {
@@ -101,6 +122,11 @@ function useSnapshots() {
101122
( snapshotI ) => snapshotI.atomicSiteId === snapshot.atomicSiteId
102123
);
103124
if ( index === -1 ) {
125+
if ( snapshot.isDeleting ) {
126+
const newSnapshots = [ ...snapshots ];
127+
newSnapshots.push( snapshot as Snapshot );
128+
return newSnapshots;
129+
}
104130
return snapshots;
105131
}
106132
const newSnapshots = [ ...snapshots ];

‎src/hooks/use-site-usage.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { useAuth } from './use-auth';
55
import { useOffline } from './use-offline';
66
import { useSiteDetails } from './use-site-details';
77

8+
export interface UsageSite {
9+
atomic_site_id: number;
10+
}
11+
812
export function useSiteUsage() {
913
const [ isLoading, setIsLoading ] = useState( false );
1014
const { snapshots } = useSiteDetails();
@@ -49,8 +53,9 @@ export function useSiteUsage() {
4953
setSiteLimit( LIMIT_OF_ZIP_SITES_PER_USER );
5054
return;
5155
}
52-
setSiteCount( response.site_count );
53-
setSiteLimit( response.site_limit );
56+
const { site_count, site_limit } = response;
57+
setSiteCount( site_count );
58+
setSiteLimit( site_limit );
5459
};
5560
fetchStats();
5661
}, [ fetchSiteUsage, snapshots.length, client ] );

0 commit comments

Comments
 (0)