Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
52 changes: 46 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,27 @@ 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':
case 'pull':
return __( 'Next' );
case 'connect':
default:
return __( 'Connect' );
}
};

useEffect( () => {
if ( ! disabled ) {
focusConnectButton();
Expand All @@ -361,20 +390,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>
);
};
115 changes: 90 additions & 25 deletions src/modules/sync/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
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,
Expand All @@ -21,6 +27,7 @@ import {
connectedSitesActions,
} 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 +41,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 existing 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 @@ -120,9 +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 [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null );

const { isAuthenticated } = useAuth();

Expand All @@ -147,6 +158,41 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
}
};

const handleLaunchSite = () => {
dispatch( connectedSitesActions.openModal( 'push' ) );
};

const handleImportSite = () => {
dispatch( connectedSitesActions.openModal( 'pull' ) );
};

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;
}

if ( mode === 'push' || mode === 'pull' ) {
dispatch( connectedSitesActions.setModalMode( mode ) );
setSelectedRemoteSite( selectedSiteFromList );
} else {
await handleConnect( selectedSiteFromList );
dispatch( connectedSitesActions.setModalMode( null ) );
dispatch( connectedSitesActions.closeModal() );
}
};

return (
<div className="flex flex-col h-full overflow-y-auto">
{ connectedSites.length > 0 ? (
Expand All @@ -159,7 +205,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
<div className="sticky bottom-0 bg-white/[0.8] backdrop-blur-sm w-full px-8 py-6 mt-auto">
<ConnectButton
variant="primary"
connectSite={ () => dispatch( connectedSitesActions.openModal() ) }
connectSite={ () => dispatch( connectedSitesActions.openModal( 'connect' ) ) }
disableConnectButtonStyle={ true }
>
{ __( 'Connect another site' ) }
Expand All @@ -168,45 +214,64 @@ 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.' ) }
>
{ __( 'Publish site' ) }
</ConnectButton>
<ConnectButton
variant="secondary"
connectSite={ handleImportSite }
disableConnectButtonStyle={ true }
tooltipText={ __( 'Importing a remote site requires an internet connection.' ) }
>
{ __( 'Connect site' ) }
{ __( '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={ reduxModalMode || 'connect' }
isLoading={ isFetching }
onRequestClose={ () => dispatch( connectedSitesActions.closeModal() ) }
onRequestClose={ () => {
dispatch( connectedSitesActions.closeModal() );
} }
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, reduxModalMode );
} }
selectedSite={ selectedSite }
/>
) }

{ reduxModalMode && reduxModalMode !== 'connect' && selectedRemoteSite && (
<SyncDialog
type={ reduxModalMode }
localSite={ selectedSite }
remoteSite={ selectedRemoteSite }
onPush={ async ( tree ) => {
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.setModalMode( null ) );
} }
/>
) }
</div>
);
}
Loading