From 11f9f3bab705b0d3f60705a38b5d533e615cfd1e Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 9 Jan 2026 13:49:11 +0100 Subject: [PATCH 01/12] 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 ); From ea3de7d93fa16c2dd45739e6f521d86bf234cc0f Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Thu, 19 Feb 2026 16:24:15 +0100 Subject: [PATCH 02/12] Fetch for one site --- .../use-listen-deep-link-connection.ts | 47 ++++---- .../sync/components/sync-connected-sites.tsx | 59 +++++++--- .../components/sync-sites-modal-selector.tsx | 103 +++++++++++------- .../src/modules/sync/tests/index.test.tsx | 2 + apps/studio/src/modules/sync/types.ts | 1 + apps/studio/src/stores/sync/wpcom-sites.ts | 78 ++++++++++++- 6 files changed, 211 insertions(+), 79 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index 8e50b375c6..5be3ba289d 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -4,12 +4,8 @@ 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, - useConnectSiteMutation, - useGetConnectedSitesForLocalSiteQuery, -} from 'src/stores/sync/connected-sites'; -import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites'; +import { connectedSitesActions, useConnectSiteMutation } from 'src/stores/sync/connected-sites'; +import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; export function useListenDeepLinkConnection() { const dispatch = useAppDispatch(); @@ -17,15 +13,6 @@ export function useListenDeepLinkConnection() { const { selectedSite, setSelectedSiteId } = useSiteDetails(); const { setSelectedTab, selectedTab } = useContentTabs(); const { user } = useAuth(); - const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( { - localSiteId: selectedSite?.id, - userId: user?.id, - } ); - const connectedSiteIds = connectedSites.map( ( { id } ) => id ); - const { refetch: refetchWpComSites } = useGetWpComSitesQuery( { - connectedSiteIds, - userId: user?.id, - } ); useIpcListener( 'sync-connect-site', @@ -52,12 +39,13 @@ export function useListenDeepLinkConnection() { 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 + isStaging: false, // Placeholder + isPressable: false, // Placeholder 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 + isLoading: true, // Mark as loading until single-site fetch completes }; // Switch to the site that initiated the connection if needed @@ -79,12 +67,31 @@ export function useListenDeepLinkConnection() { dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) ); } - // Fetch full site data in background (don't await - parallel operation) - const refetchPromise = refetchWpComSites(); + // Fetch full site data in background using optimized single-site endpoint + const fetchSingleSitePromise = dispatch( + wpcomSitesApi.endpoints.getSingleWpComSite.initiate( { + siteId: remoteSiteId, + userId: user?.id, + } ) + ); // Wait for both operations to complete for error handling try { - await Promise.all( [ connectPromise, refetchPromise ] ); + const [ , singleSiteResult ] = await Promise.all( [ + connectPromise, + fetchSingleSitePromise, + ] ); + + // If we successfully fetched the single site, update the connection with full data + if ( singleSiteResult.data ) { + const fullSiteData: SyncSite = { + ...singleSiteResult.data, + localSiteId: studioSiteId, + }; + await connectSite( { site: fullSiteData, localSiteId: studioSiteId } ); + } + + fetchSingleSitePromise.unsubscribe(); } 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/apps/studio/src/modules/sync/components/sync-connected-sites.tsx b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx index 44eff2fdd1..3694c8076e 100644 --- a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx +++ b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx @@ -263,19 +263,33 @@ const SyncConnectedSitesSectionItem = ( { key={ connectedSite.id } >
- + { connectedSite.isLoading ? ( +
+ ) : ( + + ) }
- + { connectedSite.isLoading ? ( +
+ ) : ( + + ) }
{ isPulling && ( @@ -547,7 +561,15 @@ const SyncConnectedSiteSection = ( { const isPushing = isSiteIdPushing( selectedSite.id, connectedSite.id ); let logo = ; - if ( hasConnectionErrors ) { + if ( connectedSite.isLoading ) { + // Show skeleton loader while fetching full site data + logo = ( +
+ ); + } else if ( hasConnectionErrors ) { logo = ; } else if ( connectedSite.isPressable ) { logo = ; @@ -557,9 +579,16 @@ const SyncConnectedSiteSection = ( {
{ logo } -
- { connectedSite.name } -
+ { connectedSite.isLoading ? ( +
+ ) : ( +
+ { connectedSite.name } +
+ ) }
-
- { isPressable && ( - - - - ) } - { ! isPressable && ( - - - - ) } - { site.name } -
- + { site.isLoading ? ( +
+
+
+
+ ) : ( +
+ { isPressable ? ( + + + + ) : ( + + + + ) } + { site.name } +
+ ) } + { site.isLoading ? ( +
+ ) : ( + + ) }
{ isSyncable && (
diff --git a/apps/studio/src/modules/sync/tests/index.test.tsx b/apps/studio/src/modules/sync/tests/index.test.tsx index 9d365c2286..d582e403d0 100644 --- a/apps/studio/src/modules/sync/tests/index.test.tsx +++ b/apps/studio/src/modules/sync/tests/index.test.tsx @@ -118,6 +118,7 @@ const fakeSyncSite: SyncSite = { localSiteId: 'site-id', lastPullTimestamp: null, lastPushTimestamp: null, + isLoading: false, }; describe( 'ContentTabSync', () => { @@ -306,6 +307,7 @@ describe( 'ContentTabSync', () => { isPressable: false, lastPullTimestamp: null, lastPushTimestamp: null, + isLoading: false, }; vi.mocked( useAuth, { partial: true } ).mockReturnValue( createAuthMock( true ) ); setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] ); diff --git a/apps/studio/src/modules/sync/types.ts b/apps/studio/src/modules/sync/types.ts index f2ea55a873..a580b73704 100644 --- a/apps/studio/src/modules/sync/types.ts +++ b/apps/studio/src/modules/sync/types.ts @@ -27,4 +27,5 @@ export type SyncSite = { syncSupport: SyncSupport; lastPullTimestamp: string | null; lastPushTimestamp: string | null; + isLoading?: boolean; // True when site data is incomplete (e.g., from deep link before full fetch) }; diff --git a/apps/studio/src/stores/sync/wpcom-sites.ts b/apps/studio/src/stores/sync/wpcom-sites.ts index e794abbe49..a070044194 100644 --- a/apps/studio/src/stores/sync/wpcom-sites.ts +++ b/apps/studio/src/stores/sync/wpcom-sites.ts @@ -122,6 +122,76 @@ export const wpcomSitesApi = createApi( { baseQuery: fetchBaseQuery(), tagTypes: [ 'WpComSites' ], endpoints: ( builder ) => ( { + getSingleWpComSite: builder.query< SyncSite, { siteId: number; userId?: number } >( { + queryFn: async ( { siteId } ) => { + const wpcomClient = getWpcomClient(); + if ( ! wpcomClient ) { + return { error: { status: 401, data: 'Not authenticated' } }; + } + + try { + const fields = [ + 'name', + 'ID', + 'URL', + 'plan', + 'capabilities', + 'is_wpcom_atomic', + 'options', + 'jetpack', + 'is_deleted', + 'is_a8c', + 'hosting_provider_guess', + 'environment_type', + ].join( ',' ); + + const response = await wpcomClient.req.get( + { + apiNamespace: 'rest/v1.1', + path: `/sites/${ siteId }`, + }, + { + fields, + options: 'created_at,wpcom_staging_blog_ids', + } + ); + + const parsedSite = sitesEndpointSiteSchema.parse( response ); + + // Fetch all connected sites to determine if this is a staging site + const allConnectedSites = await getIpcApi().getConnectedWpcomSites(); + + // Fetch parent sites' staging IDs to check if this site is a staging site + // Note: We can't get all staging IDs without fetching /me/sites, so we approximate + // by checking if the site has environment_type set + const isStaging = + parsedSite.environment_type === 'staging' || + parsedSite.environment_type === 'development'; + + const syncSupport = getSyncSupport( + parsedSite, + allConnectedSites.map( ( { id } ) => id ) + ); + + const syncSite = transformSingleSiteResponse( parsedSite, syncSupport, isStaging ); + + return { data: syncSite }; + } catch ( error ) { + Sentry.captureException( error ); + console.error( error ); + return { + error: { + status: 500, + data: error, + }, + }; + } + }, + providesTags: ( _result, _error, arg ) => [ + { type: 'WpComSites', userId: arg.userId }, + { type: 'WpComSites', id: arg.siteId }, + ], + } ), getWpComSites: builder.query< SyncSite[], { connectedSiteIds?: number[]; userId?: number } >( { queryFn: async ( { connectedSiteIds } ) => { const wpcomClient = getWpcomClient(); @@ -195,8 +265,12 @@ export const wpcomSitesApi = createApi( { } ), } ); -const { useGetWpComSitesQuery: useGetWpComSitesQueryBase } = wpcomSitesApi; +const { + useGetWpComSitesQuery: useGetWpComSitesQueryBase, + useGetSingleWpComSiteQuery: useGetSingleWpComSiteQueryBase, +} = wpcomSitesApi; -// Wrap the query hook with offline check +// Wrap the query hooks with offline check // Authentication is already handled in queryFn which checks wpcomClient export const useGetWpComSitesQuery = withOfflineCheck( useGetWpComSitesQueryBase ); +export const useGetSingleWpComSiteQuery = withOfflineCheck( useGetSingleWpComSiteQueryBase ); From eb9646bf84b25f8df02317ceeafd8ea8aa8fa821 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Thu, 19 Feb 2026 16:28:46 +0100 Subject: [PATCH 03/12] Clean up placeholder --- .../src/hooks/sync-sites/use-listen-deep-link-connection.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index 5be3ba289d..bc23053762 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -37,8 +37,8 @@ export function useListenDeepLinkConnection() { const minimalSite: SyncSite = { id: remoteSiteId, localSiteId: studioSiteId, - name: siteName || 'Loading site...', // Use provided name or placeholder - url: siteUrl || '', // Use provided URL or empty string + name: siteName ?? '', // Will be replaced by fetch; not shown (loading skeleton displayed) + url: siteUrl ?? '', // Will be replaced by fetch; not shown (loading skeleton displayed) isStaging: false, // Placeholder isPressable: false, // Placeholder environmentType: null, // Will be fetched @@ -67,7 +67,6 @@ export function useListenDeepLinkConnection() { dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) ); } - // Fetch full site data in background using optimized single-site endpoint const fetchSingleSitePromise = dispatch( wpcomSitesApi.endpoints.getSingleWpComSite.initiate( { siteId: remoteSiteId, From ce75d91982461d279243cf9600699154212c8dbf Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Thu, 19 Feb 2026 16:31:26 +0100 Subject: [PATCH 04/12] Cleanup --- .../src/hooks/sync-sites/use-listen-deep-link-connection.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index bc23053762..cdd29b067f 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -58,7 +58,6 @@ export function useListenDeepLinkConnection() { 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) @@ -81,7 +80,6 @@ export function useListenDeepLinkConnection() { fetchSingleSitePromise, ] ); - // If we successfully fetched the single site, update the connection with full data if ( singleSiteResult.data ) { const fullSiteData: SyncSite = { ...singleSiteResult.data, From d5d2a03a5a540e64575f483bb48e218c3db78354 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Thu, 19 Feb 2026 16:32:43 +0100 Subject: [PATCH 05/12] Cleanup --- apps/studio/src/stores/sync/wpcom-sites.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/studio/src/stores/sync/wpcom-sites.ts b/apps/studio/src/stores/sync/wpcom-sites.ts index a070044194..6c8d530c78 100644 --- a/apps/studio/src/stores/sync/wpcom-sites.ts +++ b/apps/studio/src/stores/sync/wpcom-sites.ts @@ -158,12 +158,9 @@ export const wpcomSitesApi = createApi( { const parsedSite = sitesEndpointSiteSchema.parse( response ); - // Fetch all connected sites to determine if this is a staging site const allConnectedSites = await getIpcApi().getConnectedWpcomSites(); - // Fetch parent sites' staging IDs to check if this site is a staging site - // Note: We can't get all staging IDs without fetching /me/sites, so we approximate - // by checking if the site has environment_type set + // Determine if staging by checking environment_type (can't access parent site's staging IDs without fetching /me/sites) const isStaging = parsedSite.environment_type === 'staging' || parsedSite.environment_type === 'development'; From e04f7f0141ae91ca66fbd3709c5280026060a4f8 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Thu, 19 Feb 2026 18:08:48 +0100 Subject: [PATCH 06/12] Fix implementation --- .../use-listen-deep-link-connection.ts | 56 +++++++++++----- .../sync/components/sync-connected-sites.tsx | 16 +++-- .../modules/sync/components/sync-dialog.tsx | 7 +- .../components/sync-sites-modal-selector.tsx | 13 ++-- apps/studio/src/modules/sync/index.tsx | 65 ++++--------------- .../src/modules/sync/tests/index.test.tsx | 2 - apps/studio/src/modules/sync/types.ts | 1 - .../studio/src/stores/sync/connected-sites.ts | 13 ++++ 8 files changed, 88 insertions(+), 85 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index cdd29b067f..eedc8a0581 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -2,10 +2,16 @@ 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 { getIpcApi } from 'src/lib/get-ipc-api'; import { SyncSite } from 'src/modules/sync/types'; import { useAppDispatch } from 'src/stores'; -import { connectedSitesActions, useConnectSiteMutation } from 'src/stores/sync/connected-sites'; -import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; +import { + connectedSitesActions, + connectedSitesApi, + useConnectSiteMutation, + useGetConnectedSitesForLocalSiteQuery, +} from 'src/stores/sync/connected-sites'; +import { wpcomSitesApi, useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites'; export function useListenDeepLinkConnection() { const dispatch = useAppDispatch(); @@ -13,6 +19,15 @@ export function useListenDeepLinkConnection() { const { selectedSite, setSelectedSiteId } = useSiteDetails(); const { setSelectedTab, selectedTab } = useContentTabs(); const { user } = useAuth(); + const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( { + localSiteId: selectedSite?.id, + userId: user?.id, + } ); + const connectedSiteIds = connectedSites.map( ( { id } ) => id ); + const { refetch: refetchWpComSites } = useGetWpComSitesQuery( { + connectedSiteIds, + userId: user?.id, + } ); useIpcListener( 'sync-connect-site', @@ -33,19 +48,17 @@ export function useListenDeepLinkConnection() { } ) => { // 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 ?? '', // Will be replaced by fetch; not shown (loading skeleton displayed) - url: siteUrl ?? '', // Will be replaced by fetch; not shown (loading skeleton displayed) - isStaging: false, // Placeholder - isPressable: false, // Placeholder - 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 - isLoading: true, // Mark as loading until single-site fetch completes + name: siteName ?? '', + url: siteUrl ?? '', + isStaging: false, + isPressable: false, + environmentType: null, + syncSupport: 'already-connected', + lastPullTimestamp: null, + lastPushTimestamp: null, }; // Switch to the site that initiated the connection if needed @@ -58,10 +71,12 @@ export function useListenDeepLinkConnection() { setSelectedTab( 'sync' ); } + // Mark site as loading in ephemeral Redux state (not persisted to storage) + dispatch( connectedSitesActions.addLoadingSiteId( remoteSiteId ) ); + 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 ) ); } @@ -73,7 +88,7 @@ export function useListenDeepLinkConnection() { } ) ); - // Wait for both operations to complete for error handling + // Wait for both operations to complete try { const [ , singleSiteResult ] = await Promise.all( [ connectPromise, @@ -85,13 +100,22 @@ export function useListenDeepLinkConnection() { ...singleSiteResult.data, localSiteId: studioSiteId, }; - await connectSite( { site: fullSiteData, localSiteId: studioSiteId } ); + // Use updateSingleConnectedWpcomSite to overwrite the minimal site in storage. + // connectSite only adds new sites — it won't update an existing entry. + await getIpcApi().updateSingleConnectedWpcomSite( fullSiteData ); + dispatch( connectedSitesApi.util.invalidateTags( [ 'ConnectedSites' ] ) ); } fetchSingleSitePromise.unsubscribe(); + + // Refetch all sites to update syncSites (used by the push/pull modal). + // Await so loading state persists until the modal can display full site data. + await refetchWpComSites(); } catch ( error ) { console.error( 'Error during site connection:', error ); - // Connection or refetch failed - the UI will handle the error state via mutation status + } finally { + // Always clear loading state, even on error + dispatch( connectedSitesActions.removeLoadingSiteId( remoteSiteId ) ); } } ); diff --git a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx index 3694c8076e..4a1614335e 100644 --- a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx +++ b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx @@ -35,9 +35,10 @@ import { convertTreeToPushOptions, } from 'src/modules/sync/lib/convert-tree-to-sync-options'; import { getSiteEnvironment } from 'src/modules/sync/lib/environment-utils'; -import { useAppDispatch, useI18nLocale } from 'src/stores'; +import { useAppDispatch, useI18nLocale, useRootSelector } from 'src/stores'; import { connectedSitesActions, + connectedSitesSelectors, useGetConnectedSitesForLocalSiteQuery, } from 'src/stores/sync/connected-sites'; import type { SyncSite } from 'src/modules/sync/types'; @@ -194,6 +195,8 @@ const SyncConnectedSitesSectionItem = ( { }: SyncConnectedSitesListProps ) => { const { __ } = useI18n(); const isOffline = useOffline(); + const loadingSiteIds = useRootSelector( connectedSitesSelectors.selectLoadingSiteIds ); + const isSiteLoading = loadingSiteIds.includes( connectedSite.id ); const { clearPullState, getPullState, @@ -263,7 +266,7 @@ const SyncConnectedSitesSectionItem = ( { key={ connectedSite.id } >
- { connectedSite.isLoading ? ( + { isSiteLoading ? (
- { connectedSite.isLoading ? ( + { isSiteLoading ? (
; - if ( connectedSite.isLoading ) { - // Show skeleton loader while fetching full site data + if ( isSiteLoading ) { logo = (
{ logo } - { connectedSite.isLoading ? ( + { isSiteLoading ? (
void; onPull: ( syncData: TreeNode[] ) => void; onRequestClose: () => void; - isConnectionReady?: boolean; }; const useDynamicTreeState = ( @@ -142,7 +141,6 @@ export function SyncDialog( { onPush, onPull, onRequestClose, - isConnectionReady = true, }: SyncDialogProps ) { const locale = useI18nLocale(); const { __ } = useI18n(); @@ -481,12 +479,11 @@ export function SyncDialog( { isSubmitDisabled || isLoadingRewindId || isPushSelectionOverLimit || - isSizeCheckLoading || - ! isConnectionReady + isSizeCheckLoading } data-testid={ `sync-dialog-${ type }-button` } > - { ! isConnectionReady ? __( 'Connecting...' ) : syncTexts.submit } + { syncTexts.submit }
diff --git a/apps/studio/src/modules/sync/components/sync-sites-modal-selector.tsx b/apps/studio/src/modules/sync/components/sync-sites-modal-selector.tsx index cfc69c7a1f..4fe2172434 100644 --- a/apps/studio/src/modules/sync/components/sync-sites-modal-selector.tsx +++ b/apps/studio/src/modules/sync/components/sync-sites-modal-selector.tsx @@ -18,8 +18,11 @@ import { CreateButton } from 'src/modules/sync/components/create-button'; import { EnvironmentBadge } from 'src/modules/sync/components/environment-badge'; import { NoWpcomSitesModal } from 'src/modules/sync/components/no-wpcom-sites-modal'; import { getSiteEnvironment } from 'src/modules/sync/lib/environment-utils'; -import { useI18nLocale } from 'src/stores'; -import { useGetConnectedSitesForLocalSiteQuery } from 'src/stores/sync/connected-sites'; +import { useI18nLocale, useRootSelector } from 'src/stores'; +import { + connectedSitesSelectors, + useGetConnectedSitesForLocalSiteQuery, +} from 'src/stores/sync/connected-sites'; import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites'; import type { SyncSite, SyncModalMode } from 'src/modules/sync/types'; @@ -248,6 +251,8 @@ function SiteItem( { onClick: () => void; } ) { const { __ } = useI18n(); + const loadingSiteIds = useRootSelector( connectedSitesSelectors.selectLoadingSiteIds ); + const isSiteLoading = loadingSiteIds.includes( site.id ); const isAlreadyConnected = site.syncSupport === 'already-connected'; const isSyncable = site.syncSupport === 'syncable'; const isNeedsTransfer = site.syncSupport === 'needs-transfer'; @@ -285,7 +290,7 @@ function SiteItem( { } } >
- { site.isLoading ? ( + { isSiteLoading ? (
) } - { site.isLoading ? ( + { isSiteLoading ? (
{ - 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 ) { @@ -171,46 +160,21 @@ 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 ) { @@ -235,7 +199,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } { connectedSites.length > 0 ? (
disconnectSite( { siteId: id, localSiteId: selectedSite.id } ) @@ -281,7 +245,6 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } type={ reduxModalMode } localSite={ selectedSite } remoteSite={ selectedRemoteSite } - isConnectionReady={ isConnectionReady } onPush={ async ( tree ) => { await handleConnect( selectedRemoteSite ); const pushOptions = convertTreeToPushOptions( tree ); diff --git a/apps/studio/src/modules/sync/tests/index.test.tsx b/apps/studio/src/modules/sync/tests/index.test.tsx index d582e403d0..9d365c2286 100644 --- a/apps/studio/src/modules/sync/tests/index.test.tsx +++ b/apps/studio/src/modules/sync/tests/index.test.tsx @@ -118,7 +118,6 @@ const fakeSyncSite: SyncSite = { localSiteId: 'site-id', lastPullTimestamp: null, lastPushTimestamp: null, - isLoading: false, }; describe( 'ContentTabSync', () => { @@ -307,7 +306,6 @@ describe( 'ContentTabSync', () => { isPressable: false, lastPullTimestamp: null, lastPushTimestamp: null, - isLoading: false, }; vi.mocked( useAuth, { partial: true } ).mockReturnValue( createAuthMock( true ) ); setupConnectedSitesMocks( [ fakeSyncSite ], [ fakeSyncSite ] ); diff --git a/apps/studio/src/modules/sync/types.ts b/apps/studio/src/modules/sync/types.ts index a580b73704..f2ea55a873 100644 --- a/apps/studio/src/modules/sync/types.ts +++ b/apps/studio/src/modules/sync/types.ts @@ -27,5 +27,4 @@ export type SyncSite = { syncSupport: SyncSupport; lastPullTimestamp: string | null; lastPushTimestamp: string | null; - isLoading?: boolean; // True when site data is incomplete (e.g., from deep link before full fetch) }; diff --git a/apps/studio/src/stores/sync/connected-sites.ts b/apps/studio/src/stores/sync/connected-sites.ts index 6b2796eba1..038e6820c3 100644 --- a/apps/studio/src/stores/sync/connected-sites.ts +++ b/apps/studio/src/stores/sync/connected-sites.ts @@ -8,6 +8,7 @@ type ConnectedSitesState = { isModalOpen: boolean; modalMode: SyncModalMode | null; selectedRemoteSiteId: number | null; + loadingSiteIds: number[]; }; function getInitialState(): ConnectedSitesState { @@ -15,6 +16,7 @@ function getInitialState(): ConnectedSitesState { isModalOpen: false, modalMode: null, selectedRemoteSiteId: null, + loadingSiteIds: [], }; } @@ -41,6 +43,16 @@ const connectedSitesSlice = createSlice( { clearSelectedRemoteSiteId: ( state ) => { state.selectedRemoteSiteId = null; }, + + addLoadingSiteId: ( state, action: PayloadAction< number > ) => { + if ( ! state.loadingSiteIds.includes( action.payload ) ) { + state.loadingSiteIds.push( action.payload ); + } + }, + + removeLoadingSiteId: ( state, action: PayloadAction< number > ) => { + state.loadingSiteIds = state.loadingSiteIds.filter( ( id ) => id !== action.payload ); + }, }, } ); @@ -50,6 +62,7 @@ export const connectedSitesSelectors = { selectIsModalOpen: ( state: RootState ) => state.connectedSites.isModalOpen, selectModalMode: ( state: RootState ) => state.connectedSites.modalMode, selectSelectedRemoteSiteId: ( state: RootState ) => state.connectedSites.selectedRemoteSiteId, + selectLoadingSiteIds: ( state: RootState ) => state.connectedSites.loadingSiteIds, }; export const connectedSitesApi = createApi( { From d0c08a6e2e500575ecd65aa2f04c2c89b74ed0fc Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Feb 2026 12:50:08 +0100 Subject: [PATCH 07/12] Cleanup and remove unused query --- apps/studio/src/modules/sync/index.tsx | 11 ++++++----- apps/studio/src/stores/sync/wpcom-sites.ts | 8 ++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/studio/src/modules/sync/index.tsx b/apps/studio/src/modules/sync/index.tsx index 8a88c56824..7a890037d7 100644 --- a/apps/studio/src/modules/sync/index.tsx +++ b/apps/studio/src/modules/sync/index.tsx @@ -132,10 +132,11 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } connectedSitesSelectors.selectSelectedRemoteSiteId ); const { isAuthenticated, user } = useAuth(); - const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( { - localSiteId: selectedSite.id, - userId: user?.id, - } ); + const { data: connectedSites = [], isLoading: isLoadingConnectedSites } = + useGetConnectedSitesForLocalSiteQuery( { + localSiteId: selectedSite.id, + userId: user?.id, + } ); const [ connectSite ] = useConnectSiteMutation(); const [ disconnectSite ] = useDisconnectSiteMutation(); const { pushSite, pullSite } = useSyncSites(); @@ -214,7 +215,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
- ) : ( + ) : isLoadingConnectedSites ? null : (
Date: Tue, 24 Feb 2026 13:51:38 +0100 Subject: [PATCH 08/12] Remove siteName and siteUrl --- .../hooks/sync-sites/use-listen-deep-link-connection.ts | 8 ++------ apps/studio/src/ipc-utils.ts | 2 -- .../studio/src/lib/deeplink/handlers/sync-connect-site.ts | 4 ---- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index eedc8a0581..6cd73cbcf9 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -37,22 +37,18 @@ export function useListenDeepLinkConnection() { remoteSiteId, studioSiteId, autoOpenPush, - siteName, - siteUrl, }: { remoteSiteId: number; studioSiteId: string; autoOpenPush?: boolean; - siteName?: string; - siteUrl?: string; } ) => { // Create minimal site object optimistically to connect immediately const minimalSite: SyncSite = { id: remoteSiteId, localSiteId: studioSiteId, - name: siteName ?? '', - url: siteUrl ?? '', + name: '', + url: '', isStaging: false, isPressable: false, environmentType: null, diff --git a/apps/studio/src/ipc-utils.ts b/apps/studio/src/ipc-utils.ts index f46a1c4412..3b30b915e9 100644 --- a/apps/studio/src/ipc-utils.ts +++ b/apps/studio/src/ipc-utils.ts @@ -48,8 +48,6 @@ export interface IpcEvents { remoteSiteId: number; studioSiteId: string; autoOpenPush?: boolean; - siteName?: string; - siteUrl?: string; }, ]; 'test-render-failure': [ void ]; diff --git a/apps/studio/src/lib/deeplink/handlers/sync-connect-site.ts b/apps/studio/src/lib/deeplink/handlers/sync-connect-site.ts index fd010277e1..0bf504786b 100644 --- a/apps/studio/src/lib/deeplink/handlers/sync-connect-site.ts +++ b/apps/studio/src/lib/deeplink/handlers/sync-connect-site.ts @@ -10,16 +10,12 @@ 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, } ); } } From 3b1f58db34e9c246bb6a3e15d9434c1ef095f778 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 24 Feb 2026 13:54:10 +0100 Subject: [PATCH 09/12] Reuse and extract fields --- apps/studio/src/stores/sync/wpcom-sites.ts | 49 ++++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/apps/studio/src/stores/sync/wpcom-sites.ts b/apps/studio/src/stores/sync/wpcom-sites.ts index 6c04a9cf76..9d1f634b56 100644 --- a/apps/studio/src/stores/sync/wpcom-sites.ts +++ b/apps/studio/src/stores/sync/wpcom-sites.ts @@ -117,6 +117,21 @@ function transformSitesResponse( sites: unknown[], connectedSiteIds?: number[] ) } ); } +const SITE_FIELDS = [ + 'name', + 'ID', + 'URL', + 'plan', + 'capabilities', + 'is_wpcom_atomic', + 'options', + 'jetpack', + 'is_deleted', + 'is_a8c', + 'hosting_provider_guess', + 'environment_type', +].join( ',' ); + export const wpcomSitesApi = createApi( { reducerPath: 'wpcomSitesApi', baseQuery: fetchBaseQuery(), @@ -130,28 +145,13 @@ export const wpcomSitesApi = createApi( { } try { - const fields = [ - 'name', - 'ID', - 'URL', - 'plan', - 'capabilities', - 'is_wpcom_atomic', - 'options', - 'jetpack', - 'is_deleted', - 'is_a8c', - 'hosting_provider_guess', - 'environment_type', - ].join( ',' ); - const response = await wpcomClient.req.get( { apiNamespace: 'rest/v1.1', path: `/sites/${ siteId }`, }, { - fields, + fields: SITE_FIELDS, options: 'created_at,wpcom_staging_blog_ids', } ); @@ -199,28 +199,13 @@ export const wpcomSitesApi = createApi( { try { const allConnectedSites = await getIpcApi().getConnectedWpcomSites(); - const fields = [ - 'name', - 'ID', - 'URL', - 'plan', - 'capabilities', - 'is_wpcom_atomic', - 'options', - 'jetpack', - 'is_deleted', - 'is_a8c', - 'hosting_provider_guess', - 'environment_type', - ].join( ',' ); - const response = await wpcomClient.req.get( { apiNamespace: 'rest/v1.2', path: `/me/sites`, }, { - fields, + fields: SITE_FIELDS, filter: 'atomic,wpcom', options: 'created_at,wpcom_staging_blog_ids', site_activity: 'active', From 7bce366d3cb8629701b862790627c30307f9b3bc Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 24 Feb 2026 13:56:55 +0100 Subject: [PATCH 10/12] Move unsubscribe call --- .../src/hooks/sync-sites/use-listen-deep-link-connection.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts index 6cd73cbcf9..38f31d6875 100644 --- a/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts +++ b/apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts @@ -102,15 +102,13 @@ export function useListenDeepLinkConnection() { dispatch( connectedSitesApi.util.invalidateTags( [ 'ConnectedSites' ] ) ); } - fetchSingleSitePromise.unsubscribe(); - // Refetch all sites to update syncSites (used by the push/pull modal). // Await so loading state persists until the modal can display full site data. await refetchWpComSites(); } catch ( error ) { console.error( 'Error during site connection:', error ); } finally { - // Always clear loading state, even on error + fetchSingleSitePromise.unsubscribe(); dispatch( connectedSitesActions.removeLoadingSiteId( remoteSiteId ) ); } } From 93a0ee7f1be5e797977f19db43c3ebdb9a3fea22 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 24 Feb 2026 14:41:09 +0100 Subject: [PATCH 11/12] Create Tailwind utility class for skeleton animation --- .../src/components/content-tab-overview.tsx | 8 +++----- apps/studio/src/index.css | 5 +++++ .../sync/components/sync-connected-sites.tsx | 19 ++++--------------- .../components/sync-sites-modal-selector.tsx | 12 +++--------- .../components/tree-view-loading-skeleton.tsx | 8 ++------ 5 files changed, 17 insertions(+), 35 deletions(-) diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 3a1c17ec31..6055a8931b 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -30,13 +30,11 @@ interface ContentTabOverviewProps { selectedSite: SiteDetails; } -const skeletonBg = 'animate-pulse bg-gradient-to-r from-[#F6F7F7] via-[#DCDCDE] to-[#F6F7F7]'; - const ButtonSectionSkeleton = ( { title }: { title: string } ) => { return (

{ title }

-
+
); }; @@ -222,7 +220,7 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )
- { loading &&
} + { loading &&
} { ! loading && ! isThumbnailError &&

{ themeDetails?.name }

}
diff --git a/apps/studio/src/index.css b/apps/studio/src/index.css index b4d5e8ca62..2ae0ef7a88 100644 --- a/apps/studio/src/index.css +++ b/apps/studio/src/index.css @@ -7,6 +7,11 @@ .interpolate-size-allow-keywords { interpolate-size: allow-keywords; } + + .skeleton-bg { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + background-image: linear-gradient(to right, #f6f7f7, #dcdcde, #f6f7f7); + } } body { diff --git a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx index 4a1614335e..71541c27b1 100644 --- a/apps/studio/src/modules/sync/components/sync-connected-sites.tsx +++ b/apps/studio/src/modules/sync/components/sync-connected-sites.tsx @@ -268,7 +268,7 @@ const SyncConnectedSitesSectionItem = ( {
{ isSiteLoading ? (
) : ( @@ -277,10 +277,7 @@ const SyncConnectedSitesSectionItem = ( {
{ isSiteLoading ? ( -
+
) : (