Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a1a7570
Implement new site launch and import buttons
epeicher Nov 4, 2025
83e3af9
Fix linting errors
epeicher Nov 4, 2025
d747572
Use connect button when there is a site connected
epeicher Nov 4, 2025
e9cd44b
Merge branch 'trunk' of github.com:Automattic/studio into update/conn…
epeicher Nov 5, 2025
847efa4
Simplify state, update button text and fix tests
epeicher Nov 5, 2025
c643361
Update wording, reuse ConnectButton component
epeicher Nov 5, 2025
59dd4f4
Add a new type and simplify code
epeicher Nov 5, 2025
42d50c4
Fix types
epeicher Nov 5, 2025
2e3fea4
Simplify logic
epeicher Nov 5, 2025
304b9a7
Remove superfluous comment and simplify
epeicher Nov 5, 2025
3ee87ae
Merge branch 'trunk' of github.com:Automattic/studio into update/conn…
epeicher Nov 5, 2025
53b7a23
Use strict equality operator
epeicher Nov 5, 2025
e276491
Use local variable to avoid non-null assertion
epeicher Nov 5, 2025
014d956
Merge branch 'trunk' of github.com:Automattic/studio into update/conn…
epeicher Nov 6, 2025
bb66758
Simplify logic for selecting sites, fix typo
epeicher Nov 6, 2025
38d8ae4
Move SyncModalMode piece of state to Redux
epeicher Nov 6, 2025
c5bca9b
Apply code review suggestion, use Next for Sync dialog button
epeicher Nov 6, 2025
953be86
Do not connect if cancelling the Push or Pull dialog
epeicher Nov 6, 2025
1162da0
Connect site before calling push/pull
epeicher Nov 6, 2025
51f1e67
Add border and text color from design to secondary button
epeicher Nov 7, 2025
9b85fdf
Disable buttons when there is another site syncing
epeicher Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/modules/sync/components/connect-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ConnectButtonProps {
disableConnectButtonStyle?: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was always used as true, so it has been removed, and the functionality updated

className?: string;
children?: React.ReactNode;
tooltipText?: string;
}

