Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 60 additions & 19 deletions src/hooks/sync-sites/use-listen-deep-link-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAuth } from 'src/hooks/use-auth';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { SyncSite } from 'src/modules/sync/types';
import { useAppDispatch } from 'src/stores';
import {
connectedSitesActions,
Expand All @@ -28,25 +29,65 @@ export function useListenDeepLinkConnection() {

useIpcListener(
'sync-connect-site',
async ( _event, { remoteSiteId, studioSiteId, autoOpenPush } ) => {
// Fetch latest sites from network before checking
const result = await refetchWpComSites();
const latestSites = result.data ?? [];
const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId );
if ( newConnectedSite ) {
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
// Select studio site that started the sync
setSelectedSiteId( studioSiteId );
}
await connectSite( { site: newConnectedSite, localSiteId: studioSiteId } );
if ( selectedTab !== 'sync' ) {
// Switch to sync tab
setSelectedTab( 'sync' );
}
// Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
if ( autoOpenPush ) {
dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) );
}
async (
_event,
{
remoteSiteId,
studioSiteId,
autoOpenPush,
siteName,
siteUrl,
}: {
remoteSiteId: number;
studioSiteId: string;
autoOpenPush?: boolean;
siteName?: string;
siteUrl?: string;
}
) => {
// Create minimal site object optimistically to connect immediately
// Use siteName and siteUrl from deeplink if available, otherwise use placeholders
const minimalSite: SyncSite = {
id: remoteSiteId,
localSiteId: studioSiteId,
name: siteName || 'Loading site...', // Use provided name or placeholder
url: siteUrl || '', // Use provided URL or empty string
isStaging: false, // Default assumption
isPressable: false, // Default assumption
environmentType: null, // Will be fetched
syncSupport: 'already-connected', // Safe default for new connections
lastPullTimestamp: null, // New site, no history
lastPushTimestamp: null, // New site, no history
};

// Switch to the site that initiated the connection if needed
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
setSelectedSiteId( studioSiteId );
}

// Switch to sync tab
if ( selectedTab !== 'sync' ) {
setSelectedTab( 'sync' );
}

// Connect optimistically (async, don't block modal opening)
const connectPromise = connectSite( { site: minimalSite, localSiteId: studioSiteId } );

// Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
// Open modal immediately with minimal data
if ( autoOpenPush ) {
dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) );
}

// Fetch full site data in background (don't await - parallel operation)
const refetchPromise = refetchWpComSites();

// Wait for both operations to complete for error handling
try {
await Promise.all( [ connectPromise, refetchPromise ] );
} catch ( error ) {
console.error( 'Error during site connection:', error );
// Connection or refetch failed - the UI will handle the error state via mutation status
}
}
);
Expand Down
10 changes: 9 additions & 1 deletion src/ipc-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ export interface IpcEvents {
'snapshot-key-value': [ { operationId: crypto.UUID; data: SnapshotKeyValueEventData } ];
'snapshot-success': [ { operationId: crypto.UUID } ];
'show-whats-new': [ void ];
'sync-connect-site': [ { remoteSiteId: number; studioSiteId: string; autoOpenPush?: boolean } ];
'sync-connect-site': [
{
remoteSiteId: number;
studioSiteId: string;
autoOpenPush?: boolean;
siteName?: string;
siteUrl?: string;
},
];
'test-render-failure': [ void ];
'theme-details-changed': [ { id: string; details: StartedSiteDetails[ 'themeDetails' ] } ];
'theme-details-updating': [ { id: string } ];
Expand Down
4 changes: 4 additions & 0 deletions src/lib/deeplink/handlers/sync-connect-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ export async function handleSyncConnectSiteDeeplink( urlObject: URL ): Promise<
const remoteSiteId = parseInt( searchParams.get( 'remoteSiteId' ) ?? '' );
const studioSiteId = searchParams.get( 'studioSiteId' );
const autoOpenPush = searchParams.get( 'autoOpenPush' ) === 'true';
const siteName = searchParams.get( 'siteName' ) ?? undefined;
const siteUrl = searchParams.get( 'siteUrl' ) ?? undefined;

if ( remoteSiteId && studioSiteId ) {
void sendIpcEventToRenderer( 'sync-connect-site', {
remoteSiteId,
studioSiteId,
autoOpenPush,
siteName,
siteUrl,
} );
}
}
7 changes: 5 additions & 2 deletions src/modules/sync/components/sync-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type SyncDialogProps = {
onPush: ( syncData: TreeNode[] ) => void;
onPull: ( syncData: TreeNode[] ) => void;
onRequestClose: () => void;
isConnectionReady?: boolean;
};

