diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index f329986bfe..8f36cfedb1 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -3,12 +3,7 @@ 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 { useAppDispatch } from 'src/stores'; -import { - connectedSitesActions, - useConnectSiteMutation, - useGetConnectedSitesForLocalSiteQuery, -} from 'src/stores/sync/connected-sites'; -import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites'; +import { connectedSitesActions, useConnectSiteMutation } from 'src/stores/sync/connected-sites'; export function useListenDeepLinkConnection() { const dispatch = useAppDispatch(); @@ -16,37 +11,22 @@ export function useListenDeepLinkConnection() { const { selectedSite, setSelectedSiteId } = useSiteDetails(); const { setSelectedTab, selectedTab } = useContentTabs(); const { user } = useAuth(); - const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( { - localSiteId: selectedSite?.id, - userId: user?.id, - } ); - const connectedSiteIds = connectedSites.map( ( { id } ) => id ); - const { refetch: refetchWpComSites } = useGetWpComSitesQuery( { - connectedSiteIds, - userId: user?.id, - } ); useIpcListener( 'sync-connect-site', async ( _event, { remoteSiteId, studioSiteId, autoOpenPush } ) => { - // Fetch latest sites from network before checking - const result = await refetchWpComSites(); - const latestSites = result.data ?? []; - const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId ); - if ( newConnectedSite ) { - if ( selectedSite?.id && selectedSite.id !== studioSiteId ) { - // Select studio site that started the sync - setSelectedSiteId( studioSiteId ); - } - await connectSite( { site: newConnectedSite, localSiteId: studioSiteId } ); - if ( selectedTab !== 'sync' ) { - // Switch to sync tab - setSelectedTab( 'sync' ); - } - // Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button) - if ( autoOpenPush ) { - dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) ); - } + if ( selectedSite?.id && selectedSite.id !== studioSiteId ) { + // Select studio site that started the sync + setSelectedSiteId( studioSiteId ); + } + await connectSite( { remoteSiteId, localSiteId: studioSiteId, userId: user?.id } ); + if ( selectedTab !== 'sync' ) { + // Switch to sync tab + setSelectedTab( 'sync' ); + } + // Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button) + if ( autoOpenPush ) { + dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) ); } } ); diff --git a/apps/studio/src/hooks/tests/use-add-site.test.tsx b/apps/studio/src/hooks/tests/use-add-site.test.tsx index ffa99d2306..98efb7fbed 100644 --- a/apps/studio/src/hooks/tests/use-add-site.test.tsx +++ b/apps/studio/src/hooks/tests/use-add-site.test.tsx @@ -9,6 +9,7 @@ import { useContentTabs } from 'src/hooks/use-content-tabs'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { store } from 'src/stores'; import { setProviderConstants } from 'src/stores/provider-constants-slice'; +import { useConnectSiteMutation } from 'src/stores/sync/connected-sites'; import type { SyncSitesContextType } from 'src/hooks/sync-sites/sync-sites-context'; import type { SyncSite } from 'src/modules/sync/types'; @@ -16,6 +17,13 @@ vi.mock( 'src/hooks/use-site-details' ); vi.mock( 'src/hooks/use-feature-flags' ); vi.mock( 'src/hooks/sync-sites' ); vi.mock( 'src/hooks/use-content-tabs' ); +vi.mock( 'src/stores/sync/connected-sites', async ( importOriginal ) => { + const original = await importOriginal< typeof import('src/stores/sync/connected-sites') >(); + return { + ...original, + useConnectSiteMutation: vi.fn(), + }; +} ); vi.mock( 'src/hooks/use-import-export', () => ( { useImportExport: () => ( { importFile: vi.fn(), @@ -24,6 +32,7 @@ vi.mock( 'src/hooks/use-import-export', () => ( { } ), } ) ); +const mockConnectSite = vi.fn().mockReturnValue( { unwrap: () => Promise.resolve( [] ) } ); const mockConnectWpcomSites = vi.fn().mockResolvedValue( undefined ); const mockShowOpenFolderDialog = vi.fn(); const mockGenerateProposedSitePath = vi.fn().mockResolvedValue( { @@ -62,6 +71,11 @@ describe( 'useAddSite', () => { beforeEach( () => { vi.clearAllMocks(); + vi.mocked( useConnectSiteMutation ).mockReturnValue( [ + mockConnectSite, + { isLoading: false, reset: vi.fn() }, + ] as unknown as ReturnType< typeof useConnectSiteMutation > ); + // Prepopulate store with provider constants store.dispatch( setProviderConstants( { @@ -267,12 +281,10 @@ describe( 'useAddSite', () => { await result.current.handleCreateSite( formValues ); } ); - expect( mockConnectWpcomSites ).toHaveBeenCalledWith( [ - { - sites: [ remoteSite ], - localSiteId: createdSite.id, - }, - ] ); + expect( mockConnectSite ).toHaveBeenCalledWith( { + remoteSiteId: remoteSite.id, + localSiteId: createdSite.id, + } ); expect( mockPullSite ).toHaveBeenCalledWith( remoteSite, createdSite, { optionsToSync: [ 'all' ], } ); diff --git a/apps/studio/src/hooks/use-add-site.ts b/apps/studio/src/hooks/use-add-site.ts index 8ba7590196..9f66619fe5 100644 --- a/apps/studio/src/hooks/use-add-site.ts +++ b/apps/studio/src/hooks/use-add-site.ts @@ -277,7 +277,7 @@ export function useAddSite() { body: __( 'Your new site was imported' ), } ); } else if ( selectedRemoteSite ) { - await connectSite( { site: selectedRemoteSite, localSiteId: newSite.id } ); + await connectSite( { remoteSiteId: selectedRemoteSite.id, localSiteId: newSite.id } ); const pullOptions: SyncOption[] = [ 'all' ]; pullSite( selectedRemoteSite, newSite, { optionsToSync: pullOptions, diff --git a/apps/studio/src/modules/sync/index.tsx b/apps/studio/src/modules/sync/index.tsx index f04cc2caa3..9f6ec0f687 100644 --- a/apps/studio/src/modules/sync/index.tsx +++ b/apps/studio/src/modules/sync/index.tsx @@ -29,34 +29,34 @@ import { import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites'; import type { SyncSite } from 'src/modules/sync/types'; -function SiteSyncDescription( { children }: PropsWithChildren ) { +function SiteSyncDescription({ children }: PropsWithChildren) { const { __ } = useI18n(); return (
- { __( 'Sync with WordPress.com or Pressable' ) } + {__('Sync with WordPress.com or Pressable')}
- { __( + {__( 'Launch your existing WordPress.com or Jetpack-activated Pressable sites, or import an existing one. Then, share your work with the world.' - ) } + )}
- { [ - __( 'Push and pull changes from your live site.' ), - __( 'Supports staging and production sites.' ), - __( 'Sync database and file changes.' ), - ].map( ( text ) => ( -
- - { text } + {[ + __('Push and pull changes from your live site.'), + __('Supports staging and production sites.'), + __('Sync database and file changes.'), + ].map((text) => ( +
+ + {text}
- ) ) } + ))}
- { children } + {children}
@@ -69,49 +69,49 @@ function NoAuthSyncTab() { const isOffline = useOffline(); const { __ } = useI18n(); const { authenticate } = useAuth(); - const offlineMessage = __( "You're currently offline." ); + const offlineMessage = __("You're currently offline."); return (
- +
- { __( 'New to WordPress.com?' ) }{ ' ' } + {__('New to WordPress.com?')}{' '} @@ -121,94 +121,94 @@ function NoAuthSyncTab() { ); } -export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } ) { +export function ContentTabSync({ selectedSite }: { selectedSite: SiteDetails }) { const { __ } = useI18n(); const dispatch = useAppDispatch(); - const isModalOpen = useRootSelector( connectedSitesSelectors.selectIsModalOpen ); - const reduxModalMode = useRootSelector( connectedSitesSelectors.selectModalMode ); - const selectedRemoteSiteId = useRootSelector( - connectedSitesSelectors.selectSelectedRemoteSiteId - ); + const isModalOpen = useRootSelector(connectedSitesSelectors.selectIsModalOpen); + const reduxModalMode = useRootSelector(connectedSitesSelectors.selectModalMode); + const selectedRemoteSiteId = useRootSelector(connectedSitesSelectors.selectSelectedRemoteSiteId); const { isAuthenticated, user } = useAuth(); - const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( { + const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery({ localSiteId: selectedSite.id, userId: user?.id, - } ); - const [ connectSite ] = useConnectSiteMutation(); - const [ disconnectSite ] = useDisconnectSiteMutation(); + }); + const [connectSite] = useConnectSiteMutation(); + const [disconnectSite] = useDisconnectSiteMutation(); const { pushSite, pullSite } = useSyncSites(); - const connectedSiteIds = connectedSites.map( ( { id } ) => id ); - const { data: syncSites = [] } = useGetWpComSitesQuery( { + const connectedSiteIds = connectedSites.map(({ id }) => id); + const { data: syncSites = [] } = useGetWpComSitesQuery({ connectedSiteIds, userId: user?.id, - } ); + }); - const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null ); + const [selectedRemoteSite, setSelectedRemoteSite] = useState(null); // Auto-select remote site when set via Redux (e.g., from deep link connection) - useEffect( () => { - if ( selectedRemoteSiteId ) { - const siteToSelect = syncSites.find( ( site ) => site.id === selectedRemoteSiteId ); - if ( siteToSelect ) { - setSelectedRemoteSite( siteToSelect ); - dispatch( connectedSitesActions.openModal( 'push' ) ); - dispatch( connectedSitesActions.clearSelectedRemoteSiteId() ); + useEffect(() => { + if (selectedRemoteSiteId) { + const siteToSelect = syncSites.find((site) => site.id === selectedRemoteSiteId); + if (siteToSelect) { + setSelectedRemoteSite(siteToSelect); + dispatch(connectedSitesActions.openModal('push')); + dispatch(connectedSitesActions.clearSelectedRemoteSiteId()); } } - }, [ selectedRemoteSiteId, syncSites, dispatch ] ); + }, [selectedRemoteSiteId, syncSites, dispatch]); - if ( ! isAuthenticated ) { + if (!isAuthenticated) { return ; } - const handleConnect = async ( newConnectedSite: SyncSite ) => { + const handleConnect = async (remoteSiteId: number) => { try { - await connectSite( { site: newConnectedSite, localSiteId: selectedSite.id } ); - } catch ( error ) { - getIpcApi().showErrorMessageBox( { - title: __( 'Failed to connect to site' ), - message: __( 'Please try again.' ), - } ); + await connectSite({ + remoteSiteId, + localSiteId: selectedSite.id, + userId: user?.id, + }); + } catch (error) { + getIpcApi().showErrorMessageBox({ + title: __('Failed to connect to site'), + message: __('Please try again.'), + }); } }; - const handleSiteSelection = async ( siteId: number ) => { - const selectedSiteFromList = syncSites.find( ( site ) => site.id === siteId ); - if ( ! selectedSiteFromList ) { - getIpcApi().showErrorMessageBox( { - title: __( 'Failed to select site' ), - message: __( 'Please try again.' ), - } ); + const handleSiteSelection = async (siteId: number) => { + const selectedSiteFromList = syncSites.find((site) => site.id === siteId); + if (!selectedSiteFromList) { + getIpcApi().showErrorMessageBox({ + title: __('Failed to select site'), + message: __('Please try again.'), + }); return; } - if ( reduxModalMode === 'push' || reduxModalMode === 'pull' ) { - dispatch( connectedSitesActions.openModal( reduxModalMode ) ); - setSelectedRemoteSite( selectedSiteFromList ); + if (reduxModalMode === 'push' || reduxModalMode === 'pull') { + dispatch(connectedSitesActions.openModal(reduxModalMode)); + setSelectedRemoteSite(selectedSiteFromList); } else { - await handleConnect( selectedSiteFromList ); - dispatch( connectedSitesActions.closeModal() ); + await handleConnect(siteId); + dispatch(connectedSitesActions.closeModal()); } }; return (
- { connectedSites.length > 0 ? ( + {connectedSites.length > 0 ? (
- disconnectSite( { siteId: id, localSiteId: selectedSite.id } ) - } + connectedSites={connectedSites} + selectedSite={selectedSite} + disconnectSite={(id) => disconnectSite({ siteId: id, localSiteId: selectedSite.id })} />
dispatch( connectedSitesActions.openModal( 'connect' ) ) } + connectSite={() => dispatch(connectedSitesActions.openModal('connect'))} > - { __( 'Connect another site' ) } + {__('Connect another site')}
@@ -217,48 +217,48 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
dispatch( connectedSitesActions.openModal( 'connect' ) ) } + connectSite={() => dispatch(connectedSitesActions.openModal('connect'))} > - { __( 'Connect site' ) } + {__('Connect site')}
- ) } + )} - { isModalOpen && ! selectedRemoteSite && ( + {isModalOpen && !selectedRemoteSite && ( { - dispatch( connectedSitesActions.closeModal() ); - } } - onConnect={ async ( siteId: number ) => { - await handleSiteSelection( siteId ); - } } - selectedSite={ selectedSite } + mode={reduxModalMode || 'connect'} + onRequestClose={() => { + dispatch(connectedSitesActions.closeModal()); + }} + onConnect={async (siteId: number) => { + await handleSiteSelection(siteId); + }} + selectedSite={selectedSite} /> - ) } + )} - { reduxModalMode && reduxModalMode !== 'connect' && selectedRemoteSite && ( + {reduxModalMode && reduxModalMode !== 'connect' && selectedRemoteSite && ( { - await handleConnect( selectedRemoteSite ); - const pushOptions = convertTreeToPushOptions( tree ); - void pushSite( selectedRemoteSite, selectedSite, pushOptions ); - } } - onPull={ async ( tree ) => { - await handleConnect( selectedRemoteSite ); - const pullOptions = convertTreeToPullOptions( tree ); - void pullSite( selectedRemoteSite, selectedSite, pullOptions ); - } } - onRequestClose={ () => { - setSelectedRemoteSite( null ); - dispatch( connectedSitesActions.closeModal() ); - } } + type={reduxModalMode} + localSite={selectedSite} + remoteSite={selectedRemoteSite} + onPush={async (tree) => { + await handleConnect(selectedRemoteSite.id); + const pushOptions = convertTreeToPushOptions(tree); + void pushSite(selectedRemoteSite, selectedSite, pushOptions); + }} + onPull={async (tree) => { + await handleConnect(selectedRemoteSite.id); + const pullOptions = convertTreeToPullOptions(tree); + void pullSite(selectedRemoteSite, selectedSite, pullOptions); + }} + onRequestClose={() => { + setSelectedRemoteSite(null); + dispatch(connectedSitesActions.closeModal()); + }} /> - ) } + )}
); } diff --git a/apps/studio/src/stores/sync/connected-sites.ts b/apps/studio/src/stores/sync/connected-sites.ts index 6b2796eba1..bf3e517082 100644 --- a/apps/studio/src/stores/sync/connected-sites.ts +++ b/apps/studio/src/stores/sync/connected-sites.ts @@ -2,6 +2,7 @@ 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 { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; import type { SyncSite, SyncModalMode } from 'src/modules/sync/types'; type ConnectedSitesState = { @@ -74,11 +75,35 @@ export const connectedSitesApi = createApi( { ], } ), - connectSite: builder.mutation< SyncSite[], { site: SyncSite; localSiteId: string } >( { - queryFn: async ( { site, localSiteId } ) => { + connectSite: builder.mutation< + SyncSite[], + { remoteSiteId: number; localSiteId: string; userId?: number } + >( { + queryFn: async ( { remoteSiteId, localSiteId, userId }, api ) => { + const connectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId ); + const { data: remoteSites = [] } = await api.dispatch( + wpcomSitesApi.endpoints.getWpComSites.initiate( + { + connectedSiteIds: connectedSites.map( ( site ) => site.id ), + userId, + }, + { forceRefetch: true } + ) + ); + const siteToConnect = remoteSites.find( ( site ) => site.id === remoteSiteId ); + + if ( ! siteToConnect ) { + return { + error: { + status: 'CUSTOM_ERROR', + error: 'Site not found in WordPress.com sites', + }, + }; + } + await getIpcApi().connectWpcomSites( [ { - sites: [ site ], + sites: [ siteToConnect ], localSiteId, }, ] );