From a1a75704a60b75d814fbaf00b3804bdb9d2b12c5 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Tue, 4 Nov 2025 12:13:55 +0100 Subject: [PATCH 01/18] Implement new site launch and import buttons --- .../components/sync-sites-modal-selector.tsx | 52 +++- src/modules/sync/index.tsx | 232 +++++++++++++++--- 2 files changed, 241 insertions(+), 43 deletions(-) diff --git a/src/modules/sync/components/sync-sites-modal-selector.tsx b/src/modules/sync/components/sync-sites-modal-selector.tsx index c06e4b1f8b..85015d6a83 100644 --- a/src/modules/sync/components/sync-sites-modal-selector.tsx +++ b/src/modules/sync/components/sync-sites-modal-selector.tsx @@ -32,6 +32,7 @@ export function SyncSitesModalSelector( { syncSites, onInitialRender, selectedSite, + mode = 'connect', }: { isLoading?: boolean; onRequestClose: () => void; @@ -39,6 +40,7 @@ export function SyncSitesModalSelector( { onConnect: ( siteId: number ) => void; onInitialRender?: () => void; selectedSite: SiteDetails; + mode?: 'push' | 'pull' | 'connect'; } ) { const { __ } = useI18n(); const [ selectedSiteId, setSelectedSiteId ] = useState< number | null >( null ); @@ -53,6 +55,18 @@ export function SyncSitesModalSelector( { } ); const isEmpty = filteredSites.length === 0; + const getModalTitle = () => { + switch ( mode ) { + case 'push': + return __( 'Select a site to launch' ); + case 'pull': + return __( 'Select a site to import' ); + case 'connect': + default: + return __( 'Connect your site' ); + } + }; + useEffect( () => { if ( onInitialRender ) { onInitialRender(); @@ -63,7 +77,7 @@ export function SyncSitesModalSelector( {
@@ -100,11 +114,12 @@ export function SyncSitesModalSelector( { } } disabled={ ! selectedSiteId } selectedSite={ selectedSite } + mode={ mode } /> { isOffline && (
- +
) }
@@ -335,14 +350,28 @@ function Footer( { onConnect, disabled, selectedSite, + mode = 'connect', }: { onRequestClose: () => void; onConnect: () => void; disabled: boolean; selectedSite: SiteDetails; + mode?: 'push' | 'pull' | 'connect'; } ) { const { __ } = useI18n(); + const getButtonText = () => { + switch ( mode ) { + case 'push': + return __( 'Launch' ); + case 'pull': + return __( 'Import' ); + case 'connect': + default: + return __( 'Connect' ); + } + }; + useEffect( () => { if ( ! disabled ) { focusConnectButton(); @@ -361,20 +390,31 @@ function Footer( { { __( 'Cancel' ) } ); } -const SyncSitesOfflineView = () => { - const offlineMessage = __( 'Connecting a site requires an internet connection.' ); +const SyncSitesOfflineView = ( { mode = 'connect' }: { mode?: 'push' | 'pull' | 'connect' } ) => { + const { __ } = useI18n(); + const getOfflineMessage = () => { + switch ( mode ) { + case 'push': + return __( 'Launching your site requires an internet connection.' ); + case 'pull': + return __( 'Importing a remote site requires an internet connection.' ); + case 'connect': + default: + return __( 'Connecting a site requires an internet connection.' ); + } + }; return (
- { offlineMessage } + { getOfflineMessage() }
); }; diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 37fdb384e8..a46378f1b8 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -1,6 +1,6 @@ import { check, Icon } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; -import { PropsWithChildren, useEffect } from 'react'; +import { PropsWithChildren, useEffect, useState } from 'react'; import { ArrowIcon } from 'src/components/arrow-icon'; import Button from 'src/components/button'; import offlineIcon from 'src/components/offline-icon'; @@ -8,10 +8,14 @@ import { Tooltip } from 'src/components/tooltip'; import { useAuth } from 'src/hooks/use-auth'; import { useOffline } from 'src/hooks/use-offline'; import { getIpcApi } from 'src/lib/get-ipc-api'; -import { ConnectButton } from 'src/modules/sync/components/connect-button'; import { SyncConnectedSites } from 'src/modules/sync/components/sync-connected-sites'; +import { SyncDialog } from 'src/modules/sync/components/sync-dialog'; import { SyncSitesModalSelector } from 'src/modules/sync/components/sync-sites-modal-selector'; import { SyncTabImage } from 'src/modules/sync/components/sync-tab-image'; +import { + convertTreeToPullOptions, + convertTreeToPushOptions, +} from 'src/modules/sync/lib/convert-tree-to-sync-options'; import { useAppDispatch, useRootSelector } from 'src/stores'; import { useConnectedSitesData, @@ -19,7 +23,9 @@ import { useConnectedSitesOperations, connectedSitesSelectors, connectedSitesActions, + loadAllConnectedSites, } from 'src/stores/sync'; +import { useSyncSites } from 'src/hooks/sync-sites'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; function SiteSyncDescription( { children }: PropsWithChildren ) { @@ -34,7 +40,7 @@ function SiteSyncDescription( { children }: PropsWithChildren ) {
{ __( - 'Connect your existing WordPress.com or Pressable sites with Jetpack activated, or create a new one. Then share your work with the world.' + 'Launch your site to push changes to a remote site, or import a remote site to pull changes locally.' ) }
@@ -123,6 +129,12 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const { connectedSites } = useConnectedSitesData(); const { syncSites, isFetching, refetchSites } = useSyncSitesData(); const { connectSite, disconnectSite } = useConnectedSitesOperations(); + const { pushSite, pullSite } = useSyncSites(); + const isOffline = useOffline(); + + const [ modalMode, setModalMode ] = useState< 'push' | 'pull' | 'connect' | null >( null ); + const [ syncDialogType, setSyncDialogType ] = useState< 'push' | 'pull' | null >( null ); + const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null ); const { isAuthenticated } = useAuth(); @@ -136,17 +148,102 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return ; } - const handleConnect = async ( newConnectedSite: SyncSite ) => { + const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite > => { try { await connectSite( newConnectedSite ); + // After connecting, reload connected sites to get the full site data + await dispatch( loadAllConnectedSites() ); + // Get the updated connected sites from the store + const updatedConnectedSites = await getIpcApi().getConnectedWpcomSites( selectedSite.id ); + // Find the site we just connected (it will have more metadata like localSiteId) + const connectedSite = updatedConnectedSites.find( ( site ) => site.id === newConnectedSite.id ); + // Return the connected site with full metadata, or fallback to the original site + return connectedSite || newConnectedSite; } catch ( error ) { getIpcApi().showErrorMessageBox( { title: __( 'Failed to connect to site' ), message: __( 'Please try again.' ), } ); + throw error; + } + }; + + const handleLaunchSite = () => { + setModalMode( 'push' ); + dispatch( connectedSitesActions.openModal() ); + }; + + const handleImportSite = () => { + setModalMode( 'pull' ); + dispatch( connectedSitesActions.openModal() ); + }; + + const handleSiteSelected = async ( siteId: number ) => { + const disconnectSiteId = + typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; + + if ( disconnectSiteId ) { + await disconnectSite( disconnectSiteId ); + } + + const selectedSiteFromList = syncSites.find( ( site ) => site.id === siteId ); + if ( ! selectedSiteFromList ) { + getIpcApi().showErrorMessageBox( { + title: __( 'Failed to select site' ), + message: __( 'Please try again.' ), + } ); + return; + } + + // Check if site is already connected + const isAlreadyConnected = connectedSites.some( ( site ) => site.id === siteId ); + + let siteToUse = selectedSiteFromList; + if ( ! isAlreadyConnected ) { + // Connect the site first + try { + siteToUse = await handleConnect( selectedSiteFromList ); + } catch ( error ) { + return; // Error already handled in handleConnect + } + } else { + // Use the already connected site (it has more metadata) + siteToUse = connectedSites.find( ( site ) => site.id === siteId ) || selectedSiteFromList; + } + + // Close the modal + dispatch( connectedSitesActions.closeModal() ); + setModalMode( null ); + + // Open the appropriate sync dialog + if ( modalMode === 'push' ) { + setSelectedRemoteSite( siteToUse ); + setSyncDialogType( 'push' ); + } else if ( modalMode === 'pull' ) { + setSelectedRemoteSite( siteToUse ); + setSyncDialogType( 'pull' ); } }; + const handleConnectLegacy = async ( siteId: number ) => { + const disconnectSiteId = + typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; + + if ( disconnectSiteId ) { + await disconnectSite( disconnectSiteId ); + } + + const newConnectedSite = syncSites.find( ( site ) => site.id === siteId ); + if ( ! newConnectedSite ) { + getIpcApi().showErrorMessageBox( { + title: __( 'Failed to connect to site' ), + message: __( 'Please try again.' ), + } ); + return; + } + await handleConnect( newConnectedSite ); + }; + return (
{ connectedSites.length > 0 ? ( @@ -157,56 +254,117 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } disconnectSite={ disconnectSite } />
- dispatch( connectedSitesActions.openModal() ) } - disableConnectButtonStyle={ true } - > - { __( 'Connect another site' ) } - +
+ + + + + + +
) : ( -
- dispatch( connectedSitesActions.openModal() ) } - disableConnectButtonStyle={ true } +
+ + + + - { __( 'Connect site' ) } - + +
) } { isModalOpen && ( dispatch( connectedSitesActions.closeModal() ) } + onRequestClose={ () => { + dispatch( connectedSitesActions.closeModal() ); + setModalMode( null ); + } } syncSites={ syncSites } onInitialRender={ refetchSites } - onConnect={ async ( siteId ) => { - const disconnectSiteId = - typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; - - if ( disconnectSiteId ) { - await disconnectSite( disconnectSiteId ); - } - - const newConnectedSite = syncSites.find( ( site ) => site.id === siteId ); - if ( ! newConnectedSite ) { - getIpcApi().showErrorMessageBox( { - title: __( 'Failed to connect to site' ), - message: __( 'Please try again.' ), - } ); - return; - } - void handleConnect( newConnectedSite ); - } } + onConnect={ modalMode ? handleSiteSelected : handleConnectLegacy } selectedSite={ selectedSite } /> ) } + + { syncDialogType && selectedRemoteSite && ( + { + const pushOptions = convertTreeToPushOptions( tree ); + void pushSite( selectedRemoteSite, selectedSite, pushOptions ); + setSyncDialogType( null ); + setSelectedRemoteSite( null ); + } } + onPull={ ( tree ) => { + const pullOptions = convertTreeToPullOptions( tree ); + pullSite( selectedRemoteSite, selectedSite, pullOptions ); + setSyncDialogType( null ); + setSelectedRemoteSite( null ); + } } + onRequestClose={ () => { + setSyncDialogType( null ); + setSelectedRemoteSite( null ); + } } + /> + ) }
); } From 83e3af97a3c0607e8bb75fb6861aca9593a6fc60 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Tue, 4 Nov 2025 16:39:13 +0100 Subject: [PATCH 02/18] Fix linting errors --- src/hooks/sync-sites/sync-sites-context.tsx | 2 +- src/modules/sync/index.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hooks/sync-sites/sync-sites-context.tsx b/src/hooks/sync-sites/sync-sites-context.tsx index 500ef47bc5..9a88072662 100644 --- a/src/hooks/sync-sites/sync-sites-context.tsx +++ b/src/hooks/sync-sites/sync-sites-context.tsx @@ -1,5 +1,5 @@ import { __, sprintf } from '@wordpress/i18n'; -import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { useListenDeepLinkConnection } from 'src/hooks/sync-sites/use-listen-deep-link-connection'; import { generateStateId } from 'src/hooks/sync-sites/use-pull-push-states'; import { PullStates, UseSyncPull, useSyncPull } from 'src/hooks/sync-sites/use-sync-pull'; diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index a46378f1b8..1ca328d25f 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -5,6 +5,7 @@ import { ArrowIcon } from 'src/components/arrow-icon'; import Button from 'src/components/button'; import offlineIcon from 'src/components/offline-icon'; import { Tooltip } from 'src/components/tooltip'; +import { useSyncSites } from 'src/hooks/sync-sites'; import { useAuth } from 'src/hooks/use-auth'; import { useOffline } from 'src/hooks/use-offline'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -25,7 +26,6 @@ import { connectedSitesActions, loadAllConnectedSites, } from 'src/stores/sync'; -import { useSyncSites } from 'src/hooks/sync-sites'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; function SiteSyncDescription( { children }: PropsWithChildren ) { @@ -156,7 +156,9 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } // Get the updated connected sites from the store const updatedConnectedSites = await getIpcApi().getConnectedWpcomSites( selectedSite.id ); // Find the site we just connected (it will have more metadata like localSiteId) - const connectedSite = updatedConnectedSites.find( ( site ) => site.id === newConnectedSite.id ); + const connectedSite = updatedConnectedSites.find( + ( site ) => site.id === newConnectedSite.id + ); // Return the connected site with full metadata, or fallback to the original site return connectedSite || newConnectedSite; } catch ( error ) { From d7475720456a3420c558e6392f65112e68ba133e Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Tue, 4 Nov 2025 19:55:40 +0100 Subject: [PATCH 03/18] Use connect button when there is a site connected --- .../sync/components/connect-button.tsx | 5 +- src/modules/sync/index.tsx | 138 +++++++++--------- src/modules/sync/tests/index.test.tsx | 29 ++-- 3 files changed, 88 insertions(+), 84 deletions(-) diff --git a/src/modules/sync/components/connect-button.tsx b/src/modules/sync/components/connect-button.tsx index 136c18a334..c972430e04 100644 --- a/src/modules/sync/components/connect-button.tsx +++ b/src/modules/sync/components/connect-button.tsx @@ -11,6 +11,7 @@ interface ConnectButtonProps { disableConnectButtonStyle?: boolean; className?: string; children?: React.ReactNode; + tooltipText?: string; } export const ConnectButton = ( { @@ -19,12 +20,14 @@ export const ConnectButton = ( { disableConnectButtonStyle, className, children, + tooltipText, }: ConnectButtonProps ) => { const isOffline = useOffline(); + const tooltipContent = tooltipText ?? __( 'Connecting a site requires an internet connection.' ); return ( diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 1ca328d25f..b42a330932 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -9,6 +9,7 @@ import { useSyncSites } from 'src/hooks/sync-sites'; import { useAuth } from 'src/hooks/use-auth'; import { useOffline } from 'src/hooks/use-offline'; import { getIpcApi } from 'src/lib/get-ipc-api'; +import { ConnectButton } from 'src/modules/sync/components/connect-button'; import { SyncConnectedSites } from 'src/modules/sync/components/sync-connected-sites'; import { SyncDialog } from 'src/modules/sync/components/sync-dialog'; import { SyncSitesModalSelector } from 'src/modules/sync/components/sync-sites-modal-selector'; @@ -28,33 +29,48 @@ import { } from 'src/stores/sync'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; -function SiteSyncDescription( { children }: PropsWithChildren ) { +interface SiteSyncDescriptionProps extends PropsWithChildren { + title?: string; + description?: string; + bulletPoints?: string[]; +} + +function SiteSyncDescription( { + children, + title, + description, + bulletPoints, +}: SiteSyncDescriptionProps ) { const { __ } = useI18n(); + const defaultTitle = __( 'Launch or import your site' ); + const defaultDescription = __( + 'Create a new WordPress.com site or import an existing one to sync with Studio.' + ); + const defaultBulletPoints = [ + __( 'Launch a new WordPress.com site in minutes.' ), + __( 'Import a site you already run elsewhere.' ), + __( "Push and pull your content whenever you're ready." ), + ]; + const items = bulletPoints ?? defaultBulletPoints; return (
-
- { __( 'Sync with WordPress.com or Pressable' ) } -
+
{ title ?? defaultTitle }
- { __( - 'Launch your site to push changes to a remote site, or import a remote site to pull changes locally.' - ) } -
-
- { [ - __( 'Push and pull changes from your live site.' ), - __( 'Connect multiple environments.' ), - __( 'Sync database and file changes.' ), - ].map( ( text ) => ( -
- - { text } -
- ) ) } + { description ?? defaultDescription }
+ { items.length > 0 && ( +
+ { items.map( ( text ) => ( +
+ + { text } +
+ ) ) } +
+ ) } { children }
@@ -71,7 +87,17 @@ function NoAuthSyncTab() { const offlineMessage = __( "You're currently offline." ); return ( - +
- - - - -
+ dispatch( connectedSitesActions.openModal() ) } + disableConnectButtonStyle={ true } + > + { __( 'Connect another site' ) } +
) : ( -
+
{ __( 'Launch your site' ) } + - - - + { __( 'Import your remote site' ) } +
) } diff --git a/src/modules/sync/tests/index.test.tsx b/src/modules/sync/tests/index.test.tsx index f37f38c4a2..fb0d037288 100644 --- a/src/modules/sync/tests/index.test.tsx +++ b/src/modules/sync/tests/index.test.tsx @@ -237,19 +237,21 @@ describe( 'ContentTabSync', () => { expect( getIpcApi().authenticate ).toHaveBeenCalled(); } ); - it( 'displays connect site button to authenticated user', () => { + it( 'displays launch and import actions to authenticated user', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } ); + const launchButton = screen.getByRole( 'button', { name: /Launch your site/i } ); + const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); - expect( connectSiteButton ).toBeInTheDocument(); + expect( launchButton ).toBeInTheDocument(); + expect( importButton ).toBeInTheDocument(); } ); - it( 'opens the site selector modal to connect a site authenticated user', () => { + it( 'opens the site selector modal when clicking import button', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } ); - fireEvent.click( connectSiteButton ); + const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); @@ -330,9 +332,9 @@ describe( 'ContentTabSync', () => { it( 'opens the modal and displays the create new site button', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } ); - expect( connectSiteButton ).toBeInTheDocument(); - fireEvent.click( connectSiteButton ); + const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + expect( importButton ).toBeInTheDocument(); + fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); renderWithProvider( ); const createNewSiteButton = screen.getByRole( 'button', { @@ -341,12 +343,15 @@ describe( 'ContentTabSync', () => { expect( createNewSiteButton ).toBeInTheDocument(); } ); - it( 'displays ConnectButton when there are no connected sites', () => { + it( 'displays launch and import buttons when there are no connected sites', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const connectButton = screen.getByRole( 'button', { name: /Connect site/i } ); - expect( connectButton ).toBeInTheDocument(); + const launchButton = screen.getByRole( 'button', { name: /Launch your site/i } ); + const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + + expect( launchButton ).toBeInTheDocument(); + expect( importButton ).toBeInTheDocument(); } ); it( 'displays environment badges for Pressable sites with production, staging and development environments', () => { From 847efa4421cf2f0ab02d2ad6ece0bb34402467cf Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 14:23:58 +0100 Subject: [PATCH 04/18] Simplify state, update button text and fix tests --- src/modules/sync/index.tsx | 136 ++++++++++++++------------ src/modules/sync/tests/index.test.tsx | 12 +-- 2 files changed, 82 insertions(+), 66 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index b42a330932..860d9ff2dd 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -158,9 +158,18 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const { pushSite, pullSite } = useSyncSites(); const isOffline = useOffline(); - const [ modalMode, setModalMode ] = useState< 'push' | 'pull' | 'connect' | null >( null ); - const [ syncDialogType, setSyncDialogType ] = useState< 'push' | 'pull' | null >( null ); - const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null ); + // Simplified state management - combine related modal state into single object + type ModalState = { + mode: 'push' | 'pull' | 'connect' | null; + selectedRemoteSite: SyncSite | null; + syncDialogType: 'push' | 'pull' | null; + }; + + const [ modalState, setModalState ] = useState< ModalState >( { + mode: null, + selectedRemoteSite: null, + syncDialogType: null, + } ); const { isAuthenticated } = useAuth(); @@ -177,14 +186,10 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite > => { try { await connectSite( newConnectedSite ); - // After connecting, reload connected sites to get the full site data + // After connecting, reload connected sites to get the full site data from Redux store await dispatch( loadAllConnectedSites() ); - // Get the updated connected sites from the store - const updatedConnectedSites = await getIpcApi().getConnectedWpcomSites( selectedSite.id ); - // Find the site we just connected (it will have more metadata like localSiteId) - const connectedSite = updatedConnectedSites.find( - ( site ) => site.id === newConnectedSite.id - ); + // Use Redux store as source of truth - find the site we just connected + const connectedSite = connectedSites.find( ( site ) => site.id === newConnectedSite.id ); // Return the connected site with full metadata, or fallback to the original site return connectedSite || newConnectedSite; } catch ( error ) { @@ -197,16 +202,20 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } }; const handleLaunchSite = () => { - setModalMode( 'push' ); + setModalState( ( prev ) => ( { ...prev, mode: 'push' } ) ); dispatch( connectedSitesActions.openModal() ); }; const handleImportSite = () => { - setModalMode( 'pull' ); + setModalState( ( prev ) => ( { ...prev, mode: 'pull' } ) ); dispatch( connectedSitesActions.openModal() ); }; - const handleSiteSelected = async ( siteId: number ) => { + // Unified handler for site selection with optional post-connection callback + const handleSiteSelection = async ( + siteId: number, + onAfterConnect?: ( site: SyncSite ) => void + ) => { const disconnectSiteId = typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; @@ -223,9 +232,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } - // Capture the modal mode before closing the modal - const currentModalMode = modalMode; - // Check if site is already connected const isAlreadyConnected = connectedSites.some( ( site ) => site.id === siteId ); @@ -244,35 +250,12 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } // Close the modal dispatch( connectedSitesActions.closeModal() ); - setModalMode( null ); - - // Open the appropriate sync dialog - if ( currentModalMode === 'push' ) { - setSelectedRemoteSite( siteToUse ); - setSyncDialogType( 'push' ); - } else if ( currentModalMode === 'pull' ) { - setSelectedRemoteSite( siteToUse ); - setSyncDialogType( 'pull' ); - } - }; - - const handleConnectLegacy = async ( siteId: number ) => { - const disconnectSiteId = - typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; - - if ( disconnectSiteId ) { - await disconnectSite( disconnectSiteId ); - } + setModalState( ( prev ) => ( { ...prev, mode: null } ) ); - const newConnectedSite = syncSites.find( ( site ) => site.id === siteId ); - if ( ! newConnectedSite ) { - getIpcApi().showErrorMessageBox( { - title: __( 'Failed to connect to site' ), - message: __( 'Please try again.' ), - } ); - return; + // Execute post-connection callback if provided + if ( onAfterConnect ) { + onAfterConnect( siteToUse ); } - await handleConnect( newConnectedSite ); }; return ( @@ -309,8 +292,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } disabled={ isOffline } aria-disabled={ isOffline } > - { __( 'Launch your site' ) } - + { __( 'Publish site' ) } - { __( 'Import your remote site' ) } + { __( 'Pull site' ) }
@@ -327,39 +309,73 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } { isModalOpen && ( { dispatch( connectedSitesActions.closeModal() ); - setModalMode( null ); + setModalState( ( prev ) => ( { ...prev, mode: null } ) ); } } syncSites={ syncSites } onInitialRender={ refetchSites } - onConnect={ modalMode ? handleSiteSelected : handleConnectLegacy } + onConnect={ ( siteId: number ) => { + // Capture the current modal mode before it gets cleared + const currentMode = modalState.mode; + + // Use unified handler with appropriate callback based on mode + if ( currentMode === 'push' ) { + void handleSiteSelection( siteId, ( site ) => { + setModalState( ( prev ) => ( { + ...prev, + selectedRemoteSite: site, + syncDialogType: 'push', + } ) ); + } ); + } else if ( currentMode === 'pull' ) { + void handleSiteSelection( siteId, ( site ) => { + setModalState( ( prev ) => ( { + ...prev, + selectedRemoteSite: site, + syncDialogType: 'pull', + } ) ); + } ); + } else { + // Legacy connect mode - no post-connection action needed + void handleSiteSelection( siteId ); + } + } } selectedSite={ selectedSite } /> ) } - { syncDialogType && selectedRemoteSite && ( + { modalState.syncDialogType && modalState.selectedRemoteSite && ( { const pushOptions = convertTreeToPushOptions( tree ); - void pushSite( selectedRemoteSite, selectedSite, pushOptions ); - setSyncDialogType( null ); - setSelectedRemoteSite( null ); + void pushSite( modalState.selectedRemoteSite!, selectedSite, pushOptions ); + setModalState( ( prev ) => ( { + ...prev, + syncDialogType: null, + selectedRemoteSite: null, + } ) ); } } onPull={ ( tree ) => { const pullOptions = convertTreeToPullOptions( tree ); - pullSite( selectedRemoteSite, selectedSite, pullOptions ); - setSyncDialogType( null ); - setSelectedRemoteSite( null ); + pullSite( modalState.selectedRemoteSite!, selectedSite, pullOptions ); + setModalState( ( prev ) => ( { + ...prev, + syncDialogType: null, + selectedRemoteSite: null, + } ) ); } } onRequestClose={ () => { - setSyncDialogType( null ); - setSelectedRemoteSite( null ); + setModalState( ( prev ) => ( { + ...prev, + syncDialogType: null, + selectedRemoteSite: null, + } ) ); } } /> ) } diff --git a/src/modules/sync/tests/index.test.tsx b/src/modules/sync/tests/index.test.tsx index 2a7fe4efe3..5a0386cb11 100644 --- a/src/modules/sync/tests/index.test.tsx +++ b/src/modules/sync/tests/index.test.tsx @@ -266,8 +266,8 @@ describe( 'ContentTabSync', () => { it( 'displays launch and import actions to authenticated user', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const launchButton = screen.getByRole( 'button', { name: /Launch your site/i } ); - const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + const launchButton = screen.getByRole( 'button', { name: /Publish site/i } ); + const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); expect( launchButton ).toBeInTheDocument(); expect( importButton ).toBeInTheDocument(); @@ -276,7 +276,7 @@ describe( 'ContentTabSync', () => { it( 'opens the site selector modal when clicking import button', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); @@ -358,7 +358,7 @@ describe( 'ContentTabSync', () => { it( 'opens the modal and displays the create new site button', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); expect( importButton ).toBeInTheDocument(); fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); @@ -373,8 +373,8 @@ describe( 'ContentTabSync', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const launchButton = screen.getByRole( 'button', { name: /Launch your site/i } ); - const importButton = screen.getByRole( 'button', { name: /Import your remote site/i } ); + const launchButton = screen.getByRole( 'button', { name: /Publish site/i } ); + const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); expect( launchButton ).toBeInTheDocument(); expect( importButton ).toBeInTheDocument(); From c64336174300e380192aadb5bbab40ca76c03d86 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 16:15:00 +0100 Subject: [PATCH 05/18] Update wording, reuse ConnectButton component --- .../components/sync-sites-modal-selector.tsx | 4 +- src/modules/sync/index.tsx | 87 ++++++------------- 2 files changed, 29 insertions(+), 62 deletions(-) diff --git a/src/modules/sync/components/sync-sites-modal-selector.tsx b/src/modules/sync/components/sync-sites-modal-selector.tsx index 85015d6a83..f96bdece3a 100644 --- a/src/modules/sync/components/sync-sites-modal-selector.tsx +++ b/src/modules/sync/components/sync-sites-modal-selector.tsx @@ -58,7 +58,7 @@ export function SyncSitesModalSelector( { const getModalTitle = () => { switch ( mode ) { case 'push': - return __( 'Select a site to launch' ); + return __( 'Publish your site' ); case 'pull': return __( 'Select a site to import' ); case 'connect': @@ -363,7 +363,7 @@ function Footer( { const getButtonText = () => { switch ( mode ) { case 'push': - return __( 'Launch' ); + return __( 'Publish' ); case 'pull': return __( 'Import' ); case 'connect': diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 860d9ff2dd..6e283cd529 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -29,48 +29,33 @@ import { } from 'src/stores/sync'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; -interface SiteSyncDescriptionProps extends PropsWithChildren { - title?: string; - description?: string; - bulletPoints?: string[]; -} - -function SiteSyncDescription( { - children, - title, - description, - bulletPoints, -}: SiteSyncDescriptionProps ) { +function SiteSyncDescription( { children }: PropsWithChildren ) { const { __ } = useI18n(); - const defaultTitle = __( 'Launch or import your site' ); - const defaultDescription = __( - 'Create a new WordPress.com site or import an existing one to sync with Studio.' - ); - const defaultBulletPoints = [ - __( 'Launch a new WordPress.com site in minutes.' ), - __( 'Import a site you already run elsewhere.' ), - __( "Push and pull your content whenever you're ready." ), - ]; - const items = bulletPoints ?? defaultBulletPoints; return (
-
{ title ?? defaultTitle }
+
+ { __( 'Sync with WordPress.com or Pressable' ) } +
- { description ?? defaultDescription } + { __( + 'Launch your existing WordPress.com or Jetpack-activated Pressable sites, or import an exisiting 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 } +
+ ) ) }
- { items.length > 0 && ( -
- { items.map( ( text ) => ( -
- - { text } -
- ) ) } -
- ) } { children }
@@ -87,17 +72,7 @@ function NoAuthSyncTab() { const offlineMessage = __( "You're currently offline." ); return ( - +
- + { __( 'Publish site' ) } + Date: Wed, 5 Nov 2025 16:48:51 +0100 Subject: [PATCH 06/18] Add a new type and simplify code --- .../components/sync-sites-modal-selector.tsx | 9 +++-- src/modules/sync/index.tsx | 38 +++++++------------ src/modules/sync/types.ts | 2 + 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/modules/sync/components/sync-sites-modal-selector.tsx b/src/modules/sync/components/sync-sites-modal-selector.tsx index f96bdece3a..e9eede5e2c 100644 --- a/src/modules/sync/components/sync-sites-modal-selector.tsx +++ b/src/modules/sync/components/sync-sites-modal-selector.tsx @@ -17,6 +17,7 @@ import { EnvironmentBadge } from 'src/modules/sync/components/environment-badge' import { getSiteEnvironment } from 'src/modules/sync/lib/environment-utils'; import { useI18nLocale } from 'src/stores'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; +import type { SyncModalMode } from 'src/modules/sync/types'; const SearchControl = process.env.NODE_ENV === 'test' ? () => null : SearchControlWp; @@ -40,7 +41,7 @@ export function SyncSitesModalSelector( { onConnect: ( siteId: number ) => void; onInitialRender?: () => void; selectedSite: SiteDetails; - mode?: 'push' | 'pull' | 'connect'; + mode?: SyncModalMode; } ) { const { __ } = useI18n(); const [ selectedSiteId, setSelectedSiteId ] = useState< number | null >( null ); @@ -356,7 +357,7 @@ function Footer( { onConnect: () => void; disabled: boolean; selectedSite: SiteDetails; - mode?: 'push' | 'pull' | 'connect'; + mode?: SyncModalMode; } ) { const { __ } = useI18n(); @@ -397,12 +398,12 @@ function Footer( { ); } -const SyncSitesOfflineView = ( { mode = 'connect' }: { mode?: 'push' | 'pull' | 'connect' } ) => { +const SyncSitesOfflineView = ( { mode = 'connect' }: { mode?: SyncModalMode } ) => { const { __ } = useI18n(); const getOfflineMessage = () => { switch ( mode ) { case 'push': - return __( 'Launching your site requires an internet connection.' ); + return __( 'Publishing your site requires an internet connection.' ); case 'pull': return __( 'Importing a remote site requires an internet connection.' ); case 'connect': diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 6e283cd529..e53d182499 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -28,6 +28,7 @@ import { loadAllConnectedSites, } from 'src/stores/sync'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; +import type { SyncModalMode } from 'src/modules/sync/types'; function SiteSyncDescription( { children }: PropsWithChildren ) { const { __ } = useI18n(); @@ -134,9 +135,9 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } // Simplified state management - combine related modal state into single object type ModalState = { - mode: 'push' | 'pull' | 'connect' | null; + mode: SyncModalMode | null; selectedRemoteSite: SyncSite | null; - syncDialogType: 'push' | 'pull' | null; + syncDialogType: Extract< SyncModalMode, 'push' | 'pull' > | null; }; const [ modalState, setModalState ] = useState< ModalState >( { @@ -157,10 +158,10 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return ; } - const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite > => { + const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite | undefined > => { try { await connectSite( newConnectedSite ); - // After connecting, reload connected sites to get the full site data from Redux store + await dispatch( loadAllConnectedSites() ); // Use Redux store as source of truth - find the site we just connected const connectedSite = connectedSites.find( ( site ) => site.id === newConnectedSite.id ); @@ -171,7 +172,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } title: __( 'Failed to connect to site' ), message: __( 'Please try again.' ), } ); - throw error; } }; @@ -211,11 +211,9 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } let siteToUse = selectedSiteFromList; if ( ! isAlreadyConnected ) { - // Connect the site first - try { - siteToUse = await handleConnect( selectedSiteFromList ); - } catch ( error ) { - return; // Error already handled in handleConnect + siteToUse = await handleConnect( selectedSiteFromList ); + if ( ! siteToUse ) { + return; } } else { // Use the already connected site (it has more metadata) @@ -284,30 +282,20 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } } } syncSites={ syncSites } onInitialRender={ refetchSites } - onConnect={ ( siteId: number ) => { - // Capture the current modal mode before it gets cleared + onConnect={ async ( siteId: number ) => { const currentMode = modalState.mode; // Use unified handler with appropriate callback based on mode - if ( currentMode === 'push' ) { - void handleSiteSelection( siteId, ( site ) => { - setModalState( ( prev ) => ( { - ...prev, - selectedRemoteSite: site, - syncDialogType: 'push', - } ) ); - } ); - } else if ( currentMode === 'pull' ) { - void handleSiteSelection( siteId, ( site ) => { + if ( currentMode === 'push' || currentMode === 'pull' ) { + await handleSiteSelection( siteId, ( site ) => { setModalState( ( prev ) => ( { ...prev, selectedRemoteSite: site, - syncDialogType: 'pull', + syncDialogType: currentMode, } ) ); } ); } else { - // Legacy connect mode - no post-connection action needed - void handleSiteSelection( siteId ); + await handleSiteSelection( siteId ); } } } selectedSite={ selectedSite } diff --git a/src/modules/sync/types.ts b/src/modules/sync/types.ts index 54a9f1fa0b..e921c51b4f 100644 --- a/src/modules/sync/types.ts +++ b/src/modules/sync/types.ts @@ -4,3 +4,5 @@ export type RawDirectoryEntry = { path: string; children?: RawDirectoryEntry[]; }; + +export type SyncModalMode = 'push' | 'pull' | 'connect'; From 42d50c4e8550b8967c3d0e43d062a52131c67bea Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 16:58:55 +0100 Subject: [PATCH 07/18] Fix types --- src/modules/sync/index.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index e53d182499..7bb8d68f46 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -206,20 +206,18 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } - // Check if site is already connected + // Check if site is already connected, otherwise connect it const isAlreadyConnected = connectedSites.some( ( site ) => site.id === siteId ); - - let siteToUse = selectedSiteFromList; if ( ! isAlreadyConnected ) { - siteToUse = await handleConnect( selectedSiteFromList ); - if ( ! siteToUse ) { + const connectedSite = await handleConnect( selectedSiteFromList ); + if ( ! connectedSite ) { return; } - } else { - // Use the already connected site (it has more metadata) - siteToUse = connectedSites.find( ( site ) => site.id === siteId ) || selectedSiteFromList; } + // Use the connected site from store (has full metadata) or fallback to selected site + const siteToUse = connectedSites.find( ( site ) => site.id === siteId ) || selectedSiteFromList; + // Close the modal dispatch( connectedSitesActions.closeModal() ); setModalState( ( prev ) => ( { ...prev, mode: null } ) ); From 2e3fea40e43803d8383f2e711b6c3821bf40a810 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 17:10:58 +0100 Subject: [PATCH 08/18] Simplify logic --- src/modules/sync/index.tsx | 56 ++++++++++++-------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 7bb8d68f46..2d83fcd1b0 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -137,13 +137,11 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } type ModalState = { mode: SyncModalMode | null; selectedRemoteSite: SyncSite | null; - syncDialogType: Extract< SyncModalMode, 'push' | 'pull' > | null; }; const [ modalState, setModalState ] = useState< ModalState >( { mode: null, selectedRemoteSite: null, - syncDialogType: null, } ); const { isAuthenticated } = useAuth(); @@ -186,10 +184,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } }; // Unified handler for site selection with optional post-connection callback - const handleSiteSelection = async ( - siteId: number, - onAfterConnect?: ( site: SyncSite ) => void - ) => { + const handleSiteSelection = async ( siteId: number, mode: SyncModalMode | null ) => { const disconnectSiteId = typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; @@ -220,11 +215,12 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } // Close the modal dispatch( connectedSitesActions.closeModal() ); - setModalState( ( prev ) => ( { ...prev, mode: null } ) ); // Execute post-connection callback if provided - if ( onAfterConnect ) { - onAfterConnect( siteToUse ); + if ( mode == 'push' || mode == 'pull' ) { + setModalState( ( prev ) => ( { ...prev, mode: mode, selectedRemoteSite: siteToUse } ) ); + } else { + setModalState( ( prev ) => ( { ...prev, mode: null } ) ); } }; @@ -281,54 +277,38 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } syncSites={ syncSites } onInitialRender={ refetchSites } onConnect={ async ( siteId: number ) => { - const currentMode = modalState.mode; - - // Use unified handler with appropriate callback based on mode - if ( currentMode === 'push' || currentMode === 'pull' ) { - await handleSiteSelection( siteId, ( site ) => { - setModalState( ( prev ) => ( { - ...prev, - selectedRemoteSite: site, - syncDialogType: currentMode, - } ) ); - } ); - } else { - await handleSiteSelection( siteId ); - } + await handleSiteSelection( siteId, modalState.mode ); } } selectedSite={ selectedSite } /> ) } - { modalState.syncDialogType && modalState.selectedRemoteSite && ( + { modalState.mode && modalState.mode !== 'connect' && modalState.selectedRemoteSite && ( { const pushOptions = convertTreeToPushOptions( tree ); void pushSite( modalState.selectedRemoteSite!, selectedSite, pushOptions ); - setModalState( ( prev ) => ( { - ...prev, - syncDialogType: null, + setModalState( { + mode: null, selectedRemoteSite: null, - } ) ); + } ); } } onPull={ ( tree ) => { const pullOptions = convertTreeToPullOptions( tree ); - pullSite( modalState.selectedRemoteSite!, selectedSite, pullOptions ); - setModalState( ( prev ) => ( { - ...prev, - syncDialogType: null, + void pullSite( modalState.selectedRemoteSite!, selectedSite, pullOptions ); + setModalState( { + mode: null, selectedRemoteSite: null, - } ) ); + } ); } } onRequestClose={ () => { - setModalState( ( prev ) => ( { - ...prev, - syncDialogType: null, + setModalState( { + mode: null, selectedRemoteSite: null, - } ) ); + } ); } } /> ) } From 304b9a7ec6c7959618628eb1f360a1aa1fdc49a6 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 17:51:38 +0100 Subject: [PATCH 09/18] Remove superfluous comment and simplify --- src/modules/sync/index.tsx | 16 +++------------- src/modules/sync/tests/index.test.tsx | 12 ++++++------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 2d83fcd1b0..b7d4c87ea3 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -133,13 +133,10 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const { connectSite, disconnectSite } = useConnectedSitesOperations(); const { pushSite, pullSite } = useSyncSites(); - // Simplified state management - combine related modal state into single object - type ModalState = { + const [ modalState, setModalState ] = useState< { mode: SyncModalMode | null; selectedRemoteSite: SyncSite | null; - }; - - const [ modalState, setModalState ] = useState< ModalState >( { + } >( { mode: null, selectedRemoteSite: null, } ); @@ -161,9 +158,8 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } await connectSite( newConnectedSite ); await dispatch( loadAllConnectedSites() ); - // Use Redux store as source of truth - find the site we just connected - const connectedSite = connectedSites.find( ( site ) => site.id === newConnectedSite.id ); // Return the connected site with full metadata, or fallback to the original site + const connectedSite = connectedSites.find( ( site ) => site.id === newConnectedSite.id ); return connectedSite || newConnectedSite; } catch ( error ) { getIpcApi().showErrorMessageBox( { @@ -183,7 +179,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } dispatch( connectedSitesActions.openModal() ); }; - // Unified handler for site selection with optional post-connection callback const handleSiteSelection = async ( siteId: number, mode: SyncModalMode | null ) => { const disconnectSiteId = typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined; @@ -201,7 +196,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } - // Check if site is already connected, otherwise connect it const isAlreadyConnected = connectedSites.some( ( site ) => site.id === siteId ); if ( ! isAlreadyConnected ) { const connectedSite = await handleConnect( selectedSiteFromList ); @@ -209,14 +203,10 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } } - - // Use the connected site from store (has full metadata) or fallback to selected site const siteToUse = connectedSites.find( ( site ) => site.id === siteId ) || selectedSiteFromList; - // Close the modal dispatch( connectedSitesActions.closeModal() ); - // Execute post-connection callback if provided if ( mode == 'push' || mode == 'pull' ) { setModalState( ( prev ) => ( { ...prev, mode: mode, selectedRemoteSite: siteToUse } ) ); } else { diff --git a/src/modules/sync/tests/index.test.tsx b/src/modules/sync/tests/index.test.tsx index 5a0386cb11..d6a4e86a34 100644 --- a/src/modules/sync/tests/index.test.tsx +++ b/src/modules/sync/tests/index.test.tsx @@ -263,13 +263,13 @@ describe( 'ContentTabSync', () => { expect( getIpcApi().authenticate ).toHaveBeenCalled(); } ); - it( 'displays launch and import actions to authenticated user', () => { + it( 'displays publish and import actions to authenticated user', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const launchButton = screen.getByRole( 'button', { name: /Publish site/i } ); + const publishButton = screen.getByRole( 'button', { name: /Publish site/i } ); const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); - expect( launchButton ).toBeInTheDocument(); + expect( publishButton ).toBeInTheDocument(); expect( importButton ).toBeInTheDocument(); } ); @@ -369,14 +369,14 @@ describe( 'ContentTabSync', () => { expect( createNewSiteButton ).toBeInTheDocument(); } ); - it( 'displays launch and import buttons when there are no connected sites', () => { + it( 'displays publish and import buttons when there are no connected sites', () => { ( useAuth as jest.Mock ).mockReturnValue( createAuthMock( true ) ); renderWithProvider( ); - const launchButton = screen.getByRole( 'button', { name: /Publish site/i } ); + const publishButton = screen.getByRole( 'button', { name: /Publish site/i } ); const importButton = screen.getByRole( 'button', { name: /Pull site/i } ); - expect( launchButton ).toBeInTheDocument(); + expect( publishButton ).toBeInTheDocument(); expect( importButton ).toBeInTheDocument(); } ); From 53b7a23e1a785e5c0181da5c2af9427de8f68281 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 18:10:11 +0100 Subject: [PATCH 10/18] Use strict equality operator --- src/modules/sync/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index b7d4c87ea3..f78892c0aa 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -207,7 +207,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } dispatch( connectedSitesActions.closeModal() ); - if ( mode == 'push' || mode == 'pull' ) { + if ( mode === 'push' || mode === 'pull' ) { setModalState( ( prev ) => ( { ...prev, mode: mode, selectedRemoteSite: siteToUse } ) ); } else { setModalState( ( prev ) => ( { ...prev, mode: null } ) ); From e27649198fb85065e0792f39cb8db0d352839185 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Wed, 5 Nov 2025 18:20:57 +0100 Subject: [PATCH 11/18] Use local variable to avoid non-null assertion --- src/modules/sync/index.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index f78892c0aa..a6d2332972 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -214,6 +214,9 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } } }; + // Using a local variable to avoid non-null assertion operator in pushSite and pullSite + const selectedRemoteSite = modalState.selectedRemoteSite; + return (
{ connectedSites.length > 0 ? ( @@ -273,14 +276,14 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } /> ) } - { modalState.mode && modalState.mode !== 'connect' && modalState.selectedRemoteSite && ( + { modalState.mode && modalState.mode !== 'connect' && selectedRemoteSite && ( { const pushOptions = convertTreeToPushOptions( tree ); - void pushSite( modalState.selectedRemoteSite!, selectedSite, pushOptions ); + void pushSite( selectedRemoteSite, selectedSite, pushOptions ); setModalState( { mode: null, selectedRemoteSite: null, @@ -288,7 +291,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } } } onPull={ ( tree ) => { const pullOptions = convertTreeToPullOptions( tree ); - void pullSite( modalState.selectedRemoteSite!, selectedSite, pullOptions ); + void pullSite( selectedRemoteSite, selectedSite, pullOptions ); setModalState( { mode: null, selectedRemoteSite: null, From bb6675808d6b5569851152d037574f93002b4e80 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Thu, 6 Nov 2025 13:41:56 +0100 Subject: [PATCH 12/18] Simplify logic for selecting sites, fix typo --- src/modules/sync/index.tsx | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index a6d2332972..74a8ae12c4 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -25,7 +25,6 @@ import { useConnectedSitesOperations, connectedSitesSelectors, connectedSitesActions, - loadAllConnectedSites, } from 'src/stores/sync'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; import type { SyncModalMode } from 'src/modules/sync/types'; @@ -42,7 +41,7 @@ function SiteSyncDescription( { children }: PropsWithChildren ) {
{ __( - 'Launch your existing WordPress.com or Jetpack-activated Pressable sites, or import an exisiting one. Then, share your work with the world.' + 'Launch your existing WordPress.com or Jetpack-activated Pressable sites, or import an existing one. Then, share your work with the world.' ) }
@@ -153,14 +152,9 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return ; } - const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite | undefined > => { + const handleConnect = async ( newConnectedSite: SyncSite ) => { try { await connectSite( newConnectedSite ); - - await dispatch( loadAllConnectedSites() ); - // Return the connected site with full metadata, or fallback to the original site - const connectedSite = connectedSites.find( ( site ) => site.id === newConnectedSite.id ); - return connectedSite || newConnectedSite; } catch ( error ) { getIpcApi().showErrorMessageBox( { title: __( 'Failed to connect to site' ), @@ -196,19 +190,14 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } - const isAlreadyConnected = connectedSites.some( ( site ) => site.id === siteId ); - if ( ! isAlreadyConnected ) { - const connectedSite = await handleConnect( selectedSiteFromList ); - if ( ! connectedSite ) { - return; - } - } - const siteToUse = connectedSites.find( ( site ) => site.id === siteId ) || selectedSiteFromList; - - dispatch( connectedSitesActions.closeModal() ); + await handleConnect( selectedSiteFromList ); if ( mode === 'push' || mode === 'pull' ) { - setModalState( ( prev ) => ( { ...prev, mode: mode, selectedRemoteSite: siteToUse } ) ); + setModalState( ( prev ) => ( { + ...prev, + mode: mode, + selectedRemoteSite: selectedSiteFromList, + } ) ); } else { setModalState( ( prev ) => ( { ...prev, mode: null } ) ); } From 38d8ae4e054f2318108317d38650ab0d1174e2f2 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Thu, 6 Nov 2025 13:58:19 +0100 Subject: [PATCH 13/18] Move SyncModalMode piece of state to Redux --- src/modules/sync/index.tsx | 53 +++++++----------------- src/modules/sync/tests/index.test.tsx | 7 ++++ src/stores/sync/connected-sites-slice.ts | 14 ++++++- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 74a8ae12c4..c912deb7b0 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -127,18 +127,13 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const { __ } = useI18n(); const dispatch = useAppDispatch(); const isModalOpen = useRootSelector( connectedSitesSelectors.selectIsModalOpen ); + const reduxModalMode = useRootSelector( connectedSitesSelectors.selectModalMode ); const { connectedSites } = useConnectedSitesData(); const { syncSites, isFetching, refetchSites } = useSyncSitesData(); const { connectSite, disconnectSite } = useConnectedSitesOperations(); const { pushSite, pullSite } = useSyncSites(); - const [ modalState, setModalState ] = useState< { - mode: SyncModalMode | null; - selectedRemoteSite: SyncSite | null; - } >( { - mode: null, - selectedRemoteSite: null, - } ); + const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null ); const { isAuthenticated } = useAuth(); @@ -164,13 +159,11 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } }; const handleLaunchSite = () => { - setModalState( ( prev ) => ( { ...prev, mode: 'push' } ) ); - dispatch( connectedSitesActions.openModal() ); + dispatch( connectedSitesActions.openModal( 'push' ) ); }; const handleImportSite = () => { - setModalState( ( prev ) => ( { ...prev, mode: 'pull' } ) ); - dispatch( connectedSitesActions.openModal() ); + dispatch( connectedSitesActions.openModal( 'pull' ) ); }; const handleSiteSelection = async ( siteId: number, mode: SyncModalMode | null ) => { @@ -193,19 +186,13 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } await handleConnect( selectedSiteFromList ); if ( mode === 'push' || mode === 'pull' ) { - setModalState( ( prev ) => ( { - ...prev, - mode: mode, - selectedRemoteSite: selectedSiteFromList, - } ) ); + dispatch( connectedSitesActions.setModalMode( mode ) ); + setSelectedRemoteSite( selectedSiteFromList ); } else { - setModalState( ( prev ) => ( { ...prev, mode: null } ) ); + dispatch( connectedSitesActions.setModalMode( null ) ); } }; - // Using a local variable to avoid non-null assertion operator in pushSite and pullSite - const selectedRemoteSite = modalState.selectedRemoteSite; - return (
{ connectedSites.length > 0 ? ( @@ -218,7 +205,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
dispatch( connectedSitesActions.openModal() ) } + connectSite={ () => dispatch( connectedSitesActions.openModal( 'connect' ) ) } disableConnectButtonStyle={ true } > { __( 'Connect another site' ) } @@ -250,47 +237,37 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } { isModalOpen && ( { dispatch( connectedSitesActions.closeModal() ); - setModalState( ( prev ) => ( { ...prev, mode: null } ) ); } } syncSites={ syncSites } onInitialRender={ refetchSites } onConnect={ async ( siteId: number ) => { - await handleSiteSelection( siteId, modalState.mode ); + await handleSiteSelection( siteId, reduxModalMode ); } } selectedSite={ selectedSite } /> ) } - { modalState.mode && modalState.mode !== 'connect' && selectedRemoteSite && ( + { reduxModalMode && reduxModalMode !== 'connect' && selectedRemoteSite && ( { const pushOptions = convertTreeToPushOptions( tree ); void pushSite( selectedRemoteSite, selectedSite, pushOptions ); - setModalState( { - mode: null, - selectedRemoteSite: null, - } ); + setSelectedRemoteSite( null ); } } onPull={ ( tree ) => { const pullOptions = convertTreeToPullOptions( tree ); void pullSite( selectedRemoteSite, selectedSite, pullOptions ); - setModalState( { - mode: null, - selectedRemoteSite: null, - } ); + setSelectedRemoteSite( null ); } } onRequestClose={ () => { - setModalState( { - mode: null, - selectedRemoteSite: null, - } ); + setSelectedRemoteSite( null ); } } /> ) } diff --git a/src/modules/sync/tests/index.test.tsx b/src/modules/sync/tests/index.test.tsx index d6a4e86a34..a3f551f657 100644 --- a/src/modules/sync/tests/index.test.tsx +++ b/src/modules/sync/tests/index.test.tsx @@ -45,11 +45,15 @@ jest.mock( 'src/stores/sync', () => ( { useConnectedSitesOperations: jest.fn(), connectedSitesSelectors: { selectIsModalOpen: jest.fn(), + selectModalMode: jest.fn(), }, connectedSitesActions: { openModal: jest.fn().mockImplementation( () => { return { type: 'connectedSites/openModal' }; } ), + setModalMode: jest.fn().mockImplementation( () => { + return { type: 'connectedSites/setModalMode' }; + } ), closeModal: jest.fn().mockImplementation( () => { return { type: 'connectedSites/closeModal' }; } ), @@ -190,6 +194,7 @@ describe( 'ContentTabSync', () => { 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( [ { @@ -280,6 +285,7 @@ describe( 'ContentTabSync', () => { fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); + ( connectedSitesSelectors.selectModalMode as jest.Mock ).mockReturnValue( null ); renderWithProvider( ); expect( screen.getByTestId( 'sync-sites-modal-selector' ) ).toBeInTheDocument(); @@ -362,6 +368,7 @@ describe( 'ContentTabSync', () => { expect( importButton ).toBeInTheDocument(); fireEvent.click( importButton ); ( connectedSitesSelectors.selectIsModalOpen as jest.Mock ).mockReturnValue( true ); + ( connectedSitesSelectors.selectModalMode as jest.Mock ).mockReturnValue( null ); renderWithProvider( ); const createNewSiteButton = screen.getByRole( 'button', { name: /Create a new WordPress.com site ↗/i, diff --git a/src/stores/sync/connected-sites-slice.ts b/src/stores/sync/connected-sites-slice.ts index debe8fb635..703f663f6b 100644 --- a/src/stores/sync/connected-sites-slice.ts +++ b/src/stores/sync/connected-sites-slice.ts @@ -2,6 +2,7 @@ import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@r 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 ConnectedSites = SyncSite[]; type ModalState = false | true | { disconnectSiteId?: number }; @@ -9,6 +10,7 @@ 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 { @@ -24,6 +26,7 @@ interface DisconnectSiteParams { const initialState: ConnectedSitesState = { sites: {}, isModalOpen: false, + modalMode: null, }; export const loadAllConnectedSites = createAsyncThunk( 'connectedSites/loadAll', async () => { @@ -96,12 +99,20 @@ const connectedSitesSlice = createSlice( { delete state.sites[ action.payload ]; }, - openModal: ( state ) => { + 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; + state.modalMode = null; }, }, extraReducers: ( builder ) => { @@ -125,6 +136,7 @@ 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, From c5bca9b98656bfd7841ca5061f4dde14a8912821 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Thu, 6 Nov 2025 14:01:06 +0100 Subject: [PATCH 14/18] Apply code review suggestion, use Next for Sync dialog button --- src/modules/sync/components/sync-sites-modal-selector.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/sync/components/sync-sites-modal-selector.tsx b/src/modules/sync/components/sync-sites-modal-selector.tsx index e9eede5e2c..290aaa1057 100644 --- a/src/modules/sync/components/sync-sites-modal-selector.tsx +++ b/src/modules/sync/components/sync-sites-modal-selector.tsx @@ -364,9 +364,8 @@ function Footer( { const getButtonText = () => { switch ( mode ) { case 'push': - return __( 'Publish' ); case 'pull': - return __( 'Import' ); + return __( 'Next' ); case 'connect': default: return __( 'Connect' ); From 953be86ed6b9b0611449b8c21e2c4778de7d4d9b Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Thu, 6 Nov 2025 18:00:39 +0100 Subject: [PATCH 15/18] Do not connect if cancelling the Push or Pull dialog --- src/modules/sync/index.tsx | 13 +++++++------ src/stores/sync/connected-sites-slice.ts | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index c912deb7b0..829058704d 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -183,13 +183,13 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } return; } - await handleConnect( selectedSiteFromList ); - if ( mode === 'push' || mode === 'pull' ) { dispatch( connectedSitesActions.setModalMode( mode ) ); setSelectedRemoteSite( selectedSiteFromList ); } else { + await handleConnect( selectedSiteFromList ); dispatch( connectedSitesActions.setModalMode( null ) ); + dispatch( connectedSitesActions.closeModal() ); } }; @@ -256,18 +256,19 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } type={ reduxModalMode } localSite={ selectedSite } remoteSite={ selectedRemoteSite } - onPush={ ( tree ) => { + onPush={ async ( tree ) => { const pushOptions = convertTreeToPushOptions( tree ); void pushSite( selectedRemoteSite, selectedSite, pushOptions ); - setSelectedRemoteSite( null ); + await handleConnect( selectedRemoteSite ); } } - onPull={ ( tree ) => { + onPull={ async ( tree ) => { const pullOptions = convertTreeToPullOptions( tree ); void pullSite( selectedRemoteSite, selectedSite, pullOptions ); - setSelectedRemoteSite( null ); + await handleConnect( selectedRemoteSite ); } } onRequestClose={ () => { setSelectedRemoteSite( null ); + dispatch( connectedSitesActions.setModalMode( null ) ); } } /> ) } diff --git a/src/stores/sync/connected-sites-slice.ts b/src/stores/sync/connected-sites-slice.ts index 703f663f6b..5157ad018e 100644 --- a/src/stores/sync/connected-sites-slice.ts +++ b/src/stores/sync/connected-sites-slice.ts @@ -112,7 +112,6 @@ const connectedSitesSlice = createSlice( { closeModal: ( state ) => { state.isModalOpen = false; - state.modalMode = null; }, }, extraReducers: ( builder ) => { From 1162da049d831392ab02561f02c04853a30161ab Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Thu, 6 Nov 2025 18:04:38 +0100 Subject: [PATCH 16/18] Connect site before calling push/pull --- src/modules/sync/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 829058704d..64ae4e7647 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -257,14 +257,14 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } localSite={ selectedSite } remoteSite={ selectedRemoteSite } onPush={ async ( tree ) => { + await handleConnect( selectedRemoteSite ); const pushOptions = convertTreeToPushOptions( tree ); void pushSite( selectedRemoteSite, selectedSite, pushOptions ); - await handleConnect( selectedRemoteSite ); } } onPull={ async ( tree ) => { + await handleConnect( selectedRemoteSite ); const pullOptions = convertTreeToPullOptions( tree ); void pullSite( selectedRemoteSite, selectedSite, pullOptions ); - await handleConnect( selectedRemoteSite ); } } onRequestClose={ () => { setSelectedRemoteSite( null ); From 51f1e6705903c917e4f2ff8f31143c29c7372e4c Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Fri, 7 Nov 2025 12:07:45 +0100 Subject: [PATCH 17/18] Add border and text color from design to secondary button --- src/modules/sync/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 64ae4e7647..b6ff09199d 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -225,6 +225,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } Date: Fri, 7 Nov 2025 13:43:08 +0100 Subject: [PATCH 18/18] Disable buttons when there is another site syncing --- src/modules/sync/components/connect-button.tsx | 17 +++++++---------- src/modules/sync/index.tsx | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/modules/sync/components/connect-button.tsx b/src/modules/sync/components/connect-button.tsx index c972430e04..83979e9ba2 100644 --- a/src/modules/sync/components/connect-button.tsx +++ b/src/modules/sync/components/connect-button.tsx @@ -3,12 +3,11 @@ import Button, { ButtonVariant } from 'src/components/button'; import offlineIcon from 'src/components/offline-icon'; import { Tooltip } from 'src/components/tooltip'; import { useOffline } from 'src/hooks/use-offline'; -import { cx } from 'src/lib/cx'; interface ConnectButtonProps { variant: ButtonVariant; connectSite?: () => void; - disableConnectButtonStyle?: boolean; + disabled?: boolean; className?: string; children?: React.ReactNode; tooltipText?: string; @@ -17,29 +16,27 @@ interface ConnectButtonProps { export const ConnectButton = ( { variant, connectSite, - disableConnectButtonStyle, + disabled, className, children, tooltipText, }: ConnectButtonProps ) => { const isOffline = useOffline(); const tooltipContent = tooltipText ?? __( 'Connecting a site requires an internet connection.' ); + const isDisabled = disabled || isOffline; return ( diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index b6ff09199d..dd08a82caf 100644 --- a/src/modules/sync/index.tsx +++ b/src/modules/sync/index.tsx @@ -131,7 +131,8 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } const { connectedSites } = useConnectedSitesData(); const { syncSites, isFetching, refetchSites } = useSyncSitesData(); const { connectSite, disconnectSite } = useConnectedSitesOperations(); - const { pushSite, pullSite } = useSyncSites(); + const { pushSite, pullSite, isAnySitePulling, isAnySitePushing } = useSyncSites(); + const isAnySiteSyncing = isAnySitePulling || isAnySitePushing; const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null ); @@ -206,7 +207,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } dispatch( connectedSitesActions.openModal( 'connect' ) ) } - disableConnectButtonStyle={ true } > { __( 'Connect another site' ) } @@ -218,16 +218,22 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } { __( 'Publish site' ) } { __( 'Pull site' ) }