const useDynamicTreeState = (
Expand Down Expand Up @@ -135,6 +136,7 @@ export function SyncDialog( {
onPush,
onPull,
onRequestClose,
isConnectionReady = true,
}: SyncDialogProps ) {
const locale = useI18nLocale();
const { __ } = useI18n();
Expand Down Expand Up @@ -445,10 +447,11 @@ export function SyncDialog( {
isSubmitDisabled ||
isLoadingRewindId ||
isPushSelectionOverLimit ||
isSizeCheckLoading
isSizeCheckLoading ||
! isConnectionReady
}
>
{ syncTexts.submit }
{ ! isConnectionReady ? __( 'Connecting...' ) : syncTexts.submit }
</Button>
</div>
</div>
Expand Down
65 changes: 51 additions & 14 deletions src/modules/sync/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { check, Icon } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { ArrowIcon } from 'src/components/arrow-icon';
import Button from 'src/components/button';
import offlineIcon from 'src/components/offline-icon';
Expand Down Expand Up @@ -136,7 +136,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
localSiteId: selectedSite.id,
userId: user?.id,
} );
const [ connectSite ] = useConnectSiteMutation();
const [ connectSite, { isLoading: isConnecting } ] = useConnectSiteMutation();
const [ disconnectSite ] = useDisconnectSiteMutation();
const { pushSite, pullSite } = useSyncSites();

Expand All @@ -146,8 +146,19 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
userId: user?.id,
} );

// Merge connectedSites with syncSites to get the most up-to-date data
// This ensures the Sync tab shows current data even before reconciliation updates storage
const mergedConnectedSites = connectedSites.map( ( connectedSite ) => {
const syncSite = syncSites.find( ( site ) => site.id === connectedSite.id );
// If we have data from the API (syncSites), use it; otherwise use storage data
return syncSite || connectedSite;
} );

const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null );

// Check if connection is ready using RTK Query's built-in loading state
const isConnectionReady = ! isConnecting;

// Auto-select remote site when set via Redux (e.g., from deep link connection)
useEffect( () => {
if ( selectedRemoteSiteId ) {
Expand All @@ -160,21 +171,46 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
}
}, [ selectedRemoteSiteId, syncSites, dispatch ] );

// Update selectedRemoteSite when syncSites updates with more complete data
// This ensures the modal shows updated site info when background refetch completes
useEffect( () => {
if ( selectedRemoteSite ) {
const updatedSite = syncSites.find( ( site ) => site.id === selectedRemoteSite.id );
if ( updatedSite && updatedSite !== selectedRemoteSite ) {
// Update with the more complete site data from refetch
setSelectedRemoteSite( updatedSite );
}
}
}, [ syncSites, selectedRemoteSite ] );

const handleConnect = useCallback(
async ( newConnectedSite: SyncSite ) => {
// Check if already connected (use connectedSites from storage as source of truth)
const isAlreadyConnected = connectedSites.some( ( site ) => site.id === newConnectedSite.id );
if ( isAlreadyConnected ) {
// Site is already connected, no need to reconnect
return;
}

// Note: Connection status check is handled by the disabled button state
// If connection is pending, the button will be disabled

try {
await connectSite( { site: newConnectedSite, localSiteId: selectedSite.id } );
} catch ( error ) {
getIpcApi().showErrorMessageBox( {
title: __( 'Failed to connect to site' ),
message: __( 'Please try again.' ),
} );
}
},
[ connectedSites, connectSite, selectedSite.id, __ ]
);

if ( ! isAuthenticated ) {
return <NoAuthSyncTab />;
}

const handleConnect = async ( newConnectedSite: SyncSite ) => {
try {
await connectSite( { site: newConnectedSite, localSiteId: selectedSite.id } );
} catch ( error ) {
getIpcApi().showErrorMessageBox( {
title: __( 'Failed to connect to site' ),
message: __( 'Please try again.' ),
} );
}
};

const handleSiteSelection = async ( siteId: number ) => {
const selectedSiteFromList = syncSites.find( ( site ) => site.id === siteId );
if ( ! selectedSiteFromList ) {
Expand All @@ -199,7 +235,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
{ connectedSites.length > 0 ? (
<div className="h-full relative">
<SyncConnectedSites
connectedSites={ connectedSites }
connectedSites={ mergedConnectedSites }
selectedSite={ selectedSite }
disconnectSite={ ( id ) =>
disconnectSite( { siteId: id, localSiteId: selectedSite.id } )
Expand Down Expand Up @@ -245,6 +281,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
type={ reduxModalMode }
localSite={ selectedSite }
remoteSite={ selectedRemoteSite }
isConnectionReady={ isConnectionReady }
onPush={ async ( tree ) => {
await handleConnect( selectedRemoteSite );
const pushOptions = convertTreeToPushOptions( tree );
Expand Down