export const ConnectButton = ( {
Expand All @@ -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 (
<Tooltip
disabled={ ! isOffline }
text={ __( 'Connecting a site requires an internet connection.' ) }
text={ tooltipContent }
icon={ offlineIcon }
placement="top-start"
>
Expand Down
53 changes: 47 additions & 6 deletions src/modules/sync/components/sync-sites-modal-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -32,13 +33,15 @@ export function SyncSitesModalSelector( {
syncSites,
onInitialRender,
selectedSite,
mode = 'connect',
}: {
isLoading?: boolean;
onRequestClose: () => void;
syncSites: SyncSite[];
onConnect: ( siteId: number ) => void;
onInitialRender?: () => void;
selectedSite: SiteDetails;
mode?: SyncModalMode;
} ) {
const { __ } = useI18n();
const [ selectedSiteId, setSelectedSiteId ] = useState< number | null >( null );
Expand All @@ -53,6 +56,18 @@ export function SyncSitesModalSelector( {
} );
const isEmpty = filteredSites.length === 0;

const getModalTitle = () => {
switch ( mode ) {
case 'push':
return __( 'Publish your site' );
case 'pull':
return __( 'Select a site to import' );
case 'connect':
default:
return __( 'Connect your site' );
}
};

useEffect( () => {
if ( onInitialRender ) {
onInitialRender();
Expand All @@ -63,7 +78,7 @@ export function SyncSitesModalSelector( {
<Modal
className="w-3/5 min-w-[550px] h-full max-h-[84vh] [&>div]:!p-0"
onRequestClose={ onRequestClose }
title={ __( 'Connect your site' ) }
title={ getModalTitle() }
>
<div className="relative" data-testid="sync-sites-modal-selector">
<SearchSites searchQuery={ searchQuery } setSearchQuery={ setSearchQuery } />
Expand Down Expand Up @@ -100,11 +115,12 @@ export function SyncSitesModalSelector( {
} }
disabled={ ! selectedSiteId }
selectedSite={ selectedSite }
mode={ mode }
/>

{ isOffline && (
<div className="absolute inset-0 bg-white/80 z-10 flex items-center justify-center">
<SyncSitesOfflineView />
<SyncSitesOfflineView mode={ mode } />
</div>
) }
</div>
Expand Down Expand Up @@ -335,14 +351,28 @@ function Footer( {
onConnect,
disabled,
selectedSite,
mode = 'connect',
}: {
onRequestClose: () => void;
onConnect: () => void;
disabled: boolean;
selectedSite: SiteDetails;
mode?: SyncModalMode;
} ) {
const { __ } = useI18n();

const getButtonText = () => {
switch ( mode ) {
case 'push':
return __( 'Publish' );
case 'pull':
return __( 'Import' );
case 'connect':
default:
return __( 'Connect' );
}
};

useEffect( () => {
if ( ! disabled ) {
focusConnectButton();
Expand All @@ -361,20 +391,31 @@ function Footer( {
{ __( 'Cancel' ) }
</Button>
<Button id="connect-button" variant="primary" disabled={ disabled } onClick={ onConnect }>
{ __( 'Connect' ) }
{ getButtonText() }
</Button>
</div>
</div>
);
}

const SyncSitesOfflineView = () => {
const offlineMessage = __( 'Connecting a site requires an internet connection.' );
const SyncSitesOfflineView = ( { mode = 'connect' }: { mode?: SyncModalMode } ) => {
const { __ } = useI18n();
const getOfflineMessage = () => {
switch ( mode ) {
case 'push':
return __( 'Publishing 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 (
<div className="flex items-center justify-center h-12 px-2 pt-4 text-a8c-gray-70 gap-1">
<Icon className="m-1 fill-a8c-gray-70" size={ 24 } icon={ offlineIcon } />
<span className="text-[13px] leading-[16px]">{ offlineMessage }</span>
<span className="text-[13px] leading-[16px]">{ getOfflineMessage() }</span>
</div>
);
};
148 changes: 123 additions & 25 deletions src/modules/sync/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
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';
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';
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,
useSyncSitesData,
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';

function SiteSyncDescription( { children }: PropsWithChildren ) {
const { __ } = useI18n();
Expand All @@ -34,13 +42,13 @@ function SiteSyncDescription( { children }: PropsWithChildren ) {
</div>
<div className="max-w-[40ch] text-a8c-gray-70 a8c-body">
{ __(
'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 existing WordPress.com or Jetpack-activated Pressable sites, or import an exisiting one. Then, share your work with the world.'
) }
</div>
<div className="mt-6">
{ [
__( 'Push and pull changes from your live site.' ),
__( 'Connect multiple environments.' ),
__( 'Supports staging and production sites.' ),
__( 'Sync database and file changes.' ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am finding file changes in there to sounding a bit strange, I would change it to something simpler like:

Suggested change
__( 'Sync database and file changes.' ),
__( 'Sync files and database.' ),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem changing it! It was already like that, though, and it’s in the design, so maybe worth a quick check with others? What do you think @sejas or @wojtekn?

].map( ( text ) => (
<div key={ text } className="text-a8c-gray-70 a8c-body flex items-center">
Expand Down Expand Up @@ -123,6 +131,15 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
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 { isAuthenticated } = useAuth();

Expand All @@ -136,9 +153,14 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
return <NoAuthSyncTab />;
}

const handleConnect = async ( newConnectedSite: SyncSite ) => {
const handleConnect = async ( newConnectedSite: SyncSite ): Promise< SyncSite | undefined > => {
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' ),
Expand All @@ -147,6 +169,54 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
}
};

const handleLaunchSite = () => {
setModalState( ( prev ) => ( { ...prev, mode: 'push' } ) );
dispatch( connectedSitesActions.openModal() );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the modalState could be part of the Redux slice and the openModal could receive the mode as parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great suggestion! I have actually needed to do it on #2025, I can do it in this PR though so we test with the final code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and moved to Redux, it simplifies the code a lot 38d8ae4

};

const handleImportSite = () => {
setModalState( ( prev ) => ( { ...prev, mode: 'pull' } ) );
dispatch( connectedSitesActions.openModal() );
};

const handleSiteSelection = async ( siteId: number, mode: SyncModalMode | null ) => {
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;
}

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() );

if ( mode === 'push' || mode === 'pull' ) {
setModalState( ( prev ) => ( { ...prev, mode: mode, selectedRemoteSite: siteToUse } ) );
} else {
setModalState( ( prev ) => ( { ...prev, mode: null } ) );
}
};

// Using a local variable to avoid non-null assertion operator in pushSite and pullSite
const selectedRemoteSite = modalState.selectedRemoteSite;

return (
<div className="flex flex-col h-full overflow-y-auto">
{ connectedSites.length > 0 ? (
Expand All @@ -168,45 +238,73 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
</div>
) : (
<SiteSyncDescription>
<div className="mt-8">
<div className="mt-8 flex flex-wrap gap-4">
<ConnectButton
variant="primary"
connectSite={ () => dispatch( connectedSitesActions.openModal() ) }
connectSite={ handleLaunchSite }
disableConnectButtonStyle={ true }
tooltipText={ __( 'Publishing your site requires an internet connection.' ) }
>
{ __( 'Connect site' ) }
{ __( 'Publish site' ) }
</ConnectButton>
<ConnectButton
variant="secondary"
connectSite={ handleImportSite }
disableConnectButtonStyle={ true }
tooltipText={ __( 'Importing a remote site requires an internet connection.' ) }
>
{ __( 'Pull site' ) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have a blue border on this button according to the design:

Image

And I am currently seeing the black one:

Image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted! Changed as part of 51f1e67 in a similar way than the Export database button. Ideally, we should not need to override the style buttons, but I would tackle that as a follow-up, as it implies checking all the Buttons used to avoid regressions

</ConnectButton>
</div>
</SiteSyncDescription>
) }

{ isModalOpen && (
<SyncSitesModalSelector
mode={ modalState.mode || 'connect' }
isLoading={ isFetching }
onRequestClose={ () => dispatch( connectedSitesActions.closeModal() ) }
onRequestClose={ () => {
dispatch( connectedSitesActions.closeModal() );
setModalState( ( prev ) => ( { ...prev, mode: null } ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the modalState would live inside the Redux slice, then we could clear the mode inside closeModal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's absolutely right, done as part of 38d8ae4

} }
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={ async ( siteId: number ) => {
await handleSiteSelection( siteId, modalState.mode );
} }
selectedSite={ selectedSite }
/>
) }

{ modalState.mode && modalState.mode !== 'connect' && selectedRemoteSite && (
<SyncDialog
type={ modalState.mode }
localSite={ selectedSite }
remoteSite={ selectedRemoteSite }
onPush={ ( tree ) => {
const pushOptions = convertTreeToPushOptions( tree );
void pushSite( selectedRemoteSite, selectedSite, pushOptions );
setModalState( {
mode: null,
selectedRemoteSite: null,
} );
} }
onPull={ ( tree ) => {
const pullOptions = convertTreeToPullOptions( tree );
void pullSite( selectedRemoteSite, selectedSite, pullOptions );
setModalState( {
mode: null,
selectedRemoteSite: null,
} );
} }
onRequestClose={ () => {
setModalState( {
mode: null,
selectedRemoteSite: null,
} );
} }
/>
) }
</div>
);
}
Loading