diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index 483c1c3b9e..7c71ee9d2d 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -1,5 +1,4 @@
import { resolve } from 'path';
-import { pathToFileURL } from 'url';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import topLevelAwait from 'vite-plugin-top-level-await';
diff --git a/src/components/content-tab-import-export.tsx b/src/components/content-tab-import-export.tsx
index f91b83ff92..8e03dbf9dd 100644
--- a/src/components/content-tab-import-export.tsx
+++ b/src/components/content-tab-import-export.tsx
@@ -11,6 +11,7 @@ import ProgressBar from 'src/components/progress-bar';
import { Tooltip } from 'src/components/tooltip';
import { ACCEPTED_IMPORT_FILE_TYPES } from 'src/constants';
import { useSyncSites } from 'src/hooks/sync-sites/sync-sites-context';
+import { useAuth } from 'src/hooks/use-auth';
import { useConfirmationDialog } from 'src/hooks/use-confirmation-dialog';
import { useDragAndDropFile } from 'src/hooks/use-drag-and-drop-file';
import { useImportExport } from 'src/hooks/use-import-export';
@@ -19,7 +20,7 @@ import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { getLocalizedLink } from 'src/lib/get-localized-link';
import { useI18nLocale } from 'src/stores';
-import { useConnectedSitesData } from 'src/stores/sync';
+import { useGetConnectedSitesForLocalSiteQuery } from 'src/stores/sync/connected-sites';
interface ContentTabImportExportProps {
selectedSite: SiteDetails;
@@ -320,7 +321,11 @@ export function ContentTabImportExport( { selectedSite }: ContentTabImportExport
const { __ } = useI18n();
const [ isSupported, setIsSupported ] = useState< boolean | null >( null );
const { isSiteIdPulling, isSiteIdPushing } = useSyncSites();
- const { connectedSites } = useConnectedSitesData();
+ const { user } = useAuth();
+ const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( {
+ localSiteId: selectedSite.id,
+ userId: user?.id,
+ } );
const isPulling = connectedSites.some( ( site ) => isSiteIdPulling( selectedSite.id, site.id ) );
const isPushing = connectedSites.some( ( site ) => isSiteIdPushing( selectedSite.id, site.id ) );
const isThisSiteSyncing = isPulling || isPushing;
diff --git a/src/components/publish-site-button.tsx b/src/components/publish-site-button.tsx
index 71ffe14cba..00df11a7e5 100644
--- a/src/components/publish-site-button.tsx
+++ b/src/components/publish-site-button.tsx
@@ -1,18 +1,28 @@
import { cloudUpload } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import Button from 'src/components/button';
+import { Tooltip } from 'src/components/tooltip';
import { useSyncSites } from 'src/hooks/sync-sites';
+import { useAuth } from 'src/hooks/use-auth';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useFeatureFlags } from 'src/hooks/use-feature-flags';
+import { useSiteDetails } from 'src/hooks/use-site-details';
import { useAppDispatch } from 'src/stores';
-import { connectedSitesActions, useConnectedSitesData } from 'src/stores/sync';
-import { Tooltip } from './tooltip';
+import {
+ connectedSitesActions,
+ useGetConnectedSitesForLocalSiteQuery,
+} from 'src/stores/sync/connected-sites';
export const PublishSiteButton = () => {
const { __ } = useI18n();
const dispatch = useAppDispatch();
const { setSelectedTab } = useContentTabs();
- const { connectedSites } = useConnectedSitesData();
+ const { user } = useAuth();
+ const { selectedSite } = useSiteDetails();
+ const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( {
+ localSiteId: selectedSite?.id,
+ userId: user?.id,
+ } );
const { isAnySitePulling, isAnySitePushing } = useSyncSites();
const { streamlineOnboarding } = useFeatureFlags();
const isAnySiteSyncing = isAnySitePulling || isAnySitePushing;
diff --git a/src/components/tests/content-tab-assistant.test.tsx b/src/components/tests/content-tab-assistant.test.tsx
index 88f9962892..ef4f1e7aad 100644
--- a/src/components/tests/content-tab-assistant.test.tsx
+++ b/src/components/tests/content-tab-assistant.test.tsx
@@ -15,9 +15,11 @@ import { ThemeDetailsProvider } from 'src/hooks/use-theme-details';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { store } from 'src/stores';
import { generateMessage, chatActions } from 'src/stores/chat-slice';
-import { testActions } from 'src/stores/tests/utils/test-reducer';
+import { testActions, testReducer } from 'src/stores/tests/utils/test-reducer';
import { useGetAssistantQuota, useGetWelcomeMessages } from 'src/stores/wpcom-api';
+store.replaceReducer( testReducer );
+
jest.mock( 'src/hooks/use-auth' );
jest.mock( 'src/hooks/use-offline' );
jest.mock( 'src/lib/get-ipc-api' );
diff --git a/src/components/tests/content-tab-import-export.test.tsx b/src/components/tests/content-tab-import-export.test.tsx
index 0d77021e10..f64662cc02 100644
--- a/src/components/tests/content-tab-import-export.test.tsx
+++ b/src/components/tests/content-tab-import-export.test.tsx
@@ -32,6 +32,7 @@ beforeEach( () => {
loadingServer: {},
} );
( getIpcApi as jest.Mock ).mockReturnValue( {
+ getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
showMessageBox: jest.fn().mockResolvedValue( { response: 0, checkboxChecked: false } ), // Mock showMessageBox
isImportExportSupported: jest.fn().mockResolvedValue( true ),
} );
diff --git a/src/components/tests/header.test.tsx b/src/components/tests/header.test.tsx
index f635ab47ef..44cf56894f 100644
--- a/src/components/tests/header.test.tsx
+++ b/src/components/tests/header.test.tsx
@@ -32,6 +32,7 @@ const mockedSites = [
function mockGetIpcApi( mocks: Record< string, jest.Mock > ) {
mockedGetIpcApi.mockReturnValue( {
+ getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
getSiteDetails: jest.fn( () => Promise.resolve( mockedSites ) ),
getSnapshots: jest.fn( () => Promise.resolve( [] ) ),
saveSnapshotsToStorage: jest.fn( () => Promise.resolve() ),
diff --git a/src/components/tests/main-sidebar.test.tsx b/src/components/tests/main-sidebar.test.tsx
index f14eba6f5e..c873f024b7 100644
--- a/src/components/tests/main-sidebar.test.tsx
+++ b/src/components/tests/main-sidebar.test.tsx
@@ -46,6 +46,7 @@ jest.mock( 'src/lib/get-ipc-api', () => ( {
__esModule: true,
default: jest.fn(),
getIpcApi: () => ( {
+ getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
showOpenFolderDialog: jest.fn(),
generateProposedSitePath: jest.fn(),
openURL: jest.fn(),
diff --git a/src/hooks/sync-sites/sync-sites-context.tsx b/src/hooks/sync-sites/sync-sites-context.tsx
index 9a88072662..5ad78dc83a 100644
--- a/src/hooks/sync-sites/sync-sites-context.tsx
+++ b/src/hooks/sync-sites/sync-sites-context.tsx
@@ -10,23 +10,22 @@ import {
mapImportResponseToPushState,
} from 'src/hooks/sync-sites/use-sync-push';
import { useAuth } from 'src/hooks/use-auth';
+import { useFetchWpComSites } from 'src/hooks/use-fetch-wpcom-sites';
import { useFormatLocalizedTimestamps } from 'src/hooks/use-format-localized-timestamps';
+import { useSiteDetails } from 'src/hooks/use-site-details';
import { useSyncStatesProgressInfo } from 'src/hooks/use-sync-states-progress-info';
import { getIpcApi } from 'src/lib/get-ipc-api';
-import { useAppDispatch } from 'src/stores';
-import { useConnectedSitesData, useSyncSitesData, connectedSitesActions } from 'src/stores/sync';
+import {
+ useGetConnectedSitesForLocalSiteQuery,
+ useUpdateSiteTimestampMutation,
+} from 'src/stores/sync/connected-sites';
import type { ImportResponse } from 'src/hooks/use-sync-states-progress-info';
type GetLastSyncTimeText = ( timestamp: string | null, type: 'pull' | 'push' ) => string;
-type UpdateSiteTimestamp = (
- siteId: number | undefined,
- localSiteId: string,
- type: 'pull' | 'push'
-) => Promise< void >;
export type SyncSitesContextType = Omit< UseSyncPull, 'pullStates' > &
Omit< UseSyncPush, 'pushStates' > &
- ReturnType< typeof useSyncSitesData > & {
+ ReturnType< typeof useFetchWpComSites > & {
getLastSyncTimeText: GetLastSyncTimeText;
};
@@ -54,46 +53,21 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } )
[ formatRelativeTime ]
);
- const { connectedSites } = useConnectedSitesData();
- const dispatch = useAppDispatch();
+ const { selectedSite } = useSiteDetails();
+ const { user } = useAuth();
+ const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( {
+ localSiteId: selectedSite?.id,
+ userId: user?.id,
+ } );
- const updateSiteTimestamp = useCallback< UpdateSiteTimestamp >(
- async ( siteId, localSiteIdParam, type ) => {
- const site = connectedSites.find(
- ( { id, localSiteId: siteLocalId } ) => siteId === id && localSiteIdParam === siteLocalId
- );
-
- if ( ! site ) {
- return;
- }
-
- try {
- const updatedSite = {
- ...site,
- [ type === 'pull' ? 'lastPullTimestamp' : 'lastPushTimestamp' ]: new Date().toISOString(),
- };
-
- await getIpcApi().updateSingleConnectedWpcomSite( updatedSite );
-
- dispatch(
- connectedSitesActions.updateSite( {
- localSiteId: localSiteIdParam,
- site: updatedSite,
- } )
- );
- } catch ( error ) {
- console.error( 'Failed to update timestamp:', error );
- }
- },
- [ connectedSites, dispatch ]
- );
+ const [ updateSiteTimestamp ] = useUpdateSiteTimestampMutation();
const { pullSite, isAnySitePulling, isSiteIdPulling, clearPullState, getPullState, cancelPull } =
useSyncPull( {
pullStates,
setPullStates,
onPullSuccess: ( remoteSiteId, localSiteId ) =>
- updateSiteTimestamp( remoteSiteId, localSiteId, 'pull' ),
+ updateSiteTimestamp( { siteId: remoteSiteId, localSiteId, type: 'pull' } ),
} );
const [ pushStates, setPushStates ] = useState< PushStates >( {} );
@@ -102,10 +76,12 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } )
pushStates,
setPushStates,
onPushSuccess: ( remoteSiteId, localSiteId ) =>
- updateSiteTimestamp( remoteSiteId, localSiteId, 'push' ),
+ updateSiteTimestamp( { siteId: remoteSiteId, localSiteId, type: 'push' } ),
} );
- const { syncSites, isFetching, refetchSites } = useSyncSitesData();
+ const { syncSites, isFetching, refetchSites } = useFetchWpComSites(
+ connectedSites.map( ( { id } ) => id )
+ );
useListenDeepLinkConnection( { refetchSites } );
const { client } = useAuth();
diff --git a/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/src/hooks/sync-sites/use-listen-deep-link-connection.ts
index b704831151..b90fe6ece0 100644
--- a/src/hooks/sync-sites/use-listen-deep-link-connection.ts
+++ b/src/hooks/sync-sites/use-listen-deep-link-connection.ts
@@ -2,14 +2,14 @@ import { SyncSitesContextType } from 'src/hooks/sync-sites/sync-sites-context';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { useSiteDetails } from 'src/hooks/use-site-details';
-import { useConnectedSitesOperations } from 'src/stores/sync';
+import { useConnectSiteMutation } from 'src/stores/sync/connected-sites';
export function useListenDeepLinkConnection( {
refetchSites,
}: {
refetchSites: SyncSitesContextType[ 'refetchSites' ];
} ) {
- const { connectSite } = useConnectedSitesOperations();
+ const [ connectSite ] = useConnectSiteMutation();
const { selectedSite, setSelectedSiteId } = useSiteDetails();
const { setSelectedTab, selectedTab } = useContentTabs();
@@ -22,7 +22,7 @@ export function useListenDeepLinkConnection( {
// Select studio site that started the sync
setSelectedSiteId( studioSiteId );
}
- await connectSite( newConnectedSite, studioSiteId );
+ await connectSite( { site: newConnectedSite, localSiteId: studioSiteId } );
if ( selectedTab !== 'sync' ) {
// Switch to sync tab
setSelectedTab( 'sync' );
diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx
index 5fcb6d9818..ac74e164b2 100644
--- a/src/hooks/tests/use-add-site.test.tsx
+++ b/src/hooks/tests/use-add-site.test.tsx
@@ -1,5 +1,4 @@
// Run tests: yarn test -- src/hooks/tests/use-add-site.test.tsx
-import { configureStore } from '@reduxjs/toolkit';
import { renderHook, act } from '@testing-library/react';
import nock from 'nock';
import { Provider } from 'react-redux';
@@ -8,8 +7,8 @@ import { useAddSite } from 'src/hooks/use-add-site';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { getWordPressProvider } from 'src/lib/wordpress-provider';
-import providerConstantsReducer from 'src/stores/provider-constants-slice';
-import { useConnectedSitesOperations } from 'src/stores/sync';
+import { store } from 'src/stores';
+import { setProviderConstants } from 'src/stores/provider-constants-slice';
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
jest.mock( 'src/hooks/use-site-details' );
@@ -23,10 +22,7 @@ jest.mock( 'src/hooks/use-import-export', () => ( {
} ),
} ) );
-jest.mock( 'src/stores/sync', () => ( {
- useConnectedSitesOperations: jest.fn(),
-} ) );
-
+const mockConnectWpcomSites = jest.fn().mockResolvedValue( undefined );
jest.mock( 'src/lib/get-ipc-api', () => ( {
getIpcApi: () => ( {
generateProposedSitePath: jest.fn().mockResolvedValue( {
@@ -37,32 +33,12 @@ jest.mock( 'src/lib/get-ipc-api', () => ( {
} ),
showNotification: jest.fn(),
getAllCustomDomains: jest.fn().mockResolvedValue( [] ),
+ connectWpcomSites: mockConnectWpcomSites,
+ getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
} ),
} ) );
-// Helper to create a store with preloaded provider constants
-function makeStoreWithProviderConstants( overrides = {} ) {
- return configureStore( {
- reducer: {
- providerConstants: providerConstantsReducer,
- // ...add other reducers as needed
- },
- preloadedState: {
- providerConstants: {
- defaultPhpVersion: '8.3',
- defaultWordPressVersion: 'latest',
- allowedPhpVersions: [ '8.0', '8.1', '8.2', '8.3' ],
- minimumWordPressVersion: '5.9.9',
- ...overrides,
- },
- },
- } );
-}
-
-const renderHookWithProvider = (
- hook: () => ReturnType< typeof useAddSite >,
- store = makeStoreWithProviderConstants()
-) => {
+const renderHookWithProvider = ( hook: () => ReturnType< typeof useAddSite > ) => {
return renderHook< ReturnType< typeof useAddSite >, void >( hook, {
wrapper: ( { children } ) => { children },
} );
@@ -72,13 +48,22 @@ describe( 'useAddSite', () => {
const mockCreateSite = jest.fn();
const mockUpdateSite = jest.fn();
const mockStartServer = jest.fn();
- const mockConnectSite = jest.fn();
const mockPullSite = jest.fn();
const mockSetSelectedTab = jest.fn();
beforeEach( () => {
jest.clearAllMocks();
+ // Prepopulate store with provider constants
+ store.dispatch(
+ setProviderConstants( {
+ defaultPhpVersion: '8.3',
+ defaultWordPressVersion: 'latest',
+ allowedPhpVersions: [ '8.0', '8.1', '8.2', '8.3' ],
+ minimumWordPressVersion: '5.9.9',
+ } )
+ );
+
( useSiteDetails as jest.Mock ).mockReturnValue( {
createSite: mockCreateSite,
updateSite: mockUpdateSite,
@@ -87,12 +72,6 @@ describe( 'useAddSite', () => {
startServer: mockStartServer,
} );
- mockConnectSite.mockResolvedValue( undefined );
-
- ( useConnectedSitesOperations as jest.Mock ).mockReturnValue( {
- connectSite: mockConnectSite,
- } );
-
mockPullSite.mockReset();
( useSyncSites as jest.Mock ).mockReturnValue( {
pullSite: mockPullSite,
@@ -296,7 +275,12 @@ describe( 'useAddSite', () => {
await result.current.handleAddSiteClick();
} );
- expect( mockConnectSite ).toHaveBeenCalledWith( remoteSite, createdSite.id );
+ expect( mockConnectWpcomSites ).toHaveBeenCalledWith( [
+ {
+ sites: [ remoteSite ],
+ localSiteId: createdSite.id,
+ },
+ ] );
expect( mockPullSite ).toHaveBeenCalledWith( remoteSite, createdSite, {
optionsToSync: [ 'all' ],
} );
diff --git a/src/hooks/tests/use-connected-sites-operations.test.tsx b/src/hooks/tests/use-connected-sites-operations.test.tsx
deleted file mode 100644
index ff18f1d1d5..0000000000
--- a/src/hooks/tests/use-connected-sites-operations.test.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { renderHook, waitFor } from '@testing-library/react';
-import { Provider } from 'react-redux';
-import { useAuth } from 'src/hooks/use-auth';
-import { useFetchWpComSites } from 'src/hooks/use-fetch-wpcom-sites';
-import { useSiteDetails } from 'src/hooks/use-site-details';
-import { store } from 'src/stores';
-import { useConnectedSitesData, useConnectedSitesOperations } from 'src/stores/sync';
-import { SyncSite } from '../use-fetch-wpcom-sites/types';
-
-jest.mock( 'src/hooks/use-auth' );
-jest.mock( 'src/hooks/use-site-details' );
-jest.mock( 'src/hooks/use-fetch-wpcom-sites' );
-jest.mock( 'src/stores/sync', () => ( {
- ...jest.requireActual( 'src/stores/sync' ),
- useConnectedSitesData: jest.fn(),
- useConnectedSitesOperations: jest.fn(),
-} ) );
-
-const mockConnectedWpcomSites: SyncSite[] = [
- {
- id: 6,
- localSiteId: '788a7e0c-62d2-427e-8b1a-e6d5ac84b61c',
- name: 'My simple business site',
- url: 'https://developer.wordpress.com/studio/',
- isStaging: false,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
- {
- id: 7,
- localSiteId: '788a7e0c-62d2-427e-8b1a-e6d5ac84b61c',
- name: 'Staging: My simple business site',
- url: 'https://developer-staging.wordpress.com/studio/',
- isStaging: true,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
-];
-
-const mockSyncSites: SyncSite[] = [
- {
- id: 8,
- localSiteId: '',
- name: 'My simple store',
- url: 'https://developer.wordpress.com/studio/store',
- isStaging: false,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
- {
- id: 9,
- localSiteId: '',
- name: 'Staging: My simple test store',
- url: 'https://developer-staging.wordpress.com/studio/test-store',
- isStaging: true,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
-];
-
-jest.mock( 'src/lib/get-ipc-api', () => ( {
- getIpcApi: () => ( {
- getConnectedWpcomSites: jest.fn().mockResolvedValue( mockConnectedWpcomSites ),
- connectWpcomSites: jest.fn(),
- disconnectWpcomSites: jest.fn(),
- updateConnectedWpcomSites: jest.fn(),
- } ),
-} ) );
-
-describe( 'useConnectedSitesOperations', () => {
- const wrapper = ( { children }: { children: React.ReactNode } ) => (
- { children }
- );
-
- const mockDisconnectSite = jest.fn().mockResolvedValue( [] );
- const mockConnectSite = jest.fn().mockResolvedValue( [ ...mockConnectedWpcomSites, { id: 6 } ] );
-
- beforeEach( () => {
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
- ( useSiteDetails as jest.Mock ).mockReturnValue( {
- selectedSite: { id: '788a7e0c-62d2-427e-8b1a-e6d5ac84b61c' },
- } );
- ( useFetchWpComSites as jest.Mock ).mockReturnValue( {
- syncSites: mockSyncSites,
- isFetching: false,
- } );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: mockConnectedWpcomSites,
- localSiteId: '788a7e0c-62d2-427e-8b1a-e6d5ac84b61c',
- } );
- ( useConnectedSitesOperations as jest.Mock ).mockReturnValue( {
- connectSite: mockConnectSite,
- disconnectSite: mockDisconnectSite,
- } );
- } );
-
- afterEach( () => {
- jest.clearAllMocks();
- } );
-
- it( 'loads connected sites on mount when authenticated', async () => {
- const { result } = renderHook( () => useConnectedSitesData(), { wrapper } );
-
- await waitFor( () => {
- expect( result.current.connectedSites ).toEqual( mockConnectedWpcomSites );
- } );
- } );
-
- it( 'does not load connected sites when not authenticated', async () => {
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false } );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [],
- loading: false,
- localSiteId: '788a7e0c-62d2-427e-8b1a-e6d5ac84b61c',
- } );
- const { result } = renderHook( () => useConnectedSitesData(), { wrapper } );
-
- await waitFor( () => {
- expect( result.current.connectedSites ).toEqual( [] );
- } );
- } );
-
- it( 'connects a site and its staging sites successfully', async () => {
- const { result } = renderHook( () => useConnectedSitesOperations(), { wrapper } );
- const siteToConnect = mockSyncSites[ 0 ];
-
- await waitFor( async () => {
- await result.current.connectSite( {
- ...siteToConnect,
- isPressable: false,
- syncSupport: 'syncable',
- } );
- } );
-
- await waitFor( () => {
- expect( mockConnectSite ).toHaveBeenCalledWith( siteToConnect );
- } );
- } );
-
- it( 'disconnects a site and its staging sites successfully', async () => {
- const { result } = renderHook( () => useConnectedSitesOperations(), { wrapper } );
- const { result: resultConnectedSites } = renderHook( () => useConnectedSitesData(), {
- wrapper,
- } );
- const siteToDisconnect = mockConnectedWpcomSites[ 0 ];
-
- await waitFor( () => {
- expect( resultConnectedSites.current.connectedSites ).toBeDefined();
- expect( resultConnectedSites.current.connectedSites ).toEqual( mockConnectedWpcomSites );
- } );
-
- await waitFor( async () => {
- await result.current.disconnectSite( siteToDisconnect.id );
- } );
-
- expect( mockDisconnectSite ).toHaveBeenCalledWith( siteToDisconnect.id );
- } );
-} );
diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts
index f8762494e7..50882ce492 100644
--- a/src/hooks/use-add-site.ts
+++ b/src/hooks/use-add-site.ts
@@ -13,7 +13,7 @@ import {
selectDefaultPhpVersion,
selectDefaultWordPressVersion,
} from 'src/stores/provider-constants-slice';
-import { useConnectedSitesOperations } from 'src/stores/sync';
+import { useConnectSiteMutation } from 'src/stores/sync/connected-sites';
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
import type { Blueprint } from 'src/stores/wpcom-api';
import type { SyncOption } from 'src/types';
@@ -22,7 +22,7 @@ export function useAddSite() {
const { __ } = useI18n();
const { createSite, data: sites, loadingSites, startServer, updateSite } = useSiteDetails();
const { importFile, clearImportState } = useImportExport();
- const { connectSite } = useConnectedSitesOperations();
+ const [ connectSite ] = useConnectSiteMutation();
const { pullSite } = useSyncSites();
const { setSelectedTab } = useContentTabs();
const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion );
@@ -153,7 +153,7 @@ export function useAddSite() {
} );
} else {
if ( selectedRemoteSite ) {
- await connectSite( selectedRemoteSite, newSite.id );
+ await connectSite( { site: selectedRemoteSite, localSiteId: newSite.id } );
const pullOptions: SyncOption[] = [ 'all' ];
pullSite( selectedRemoteSite, newSite, {
optionsToSync: pullOptions,
diff --git a/src/index.html b/src/index.html
deleted file mode 100644
index dd5c766413..0000000000
--- a/src/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
+ disconnectSite( { siteId: id, localSiteId: selectedSite.id } )
+ }
/>
setPendingModalMode( 'push' ) }
disabled={ isAnySiteSyncing || pendingModalMode !== null }
isBusy={ pendingModalMode === 'push' }
tooltipText={
@@ -250,7 +236,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
setPendingModalMode( 'pull' ) }
className={ isAnySiteSyncing ? '' : '!text-a8c-blue-50 !shadow-a8c-blue-50' }
disabled={ isAnySiteSyncing || pendingModalMode !== null }
isBusy={ pendingModalMode === 'pull' }
@@ -285,7 +271,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
{ reduxModalMode === 'connect' ? (
{
dispatch( connectedSitesActions.closeModal() );
} }
@@ -296,7 +282,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
} }
selectedSite={ selectedSite }
/>
- ) : syncSites.length === 0 ? (
+ ) : syncSites.length === 0 && ! isFetchingSyncSites ? (
{
dispatch( connectedSitesActions.closeModal() );
@@ -306,7 +292,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
) : (
{
dispatch( connectedSitesActions.closeModal() );
} }
@@ -338,7 +324,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
} }
onRequestClose={ () => {
setSelectedRemoteSite( null );
- dispatch( connectedSitesActions.setModalMode( null ) );
+ dispatch( connectedSitesActions.closeModal() );
} }
/>
) }
diff --git a/src/modules/sync/tests/index.test.tsx b/src/modules/sync/tests/index.test.tsx
index 16bb4524b9..5b1e49715b 100644
--- a/src/modules/sync/tests/index.test.tsx
+++ b/src/modules/sync/tests/index.test.tsx
@@ -6,33 +6,26 @@ import { SyncPushState } from 'src/hooks/sync-sites/use-sync-push';
import { useAuth } from 'src/hooks/use-auth';
import { ContentTabsProvider } from 'src/hooks/use-content-tabs';
import { useFeatureFlags } from 'src/hooks/use-feature-flags';
+import { useFetchWpComSites } from 'src/hooks/use-fetch-wpcom-sites';
import { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { ContentTabSync } from 'src/modules/sync';
import { useSelectedItemsPushSize } from 'src/modules/sync/hooks/use-selected-items-push-size';
import { store } from 'src/stores';
-import {
- useLatestRewindId,
- useRemoteFileTree,
- useConnectedSitesData,
- useSyncSitesData,
- useConnectedSitesOperations,
- connectedSitesSelectors,
-} from 'src/stores/sync';
+import { useLatestRewindId, useRemoteFileTree } from 'src/stores/sync';
+import { testActions, testReducer } from 'src/stores/tests/utils/test-reducer';
+
+store.replaceReducer( testReducer );
-jest.mock( 'src/hooks/use-auth' );
jest.mock( 'src/lib/get-ipc-api' );
+jest.mock( 'src/hooks/use-auth' );
+jest.mock( 'src/hooks/use-fetch-wpcom-sites' );
jest.mock( 'src/hooks/use-feature-flags' );
jest.mock( 'src/hooks/sync-sites/sync-sites-context', () => ( {
...jest.requireActual( '../../../hooks/sync-sites/sync-sites-context' ),
useSyncSites: jest.fn(),
} ) );
-jest.mock( 'src/stores', () => ( {
- ...jest.requireActual( 'src/stores' ),
- useAppDispatch: jest.fn(),
-} ) );
-
jest.mock( 'src/stores/sync', () => ( {
...jest.requireActual( 'src/stores/sync' ),
useLatestRewindId: jest.fn(),
@@ -41,9 +34,6 @@ jest.mock( 'src/stores/sync', () => ( {
error: null,
isLoading: false,
} ),
- useConnectedSitesData: jest.fn(),
- useSyncSitesData: jest.fn(),
- useConnectedSitesOperations: jest.fn(),
connectedSitesSelectors: {
selectIsModalOpen: jest.fn(),
selectModalMode: jest.fn(),
@@ -66,6 +56,7 @@ jest.mock( 'src/modules/sync/hooks/use-selected-items-push-size' );
const createAuthMock = ( isAuthenticated: boolean = false ) => ( {
isAuthenticated,
authenticate: jest.fn(),
+ user: isAuthenticated ? { id: 123, email: 'user@example.com' } : null,
} );
const selectedSite: SiteDetails = {
@@ -89,24 +80,30 @@ const inProgressPushState: SyncPushState = {
remoteSiteUrl: 'https://example.com',
};
-const fakeSyncSite = {
+const fakeSyncSite: SyncSite = {
id: 6,
- name: 'My simple business site that needs a transfer',
+ name: 'My simple business site',
url: 'https://developer.wordpress.com/studio/',
syncSupport: 'already-connected',
+ isStaging: false,
+ isPressable: false,
+ localSiteId: 'site-id',
+ lastPullTimestamp: null,
+ lastPushTimestamp: null,
};
describe( 'ContentTabSync', () => {
const mockSyncSites = {
pullSite: jest.fn(),
+ pushSite: jest.fn(),
isAnySitePulling: false,
isAnySitePushing: false,
getPullState: jest.fn(),
- getPushState: jest.fn().mockReturnValue( inProgressPushState ),
+ getPushState: jest.fn(),
updateTimestamp: jest.fn(),
- getLastSyncTimeWithType: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
+ getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
+ isSiteIdPulling: jest.fn().mockReturnValue( false ),
+ isSiteIdPushing: jest.fn().mockReturnValue( false ),
clearTimeout: jest.fn(),
};
@@ -114,19 +111,21 @@ describe( 'ContentTabSync', () => {
connectedSites: SyncSite[] = [],
syncSites: SyncSite[] = []
) => {
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites,
- loading: false,
- localSiteId: 'site-id',
- } );
- ( useSyncSitesData as jest.Mock ).mockReturnValue( {
+ // Update the IPC API mock to return the connected sites
+ const currentMock = ( getIpcApi as jest.Mock )();
+ currentMock.getConnectedWpcomSites.mockResolvedValue( connectedSites );
+
+ ( useFetchWpComSites as jest.Mock ).mockReturnValue( {
syncSites,
isFetching: false,
refetchSites: jest.fn(),
} );
};
+
beforeEach( () => {
jest.resetAllMocks();
+ store.dispatch( testActions.resetState() );
+ store.dispatch( { type: 'connectedSitesApi/resetApiState' } );
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( false ) );
( useFeatureFlags as jest.Mock ).mockReturnValue( {
enableBlueprints: true,
@@ -140,6 +139,7 @@ describe( 'ContentTabSync', () => {
updateConnectedWpcomSites: jest.fn(),
getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
getDirectorySize: jest.fn().mockResolvedValue( 0 ),
+ connectWpcomSites: jest.fn(),
listLocalFileTree: jest.fn().mockResolvedValue( [
{
name: 'plugins',
@@ -178,28 +178,12 @@ describe( 'ContentTabSync', () => {
error: null,
} );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [],
- loading: false,
- localSiteId: 'site-id',
- } );
-
- ( useSyncSitesData as jest.Mock ).mockReturnValue( {
+ ( useFetchWpComSites as jest.Mock ).mockReturnValue( {
syncSites: [],
isFetching: false,
refetchSites: jest.fn(),
} );
- ( useConnectedSitesOperations as jest.Mock ).mockReturnValue( {
- connectSite: jest.fn(),
- disconnectSite: jest.fn(),
- } );
-
- const { useAppDispatch } = jest.requireMock( 'src/stores' );
- useAppDispatch.mockReturnValue( jest.fn() );
-
- ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( false );
- ( connectedSitesSelectors.selectModalMode as jest.Mock ).mockReturnValue( null );
( useRemoteFileTree as jest.Mock ).mockReturnValue( {
fetchChildren: jest.fn().mockResolvedValue( [
{
@@ -287,95 +271,51 @@ describe( 'ContentTabSync', () => {
expect( importButton ).toBeInTheDocument();
} );
- it( 'opens the site selector modal when clicking import button', () => {
- const mockSyncSite: SyncSite = {
- id: 123,
- name: 'Test Site',
- url: 'https://example.wordpress.com',
- isStaging: false,
- syncSupport: 'already-connected',
- localSiteId: 'site-id',
- isPressable: false,
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- };
-
+ it( 'opens the site selector modal when clicking "Pull site" button', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
( useFeatureFlags as jest.Mock ).mockReturnValue( {
enableBlueprints: true,
streamlineOnboarding: true,
} );
- setupConnectedSitesMocks( [], [ mockSyncSite ] );
- ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true );
- ( connectedSitesSelectors.selectModalMode as jest.Mock ).mockReturnValue( 'pull' );
+ setupConnectedSitesMocks( [], [ fakeSyncSite ] );
renderWithProvider( );
- expect( screen.getByTestId( 'sync-sites-modal-selector' ) ).toBeInTheDocument();
+ const pullSiteButton = await screen.findByRole( 'button', { name: 'Pull site' } );
+ fireEvent.click( pullSiteButton );
+
+ expect( await screen.findByTestId( 'sync-sites-modal-selector' ) ).toBeInTheDocument();
} );
it( 'displays the list of connected sites', async () => {
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- isStaging: false,
- syncSupport: 'already-connected',
- };
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- loading: false,
- localSiteId: 'site-id',
- } );
- ( useSyncSites as jest.Mock ).mockReturnValue( {
- pullSite: jest.fn(),
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn().mockReturnValue( undefined ),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- expect( screen.getByText( fakeSyncSite.name ) ).toBeInTheDocument();
+ await screen.findByText( fakeSyncSite.name );
expect( screen.getByRole( 'button', { name: /Disconnect/i } ) ).toBeInTheDocument();
- expect( screen.getByRole( 'button', { name: /Pull/i } ) ).toBeInTheDocument();
- expect( screen.getByRole( 'button', { name: /Push/i } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Pull' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Push' } ) ).toBeInTheDocument();
expect( screen.getByText( 'Production' ) ).toBeInTheDocument();
} );
it( 'opens URL for connected sites', async () => {
- const fakeSyncSite = {
+ const fakeSyncSite: SyncSite = {
id: 6,
name: 'My simple business site that needs a transfer',
url: 'https://developer.wordpress.com/studio/',
isStaging: false,
syncSupport: 'already-connected',
+ localSiteId: 'site-id',
+ isPressable: false,
+ lastPullTimestamp: null,
+ lastPushTimestamp: null,
};
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- loading: false,
- localSiteId: 'site-id',
- } );
- ( useSyncSites as jest.Mock ).mockReturnValue( {
- pullSite: jest.fn(),
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn().mockReturnValue( inProgressPushState ),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const readableUrl = fakeSyncSite.url.replace( 'https://', '' );
- const urlButton = screen.getByRole( 'button', {
+ const readableUrl = fakeSyncSite.url.replace( /^https?:\/\//, '' );
+ const urlButton = await screen.findByRole( 'button', {
name: ( content ) => content.includes( readableUrl ),
} );
expect( urlButton ).toBeInTheDocument();
@@ -384,26 +324,17 @@ describe( 'ContentTabSync', () => {
expect( getIpcApi().openURL ).toHaveBeenCalledWith( fakeSyncSite.url );
} );
- it( 'opens the modal and displays the create new site button', () => {
- const mockSyncSite: SyncSite = {
- id: 123,
- name: 'Test Site',
- url: 'https://example.wordpress.com',
- isStaging: false,
- syncSupport: 'already-connected',
- localSiteId: 'site-id',
- isPressable: false,
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- };
-
+ it( 'opens the modal and displays the create new site button', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- setupConnectedSitesMocks( [], [ mockSyncSite ] );
- ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true );
- ( connectedSitesSelectors.selectModalMode as jest.Mock ).mockReturnValue( 'connect' );
+ setupConnectedSitesMocks( [], [ fakeSyncSite ] );
renderWithProvider( );
- const createNewSiteButton = screen.getByRole( 'button', {
+
+ const connectSiteButton = await screen.findByRole( 'button', { name: 'Connect site' } );
+ expect( connectSiteButton ).toBeInTheDocument();
+ fireEvent.click( connectSiteButton );
+
+ const createNewSiteButton = await screen.findByRole( 'button', {
name: /Create a new WordPress.com site ↗/i,
} );
expect( createNewSiteButton ).toBeInTheDocument();
@@ -424,7 +355,7 @@ describe( 'ContentTabSync', () => {
expect( importButton ).toBeInTheDocument();
} );
- it( 'displays environment badges for Pressable sites with production, staging and development environments', () => {
+ it( 'displays environment badges for Pressable sites with production, staging and development environments', async () => {
const fakePressableProductionSite: SyncSite = {
id: 6,
name: 'My Pressable Production site',
@@ -469,24 +400,9 @@ describe( 'ContentTabSync', () => {
fakePressableDevelopmentSite,
];
setupConnectedSitesMocks( allSites, [ fakePressableProductionSite ] );
-
- ( useSyncSites as jest.Mock ).mockReturnValue( {
- connectedSites: allSites,
- syncSites: [ fakePressableProductionSite ],
- pullSite: jest.fn(),
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn().mockReturnValue( undefined ),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
- } );
-
renderWithProvider( );
- expect( screen.getByText( fakePressableProductionSite.name ) ).toBeInTheDocument();
+ await screen.findByText( fakePressableProductionSite.name );
expect( screen.getByText( fakePressableStagingSite.name ) ).toBeInTheDocument();
expect( screen.getByText( fakePressableDevelopmentSite.name ) ).toBeInTheDocument();
@@ -494,70 +410,37 @@ describe( 'ContentTabSync', () => {
expect( screen.getByText( 'Staging' ) ).toBeInTheDocument();
expect( screen.getByText( 'Development' ) ).toBeInTheDocument();
} );
- it( 'displays the progress bar when the site is being pushed', () => {
+ it( 'displays the progress bar when the site is being pushed', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- const fakeSyncSite: SyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
- isStaging: false,
- localSiteId: 'site-id',
- isPressable: false,
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- };
-
setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
( useSyncSites as jest.Mock ).mockReturnValue( {
- pullSite: jest.fn(),
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
+ ...mockSyncSites,
getPushState: jest.fn().mockReturnValue( inProgressPushState ),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
isSiteIdPushing: jest.fn().mockReturnValue( true ),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- expect( screen.getByRole( 'progressbar' ) ).toBeInTheDocument();
+ await screen.findByRole( 'progressbar' );
} );
it( 'opens sync pullSite dialog with development environment label', async () => {
const mockPullSite = jest.fn();
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
+ const fakeDevelopmentSyncSite: SyncSite = {
+ ...fakeSyncSite,
isPressable: true,
environmentType: 'development',
};
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeDevelopmentSyncSite ], [ fakeDevelopmentSyncSite ] );
( useSyncSites as jest.Mock ).mockReturnValue( {
- syncSites: [ fakeSyncSite ],
+ ...mockSyncSites,
+ syncSites: [ fakeDevelopmentSyncSite ],
pullSite: mockPullSite,
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn(),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( pullButton ).toBeInTheDocument();
fireEvent.click( pullButton );
@@ -569,33 +452,21 @@ describe( 'ContentTabSync', () => {
it( 'opens sync pullSite dialog and displays production when the environment is not supported', async () => {
const mockPullSite = jest.fn();
- ( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
+ ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
+ const fakeDevelopmentSyncSite: SyncSite = {
+ ...fakeSyncSite,
isPressable: true,
environmentType: 'non-supported-environment-example-or-sandbox',
};
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeDevelopmentSyncSite ], [ fakeDevelopmentSyncSite ] );
( useSyncSites as jest.Mock ).mockReturnValue( {
+ ...mockSyncSites,
pullSite: mockPullSite,
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn(),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( pullButton ).toBeInTheDocument();
fireEvent.click( pullButton );
@@ -608,30 +479,15 @@ describe( 'ContentTabSync', () => {
it( 'calls pullSite with correct optionsToSync when all options are selected', async () => {
const mockPullSite = jest.fn();
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
- };
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSyncSites as jest.Mock ).mockReturnValue( {
+ ...mockSyncSites,
pullSite: mockPullSite,
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn(),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( pullButton ).toBeInTheDocument();
fireEvent.click( pullButton );
@@ -642,8 +498,8 @@ describe( 'ContentTabSync', () => {
const databaseCheckbox = screen.getByRole( 'checkbox', { name: 'Database' } );
fireEvent.click( databaseCheckbox );
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } );
- fireEvent.click( dialogPullButton[ 1 ] );
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
+ fireEvent.click( dialogPullButton );
expect( mockPullSite ).toHaveBeenCalledWith( fakeSyncSite, selectedSite, {
optionsToSync: [ 'all' ],
@@ -653,30 +509,15 @@ describe( 'ContentTabSync', () => {
it( 'calls pullSite with correct optionsToSync when only database is selected', async () => {
const mockPullSite = jest.fn();
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
- };
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSyncSites as jest.Mock ).mockReturnValue( {
+ ...mockSyncSites,
pullSite: mockPullSite,
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn(),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( pullButton ).toBeInTheDocument();
fireEvent.click( pullButton );
@@ -685,8 +526,8 @@ describe( 'ContentTabSync', () => {
const databaseCheckbox = screen.getByRole( 'checkbox', { name: 'Database' } );
fireEvent.click( databaseCheckbox );
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } );
- fireEvent.click( dialogPullButton[ 1 ] );
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
+ fireEvent.click( dialogPullButton );
expect( mockPullSite ).toHaveBeenCalledWith( fakeSyncSite, selectedSite, {
optionsToSync: [ 'sqls' ],
@@ -761,30 +602,16 @@ describe( 'ContentTabSync', () => {
error: null,
isLoading: false,
} );
- const fakeSyncSite = {
- id: 6,
- name: 'My simple business site that needs a transfer',
- url: 'https://developer.wordpress.com/studio/',
- syncSupport: 'already-connected',
- };
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSyncSites as jest.Mock ).mockReturnValue( {
+ ...mockSyncSites,
pullSite: mockPullSite,
- isAnySitePulling: false,
- isAnySitePushing: false,
- getPullState: jest.fn(),
- getPushState: jest.fn(),
- getLastSyncTimeText: jest.fn().mockReturnValue( 'You have not pulled this site yet.' ),
- isSiteIdPulling: jest.fn(),
- isSiteIdPushing: jest.fn(),
- clearTimeout: jest.fn(),
} );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( pullButton ).toBeInTheDocument();
fireEvent.click( pullButton );
@@ -803,8 +630,8 @@ describe( 'ContentTabSync', () => {
const uploadsCheckbox = screen.getByRole( 'checkbox', { name: 'uploads' } );
fireEvent.click( uploadsCheckbox );
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } );
- fireEvent.click( dialogPullButton[ 1 ] );
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
+ fireEvent.click( dialogPullButton );
expect( mockPullSite ).toHaveBeenCalledWith( fakeSyncSite, selectedSite, {
optionsToSync: [ 'paths', 'sqls' ],
@@ -814,45 +641,39 @@ describe( 'ContentTabSync', () => {
it( 'disables the pull button when all checkboxes are unchecked, which is the initial state', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
fireEvent.click( pullButton );
await screen.findByText( 'Pull from Production' );
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } )[ 1 ];
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( dialogPullButton ).toBeDisabled();
} );
it( 'disables the push button when all checkboxes are unchecked, which is the initial state', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
- const dialogPushButton = screen.getAllByRole( 'button', { name: /Push/i } )[ 1 ];
+ const dialogPushButton = await screen.findByRole( 'button', { name: 'Push' } );
expect( dialogPushButton ).toBeDisabled();
} );
it( 'enables the pull button when at least one checkbox is checked', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
fireEvent.click( pullButton );
await screen.findByText( 'Pull from Production' );
@@ -861,19 +682,17 @@ describe( 'ContentTabSync', () => {
const databaseCheckbox = screen.getByRole( 'checkbox', { name: 'Database' } );
fireEvent.click( databaseCheckbox );
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } )[ 1 ];
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( dialogPullButton ).toBeEnabled();
} );
it( 'enables the pull button when at least one checkbox children is checked', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
fireEvent.click( pullButton );
await screen.findByText( 'Pull from Production' );
@@ -889,53 +708,48 @@ describe( 'ContentTabSync', () => {
expect( databaseCheckbox ).not.toBeChecked();
expect( filesAndFoldersCheckbox ).not.toBeChecked();
- const dialogPullButton = screen.getAllByRole( 'button', { name: /Pull/i } )[ 1 ];
+ const dialogPullButton = await screen.findByRole( 'button', { name: 'Pull' } );
expect( dialogPullButton ).toBeEnabled();
} );
+
it( 'disables the push button when all checkboxes are unchecked', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
- const dialogPushButton = screen.getAllByRole( 'button', { name: /Push/i } )[ 1 ];
+ const dialogPushButton = await screen.findByRole( 'button', { name: 'Push' } );
expect( dialogPushButton ).toBeDisabled();
} );
it( 'enables the push button when at least one checkbox is checked', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
const databaseCheckbox = screen.getByRole( 'checkbox', { name: 'Database' } );
fireEvent.click( databaseCheckbox );
- const dialogPushButton = screen.getAllByRole( 'button', { name: /Push/i } )[ 1 ];
+ const dialogPushButton = await screen.findByRole( 'button', { name: 'Push' } );
expect( dialogPushButton ).toBeEnabled();
} );
it( 'enables the push button when at least one checkbox children is checked', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
@@ -952,18 +766,14 @@ describe( 'ContentTabSync', () => {
expect( databaseCheckbox ).not.toBeChecked();
expect( filesAndFoldersCheckbox ).not.toBeChecked();
- const dialogPushButton = screen.getAllByRole( 'button', { name: /Push/i } )[ 1 ];
+ const dialogPushButton = await screen.findByRole( 'button', { name: 'Push' } );
expect( dialogPushButton ).toBeEnabled();
} );
describe( 'Sync Dialog Push Selection Over Limit Notice', () => {
it( 'shows warning notice when push selection exceeds limit', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- loading: false,
- localSiteId: 'site-id',
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSelectedItemsPushSize as jest.Mock ).mockReturnValue( {
isPushSelectionOverLimit: true,
isLoading: false,
@@ -971,7 +781,7 @@ describe( 'ContentTabSync', () => {
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
@@ -979,17 +789,13 @@ describe( 'ContentTabSync', () => {
const warningNotice = screen.getByTestId( 'push-selection-over-limit-notice' );
expect( warningNotice ).toBeInTheDocument();
- const dialogPushButton = screen.getAllByRole( 'button', { name: /Push/i } )[ 1 ];
+ const dialogPushButton = await screen.findByRole( 'button', { name: 'Push' } );
expect( dialogPushButton ).toBeDisabled();
} );
it( 'does not show warning notice when push selection is within limit', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- loading: false,
- localSiteId: 'site-id',
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSelectedItemsPushSize as jest.Mock ).mockReturnValue( {
isPushSelectionOverLimit: false,
isLoading: false,
@@ -997,7 +803,7 @@ describe( 'ContentTabSync', () => {
renderWithProvider( );
- const pushButton = screen.getByRole( 'button', { name: /Push/i } );
+ const pushButton = await screen.findByRole( 'button', { name: 'Push' } );
fireEvent.click( pushButton );
await screen.findByText( 'Push to Production' );
@@ -1008,11 +814,7 @@ describe( 'ContentTabSync', () => {
it( 'does not show warning notice for pull operations even when limit exceeded', async () => {
( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) );
- ( useConnectedSitesData as jest.Mock ).mockReturnValue( {
- connectedSites: [ fakeSyncSite ],
- loading: false,
- localSiteId: 'site-id',
- } );
+ setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] );
( useSelectedItemsPushSize as jest.Mock ).mockReturnValue( {
isPushSelectionOverLimit: true,
isLoading: false,
@@ -1020,7 +822,7 @@ describe( 'ContentTabSync', () => {
renderWithProvider( );
- const pullButton = screen.getByRole( 'button', { name: /Pull/i } );
+ const pullButton = await screen.findByRole( 'button', { name: 'Pull' } );
fireEvent.click( pullButton );
await screen.findByText( 'Pull from Production' );
diff --git a/src/stores/index.ts b/src/stores/index.ts
index b86862e89c..2ba05c343f 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -25,10 +25,7 @@ import {
snapshotActions,
} from 'src/stores/snapshot-slice';
import { syncReducer } from 'src/stores/sync';
-import {
- connectedSitesReducer,
- loadAllConnectedSites,
-} from 'src/stores/sync/connected-sites-slice';
+import { connectedSitesApi, connectedSitesReducer } from 'src/stores/sync/connected-sites';
import { wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api';
import { wordpressVersionsApi } from './wordpress-versions-api';
import type { SupportedLocale } from 'common/lib/locale';
@@ -43,6 +40,7 @@ export type RootState = {
providerConstants: ReturnType< typeof providerConstantsReducer >;
snapshot: ReturnType< typeof snapshotReducer >;
sync: ReturnType< typeof syncReducer >;
+ connectedSitesApi: ReturnType< typeof connectedSitesApi.reducer >;
connectedSites: ReturnType< typeof connectedSitesReducer >;
wordpressVersionsApi: ReturnType< typeof wordpressVersionsApi.reducer >;
wpcomApi: ReturnType< typeof wpcomApi.reducer >;
@@ -96,11 +94,12 @@ export const rootReducer = combineReducers( {
chat: chatReducer,
newSites: newSitesReducer,
installedAppsApi: installedAppsApi.reducer,
+ connectedSitesApi: connectedSitesApi.reducer,
+ connectedSites: connectedSitesReducer,
onboarding: onboardingReducer,
providerConstants: providerConstantsReducer,
snapshot: snapshotReducer,
sync: syncReducer,
- connectedSites: connectedSitesReducer,
wordpressVersionsApi: wordpressVersionsApi.reducer,
wpcomApi: wpcomApi.reducer,
wpcomPublicApi: wpcomPublicApi.reducer,
@@ -115,6 +114,7 @@ export const store = configureStore( {
.prepend( listenerMiddleware.middleware )
.concat( appVersionApi.middleware )
.concat( installedAppsApi.middleware )
+ .concat( connectedSitesApi.middleware )
.concat( wordpressVersionsApi.middleware )
.concat( wpcomApi.middleware )
.concat( wpcomPublicApi.middleware )
@@ -143,8 +143,6 @@ async function initializeProviderConstants() {
// Initialize provider constants immediately, but skip in test environment
if ( typeof jest === 'undefined' && process.env.NODE_ENV !== 'test' ) {
void initializeProviderConstants();
- // Initialize connected sites on store initialization only in non-test environment
- void store.dispatch( loadAllConnectedSites() );
// Initialize beta features on store initialization only in non-test environment
void store.dispatch( loadBetaFeatures() );
}
diff --git a/src/stores/sync/connected-sites-hooks.ts b/src/stores/sync/connected-sites-hooks.ts
deleted file mode 100644
index f91f64ad98..0000000000
--- a/src/stores/sync/connected-sites-hooks.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { useCallback } from 'react';
-import { useFetchWpComSites } from 'src/hooks/use-fetch-wpcom-sites';
-import { useSiteDetails } from 'src/hooks/use-site-details';
-import { useAppDispatch, useRootSelector } from 'src/stores';
-import {
- connectedSitesActions,
- connectedSitesSelectors,
- connectSite,
- disconnectSite,
- loadAllConnectedSites,
-} from 'src/stores/sync/connected-sites-slice';
-import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
-
-export const useConnectedSitesData = () => {
- const { selectedSite } = useSiteDetails();
- const localSiteId = selectedSite?.id;
- const connectedSites = useRootSelector( ( state ) =>
- connectedSitesSelectors.selectSitesByLocalSiteId( state, localSiteId )
- );
-
- return { connectedSites, localSiteId };
-};
-
-export const useSyncSitesData = () => {
- const { connectedSites } = useConnectedSitesData();
- const { syncSites, isFetching, refetchSites } = useFetchWpComSites(
- connectedSites.map( ( { id } ) => id )
- );
-
- return { syncSites, isFetching, refetchSites };
-};
-
-export const useConnectedSitesOperations = () => {
- const dispatch = useAppDispatch();
- const { localSiteId, connectedSites } = useConnectedSitesData();
-
- const connectSiteToLocal = useCallback(
- async ( site: SyncSite, overrideLocalSiteId?: string ) => {
- const targetLocalSiteId = overrideLocalSiteId || localSiteId;
-
- if ( ! targetLocalSiteId ) {
- throw new Error( 'No local site ID available' );
- }
-
- try {
- await dispatch(
- connectSite( {
- site,
- localSiteId: targetLocalSiteId,
- } )
- ).unwrap();
-
- dispatch( connectedSitesActions.closeModal() );
-
- if ( overrideLocalSiteId && overrideLocalSiteId !== localSiteId ) {
- await dispatch( loadAllConnectedSites() );
- }
- } catch ( error ) {
- console.error( 'Failed to connect site:', error );
- throw error;
- }
- },
- [ dispatch, localSiteId ]
- );
-
- const disconnectSiteFromLocal = useCallback(
- async ( siteId: number ) => {
- if ( ! localSiteId ) {
- throw new Error( 'No local site ID available' );
- }
-
- try {
- const siteToDisconnect = connectedSites.find( ( site ) => site.id === siteId );
-
- if ( ! siteToDisconnect ) {
- throw new Error( 'Site not found' );
- }
-
- await dispatch( disconnectSite( { siteId, localSiteId } ) ).unwrap();
- } catch ( error ) {
- console.error( 'Failed to disconnect site:', error );
- throw error;
- }
- },
- [ dispatch, localSiteId, connectedSites ]
- );
-
- return {
- connectSite: connectSiteToLocal,
- disconnectSite: disconnectSiteFromLocal,
- };
-};
diff --git a/src/stores/sync/connected-sites-slice.ts b/src/stores/sync/connected-sites-slice.ts
deleted file mode 100644
index 2e78853e10..0000000000
--- a/src/stores/sync/connected-sites-slice.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
-import fastDeepEqual from 'fast-deep-equal';
-import { getIpcApi } from 'src/lib/get-ipc-api';
-import { RootState, store } from 'src/stores';
-import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
-import type { SyncModalMode } from 'src/modules/sync/types';
-
-type ConnectedSites = SyncSite[];
-type ModalState = false | true | { disconnectSiteId?: number };
-
-interface ConnectedSitesState {
- sites: Record< string, ConnectedSites >; // Keyed by localSiteId for efficient lookups
- isModalOpen: ModalState;
- modalMode: SyncModalMode | null;
-}
-
-interface ConnectSiteParams {
- site: SyncSite;
- localSiteId: string;
-}
-
-interface DisconnectSiteParams {
- siteId: number;
- localSiteId: string;
-}
-
-const initialState: ConnectedSitesState = {
- sites: {},
- isModalOpen: false,
- modalMode: null,
-};
-
-export const loadAllConnectedSites = createAsyncThunk( 'connectedSites/loadAll', async () => {
- const allSites = await getIpcApi().getConnectedWpcomSites();
-
- const sitesByLocalSiteId: Record< string, ConnectedSites > = {};
- allSites.forEach( ( site ) => {
- if ( ! sitesByLocalSiteId[ site.localSiteId ] ) {
- sitesByLocalSiteId[ site.localSiteId ] = [];
- }
- sitesByLocalSiteId[ site.localSiteId ].push( site );
- } );
-
- return sitesByLocalSiteId;
-} );
-
-export const connectSite = createAsyncThunk(
- 'connectedSites/connect',
- async ( { site, localSiteId }: ConnectSiteParams ) => {
- await getIpcApi().connectWpcomSites( [
- {
- sites: [ site ],
- localSiteId,
- },
- ] );
-
- const actualConnectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
-
- return {
- localSiteId,
- connectedSites: actualConnectedSites,
- };
- }
-);
-
-export const disconnectSite = createAsyncThunk(
- 'connectedSites/disconnect',
- async ( { siteId, localSiteId }: DisconnectSiteParams ) => {
- await getIpcApi().disconnectWpcomSites( [
- {
- siteIds: [ siteId ],
- localSiteId,
- },
- ] );
-
- const actualConnectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
-
- return {
- localSiteId,
- connectedSites: actualConnectedSites,
- };
- }
-);
-
-const connectedSitesSlice = createSlice( {
- name: 'connectedSites',
- initialState,
- reducers: {
- updateSite: ( state, action: PayloadAction< { localSiteId: string; site: SyncSite } > ) => {
- const { localSiteId, site } = action.payload;
- const sites = state.sites[ localSiteId ] || [];
- const index = sites.findIndex( ( s ) => s.id === site.id );
-
- if ( index !== -1 ) {
- sites[ index ] = site;
- }
- },
-
- clearSitesForLocalSite: ( state, action: PayloadAction< string > ) => {
- delete state.sites[ action.payload ];
- },
-
- openModal: ( state, action: PayloadAction< SyncModalMode | undefined > ) => {
- state.isModalOpen = true;
- if ( action.payload ) {
- state.modalMode = action.payload;
- }
- },
-
- setModalMode: ( state, action: PayloadAction< SyncModalMode | null > ) => {
- state.modalMode = action.payload;
- },
-
- closeModal: ( state ) => {
- state.isModalOpen = false;
- },
- },
- extraReducers: ( builder ) => {
- builder
- .addCase( loadAllConnectedSites.fulfilled, ( state, action ) => {
- state.sites = action.payload;
- } )
- .addCase( connectSite.fulfilled, ( state, action ) => {
- const { localSiteId, connectedSites } = action.payload;
- state.sites[ localSiteId ] = connectedSites;
- } )
- .addCase( disconnectSite.fulfilled, ( state, action ) => {
- const { localSiteId, connectedSites } = action.payload;
- state.sites[ localSiteId ] = connectedSites;
- } );
- },
-} );
-
-export const connectedSitesActions = connectedSitesSlice.actions;
-export const connectedSitesReducer = connectedSitesSlice.reducer;
-
-export const connectedSitesSelectors = {
- selectIsModalOpen: ( state: RootState ) => state.connectedSites.isModalOpen,
- selectModalMode: ( state: RootState ) => state.connectedSites.modalMode,
- selectSitesByLocalSiteId: createSelector(
- [
- ( state: RootState ) => state.connectedSites,
- ( _: RootState, localSiteId: string | undefined ) => localSiteId,
- ],
- ( connectedSitesState, localSiteId ) =>
- localSiteId ? connectedSitesState.sites[ localSiteId ] || [] : []
- ),
-};
-
-window.ipcListener.subscribe( 'user-data-updated', async ( _, userData ) => {
- const state = store.getState();
- const currentUserId = userData.authToken?.id;
-
- if ( ! currentUserId ) {
- return;
- }
-
- const connectedSitesFromUserData = userData.connectedWpcomSites?.[ currentUserId ] || [];
- const connectedSitesFromState = Object.values( state.connectedSites.sites ).flat();
-
- if ( ! fastDeepEqual( connectedSitesFromUserData, connectedSitesFromState ) ) {
- void store.dispatch( loadAllConnectedSites() );
- }
-} );
diff --git a/src/stores/sync/connected-sites.ts b/src/stores/sync/connected-sites.ts
new file mode 100644
index 0000000000..20a49ededc
--- /dev/null
+++ b/src/stores/sync/connected-sites.ts
@@ -0,0 +1,138 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { getIpcApi } from 'src/lib/get-ipc-api';
+import { RootState } from 'src/stores';
+import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
+import type { SyncModalMode } from 'src/modules/sync/types';
+
+type ConnectedSitesState = {
+ isModalOpen: boolean;
+ modalMode: SyncModalMode | null;
+};
+
+function getInitialState(): ConnectedSitesState {
+ return {
+ isModalOpen: false,
+ modalMode: null,
+ };
+}
+
+const connectedSitesSlice = createSlice( {
+ name: 'connectedSites',
+ initialState: getInitialState(),
+ reducers: {
+ openModal: ( state, action: PayloadAction< SyncModalMode | undefined > ) => {
+ state.isModalOpen = true;
+ if ( action.payload ) {
+ state.modalMode = action.payload;
+ }
+ },
+
+ closeModal: ( state ) => {
+ state.isModalOpen = false;
+ },
+ },
+} );
+
+export const connectedSitesActions = connectedSitesSlice.actions;
+export const connectedSitesReducer = connectedSitesSlice.reducer;
+export const connectedSitesSelectors = {
+ selectIsModalOpen: ( state: RootState ) => state.connectedSites.isModalOpen,
+ selectModalMode: ( state: RootState ) => state.connectedSites.modalMode,
+};
+
+export const connectedSitesApi = createApi( {
+ reducerPath: 'connectedSitesApi',
+ baseQuery: fetchBaseQuery(),
+ tagTypes: [ 'ConnectedSites' ],
+ endpoints: ( builder ) => ( {
+ getConnectedSitesForLocalSite: builder.query<
+ SyncSite[],
+ { localSiteId?: string; userId?: number }
+ >( {
+ queryFn: async ( { localSiteId } ) => {
+ if ( ! localSiteId ) {
+ return { data: [] };
+ }
+
+ const sites = await getIpcApi().getConnectedWpcomSites( localSiteId );
+ return { data: sites };
+ },
+ providesTags: ( result, error, arg ) => [
+ { type: 'ConnectedSites', localSiteId: arg.localSiteId, userId: arg.userId },
+ ],
+ } ),
+
+ connectSite: builder.mutation< SyncSite[], { site: SyncSite; localSiteId: string } >( {
+ queryFn: async ( { site, localSiteId } ) => {
+ await getIpcApi().connectWpcomSites( [
+ {
+ sites: [ site ],
+ localSiteId,
+ },
+ ] );
+
+ const actualConnectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
+
+ return { data: actualConnectedSites };
+ },
+ invalidatesTags: ( result, error, { localSiteId } ) => [
+ { type: 'ConnectedSites', localSiteId },
+ ],
+ } ),
+
+ disconnectSite: builder.mutation< SyncSite[], { siteId: number; localSiteId: string } >( {
+ queryFn: async ( { siteId, localSiteId } ) => {
+ await getIpcApi().disconnectWpcomSites( [
+ {
+ siteIds: [ siteId ],
+ localSiteId,
+ },
+ ] );
+
+ const actualConnectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
+
+ return { data: actualConnectedSites };
+ },
+ invalidatesTags: ( result, error, { localSiteId } ) => [
+ { type: 'ConnectedSites', localSiteId },
+ ],
+ } ),
+
+ updateSiteTimestamp: builder.mutation<
+ void,
+ { siteId: number; localSiteId: string; type: 'pull' | 'push' }
+ >( {
+ queryFn: async ( { siteId, localSiteId, type } ) => {
+ const connectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
+ const connectedSite = connectedSites.find(
+ ( { id, localSiteId: siteLocalId } ) => siteId === id && localSiteId === siteLocalId
+ );
+
+ if ( ! connectedSite ) {
+ return { error: { status: 'CUSTOM_ERROR', error: 'Site not found' } };
+ }
+
+ const timestampKey = type === 'pull' ? 'lastPullTimestamp' : 'lastPushTimestamp';
+ const updatedConnectedSite = {
+ ...connectedSite,
+ [ timestampKey ]: new Date().toISOString(),
+ };
+
+ await getIpcApi().updateSingleConnectedWpcomSite( updatedConnectedSite );
+
+ return { data: undefined };
+ },
+ invalidatesTags: ( result, error, { localSiteId } ) => [
+ { type: 'ConnectedSites', localSiteId },
+ ],
+ } ),
+ } ),
+} );
+
+export const {
+ useGetConnectedSitesForLocalSiteQuery,
+ useConnectSiteMutation,
+ useDisconnectSiteMutation,
+ useUpdateSiteTimestampMutation,
+} = connectedSitesApi;
diff --git a/src/stores/sync/index.ts b/src/stores/sync/index.ts
index c462f4f349..1b36091b56 100644
--- a/src/stores/sync/index.ts
+++ b/src/stores/sync/index.ts
@@ -1,17 +1,4 @@
export { syncReducer, syncActions, syncSelectors } from './sync-slice';
export { useLatestRewindId, useRemoteFileTree, useLocalFileTree } from './sync-hooks';
export { useGetLatestRewindIdQuery, fetchRemoteFileTree } from './sync-api';
-export {
- connectedSitesReducer,
- connectedSitesActions,
- connectedSitesSelectors,
- loadAllConnectedSites,
- connectSite,
- disconnectSite,
-} from './connected-sites-slice';
-export {
- useConnectedSitesData,
- useSyncSitesData,
- useConnectedSitesOperations,
-} from './connected-sites-hooks';
export * from './sync-types';