From 11f9f3bab705b0d3f60705a38b5d533e615cfd1e Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 9 Jan 2026 13:49:11 +0100 Subject: [PATCH] Proof of concept --- .../use-listen-deep-link-connection.ts | 79 ++++++++++++++----- src/ipc-utils.ts | 10 ++- .../deeplink/handlers/sync-connect-site.ts | 4 + src/modules/sync/components/sync-dialog.tsx | 7 +- src/modules/sync/index.tsx | 65 +++++++++++---- 5 files changed, 129 insertions(+), 36 deletions(-) diff --git a/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/src/hooks/sync-sites/use-listen-deep-link-connection.ts index f329986bfe..8e50b375c6 100644 --- a/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -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, @@ -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 } } ); diff --git a/src/ipc-utils.ts b/src/ipc-utils.ts index e5e8de413b..aaea53100a 100644 --- a/src/ipc-utils.ts +++ b/src/ipc-utils.ts @@ -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 } ]; diff --git a/src/lib/deeplink/handlers/sync-connect-site.ts b/src/lib/deeplink/handlers/sync-connect-site.ts index 0bf504786b..fd010277e1 100644 --- a/src/lib/deeplink/handlers/sync-connect-site.ts +++ b/src/lib/deeplink/handlers/sync-connect-site.ts @@ -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, } ); } } diff --git a/src/modules/sync/components/sync-dialog.tsx b/src/modules/sync/components/sync-dialog.tsx index 72392f3450..8741aecb52 100644 --- a/src/modules/sync/components/sync-dialog.tsx +++ b/src/modules/sync/components/sync-dialog.tsx @@ -36,6 +36,7 @@ type SyncDialogProps = { onPush: ( syncData: TreeNode[] ) => void; onPull: ( syncData: TreeNode[] ) => void; onRequestClose: () => void; + isConnectionReady?: boolean; }; const useDynamicTreeState = ( @@ -135,6 +136,7 @@ export function SyncDialog( { onPush, onPull, onRequestClose, + isConnectionReady = true, }: SyncDialogProps ) { const locale = useI18nLocale(); const { __ } = useI18n(); @@ -445,10 +447,11 @@ export function SyncDialog( { isSubmitDisabled || isLoadingRewindId || isPushSelectionOverLimit || - isSizeCheckLoading + isSizeCheckLoading || + ! isConnectionReady } > - { syncTexts.submit } + { ! isConnectionReady ? __( 'Connecting...' ) : syncTexts.submit } diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx index 8a88c56824..5a03174a0d 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, 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'; @@ -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(); @@ -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 ) { @@ -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 ; } - 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 ) { @@ -199,7 +235,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } { connectedSites.length > 0 ? (
disconnectSite( { siteId: id, localSiteId: selectedSite.id } ) @@ -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 );