Skip to content
43 changes: 36 additions & 7 deletions src/components/user-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { DropdownMenu, Icon, MenuGroup, MenuItem, Spinner } from '@wordpress/com
import { sprintf } from '@wordpress/i18n';
import { moreVertical, trash } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useState } from 'react';
import { useCallback, useState, useEffect } from 'react';
import { WPCOM_PROFILE_URL } from '../constants';
import { useAuth } from '../hooks/use-auth';
import { useDeleteSnapshot } from '../hooks/use-delete-snapshot';
import { useFetchSnapshots } from '../hooks/use-fetch-snaphsots';
import { useIpcListener } from '../hooks/use-ipc-listener';
import { useOffline } from '../hooks/use-offline';
import { useSiteDetails } from '../hooks/use-site-details';
import { useSiteUsage } from '../hooks/use-site-usage';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
Expand Down Expand Up @@ -142,9 +142,10 @@ const SnapshotInfo = ( {

export default function UserSettings() {
const { __ } = useI18n();
const [ deletedAllSnapshots, setDeletedAllSnapshots ] = useState( false );
const { isAuthenticated, authenticate, logout, user } = useAuth();
const { snapshots } = useSiteDetails();
const { siteLimit, siteCount, isLoading: isLoadingSiteUsage } = useSiteUsage();
const { allSnapshots, isLoading: isLoadingAllSnapshots } = useFetchSnapshots();
const { deleteAllSnapshots, loadingDeletingAllSnapshots } = useDeleteSnapshot( {
displayAlert: true,
} );
Expand All @@ -161,12 +162,39 @@ export default function UserSettings() {
setNeedsToOpenUserSettings( ! needsToOpenUserSettings );
} );

useEffect( () => {
if ( deletedAllSnapshots && ! loadingDeletingAllSnapshots ) {
setDeletedAllSnapshots( false );
getIpcApi().showNotification( {
title: __( 'Delete Successful' ),
body: __( 'All demo sites have been deleted.' ),
} );
}
}, [ __, loadingDeletingAllSnapshots, deletedAllSnapshots ] );

const onRemoveSnapshots = useCallback( async () => {
if ( snapshots?.length === 0 ) {
if ( ! allSnapshots || allSnapshots.length === 0 ) {
return;
}
await deleteAllSnapshots( snapshots );
}, [ snapshots, deleteAllSnapshots ] );

const CANCEL_BUTTON_INDEX = 0;
const DELETE_BUTTON_INDEX = 1;

const { response } = await getIpcApi().showMessageBox( {
type: 'warning',
message: __( 'Delete all demo sites' ),
detail: __(
'All demo sites that exist for your WordPress.com account, along with all posts, pages, comments, and media, will be lost.'
),
buttons: [ __( 'Cancel' ), __( 'Delete all' ) ],
Comment thread
kozer marked this conversation as resolved.
cancelId: CANCEL_BUTTON_INDEX,
} );

if ( response === DELETE_BUTTON_INDEX ) {
await deleteAllSnapshots( allSnapshots );
Comment thread
kozer marked this conversation as resolved.
setDeletedAllSnapshots( true );
}
}, [ allSnapshots, deleteAllSnapshots, __ ] );

return (
<>
Expand Down Expand Up @@ -201,8 +229,9 @@ export default function UserSettings() {
isDisabled={
siteCount === 0 ||
loadingDeletingAllSnapshots ||
isLoadingAllSnapshots ||
isLoadingSiteUsage ||
snapshots.length === 0 ||
allSnapshots?.length === 0 ||
isOffline
}
siteCount={ siteCount }
Expand Down
95 changes: 95 additions & 0 deletions src/hooks/tests/use-fetch-snapshots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// To run tests, execute `npm run test -- src/hooks/tests/use-fetch-snapshots.test.ts` from the root directory
import * as Sentry from '@sentry/electron/renderer';
import { waitFor, renderHook } from '@testing-library/react';
import { useOffline } from '../../hooks/use-offline';
import { useAuth } from '../use-auth';
import { useFetchSnapshots } from '../use-fetch-snaphsots';
import { useSiteUsage } from '../use-site-usage';

jest.mock( '@sentry/electron/renderer' );
jest.mock( '../use-auth' );
jest.mock( '../use-site-usage' );

describe( 'useFetchSnapshots', () => {
// Mock data and responses
const clientReqGet = jest.fn();

beforeEach( () => {
jest.resetAllMocks();
( useSiteUsage as jest.Mock ).mockReturnValue( { siteCount: 1 } );
} );
it( 'sets initial state correctly when client is not available', async () => {
( useAuth as jest.Mock ).mockImplementation( () => ( {
client: null,
} ) );
const { result } = renderHook( () => useFetchSnapshots() );

expect( result.current.isLoading ).toBe( false );
expect( result.current.allSnapshots ).toBe( null );
} );
it( 'handles fetch snapshots failure', async () => {
const error = new Error( 'Failed to fetch' );
( useAuth as jest.Mock ).mockReturnValue( {
client: {
req: {
get: clientReqGet.mockRejectedValue( error ),
},
},
} );

const { result } = renderHook( () => useFetchSnapshots() );

await waitFor( () => {
expect( result.current.isLoading ).toBe( false );
expect( Sentry.captureException ).toHaveBeenCalledWith( error );
expect( result.current.allSnapshots ).toBe( null );
} );
} );
it( 'fetches snapshots on mount when client is available', async () => {
( useAuth as jest.Mock ).mockReturnValue( {
client: {
req: {
get: clientReqGet.mockResolvedValue( {
sites: [
{
atomic_site_id: 12345,
},
{
atomic_site_id: 67890,
},
{
atomic_site_id: 13579,
},
],
} ),
},
},
} );

const { result } = renderHook( () => useFetchSnapshots() );

await waitFor( () => {
expect( result.current.isLoading ).toBe( false );
expect( clientReqGet ).toHaveBeenCalled();
expect( result.current.allSnapshots ).toStrictEqual( [
{ atomicSiteId: 12345 },
{ atomicSiteId: 67890 },
{ atomicSiteId: 13579 },
] );
} );
} );

it( 'should not check for snapshots when lacking an internet connection', () => {
( useAuth as jest.Mock ).mockReturnValue( {
client: {
req: {
get: clientReqGet.mockResolvedValue( { sites: [] } ),
},
},
} );
( useOffline as jest.Mock ).mockReturnValue( true );
renderHook( () => useSiteUsage() );

expect( clientReqGet ).not.toHaveBeenCalled();
} );
} );
7 changes: 5 additions & 2 deletions src/hooks/tests/use-site-usage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// To run tests, execute `npm run test -- src/hooks/use-site-usage.test.ts` from the root directory
// To run tests, execute `npm run test -- src/hooks/tests/use-site-usage.test.ts` from the root directory
import * as Sentry from '@sentry/electron/renderer';
import { waitFor, renderHook } from '@testing-library/react';
import { LIMIT_OF_ZIP_SITES_PER_USER } from '../../constants';
Expand Down Expand Up @@ -53,7 +53,10 @@ describe( 'useSiteUsage', () => {
( useAuth as jest.Mock ).mockReturnValue( {
client: {
req: {
get: clientReqGet.mockResolvedValue( { site_count: 3, site_limit: 15 } ),
get: clientReqGet.mockResolvedValue( {
site_count: 3,
site_limit: 15,
} ),
},
},
} );
Expand Down
56 changes: 56 additions & 0 deletions src/hooks/use-fetch-snaphsots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as Sentry from '@sentry/electron/renderer';
import { useCallback, useEffect, useState } from 'react';
import { useAuth } from './use-auth';
import { useOffline } from './use-offline';
import { useSiteUsage } from './use-site-usage';

interface FetchSnapshotResponse {
sites: { atomic_site_id: number }[];
}

export function useFetchSnapshots() {
const [ allSnapshots, setAllSnapshots ] = useState< Pick< Snapshot, 'atomicSiteId' >[] | null >(
null
);
const { siteCount } = useSiteUsage();
const [ isLoading, setIsLoading ] = useState( false );
const { client } = useAuth();
const isOffline = useOffline();

const fetchAllSnapshots = useCallback( async () => {
if ( ! client?.req || isOffline ) {
return null;
}
setIsLoading( true );
try {
const response: FetchSnapshotResponse = await client.req.get( {
path: '/jurassic-ninja/list',
apiNamespace: 'wpcom/v2',
} );
const sites: Pick< Snapshot, 'atomicSiteId' >[] =
response.sites?.map( ( { atomic_site_id } ) => ( { atomicSiteId: atomic_site_id } ) ) ?? [];
return sites;
} catch ( error ) {
Sentry.captureException( error );
} finally {
setIsLoading( false );
}
}, [ client?.req, isOffline ] );

useEffect( () => {
if ( ! client ) {
return;
}
const fetchSnapshots = async () => {
const sites = await fetchAllSnapshots();
if ( ! sites ) {
return;
}
setAllSnapshots( sites );
setIsLoading( false );
};
fetchSnapshots();
}, [ client, fetchAllSnapshots, siteCount ] );

return { fetchAllSnapshots, allSnapshots, isLoading };
}
26 changes: 26 additions & 0 deletions src/hooks/use-site-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { __ } from '@wordpress/i18n';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { getIpcApi } from '../lib/get-ipc-api';
import { useDeleteSnapshot } from './use-delete-snapshot';
import { useFetchSnapshots } from './use-fetch-snaphsots';

interface SiteDetailsContext {
selectedSite: SiteDetails | null;
Expand Down Expand Up @@ -70,6 +71,7 @@ function useSelectedSite( firstSiteId: string | null ) {
}

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

Expand All @@ -83,6 +85,25 @@ function useSnapshots() {
} );
}, [] );

const clearFloatingSnapshots = useCallback( function clearFloatingSnapshots(
allSnapshots: Pick< Snapshot, 'atomicSiteId' >[]
) {
const siteIds = allSnapshots.map( ( snapshot ) => snapshot.atomicSiteId );
if ( ! siteIds.length ) {
setSnapshots( [] );
return;
}
setSnapshots( ( snapshots ) =>
snapshots.filter( ( snapshot ) => siteIds.includes( snapshot.atomicSiteId ) )
);
}, [] );

useEffect( () => {
if ( initiated && ! loadingServerSnapshots && allSnapshots ) {
clearFloatingSnapshots( allSnapshots );
}
}, [ allSnapshots, clearFloatingSnapshots, initiated, loadingServerSnapshots ] );

// Save snapshots to storage when they change
useEffect( () => {
if ( ! initiated ) {
Expand All @@ -101,6 +122,11 @@ function useSnapshots() {
( snapshotI ) => snapshotI.atomicSiteId === snapshot.atomicSiteId
);
if ( index === -1 ) {
if ( snapshot.isDeleting ) {
const newSnapshots = [ ...snapshots ];
newSnapshots.push( snapshot as Snapshot );
return newSnapshots;
}
return snapshots;
}
const newSnapshots = [ ...snapshots ];
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/use-site-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { useAuth } from './use-auth';
import { useOffline } from './use-offline';
import { useSiteDetails } from './use-site-details';

export interface UsageSite {
atomic_site_id: number;
}

export function useSiteUsage() {
const [ isLoading, setIsLoading ] = useState( false );
const { snapshots } = useSiteDetails();
Expand Down Expand Up @@ -49,8 +53,9 @@ export function useSiteUsage() {
setSiteLimit( LIMIT_OF_ZIP_SITES_PER_USER );
return;
}
setSiteCount( response.site_count );
setSiteLimit( response.site_limit );
const { site_count, site_limit } = response;
setSiteCount( site_count );
setSiteLimit( site_limit );
};
fetchStats();
}, [ fetchSiteUsage, snapshots.length, client ] );
Expand Down