Skip to content
Merged
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
8 changes: 3 additions & 5 deletions apps/studio/src/components/content-tab-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="w-full max-w-96">
<h2 className="a8c-subtitle-small mb-3">{ title }</h2>
<div className={ `w-full h-20 my-1 ${ skeletonBg }` }></div>
<div className="w-full h-20 my-1 skeleton-bg"></div>
</div>
);
};
Expand Down Expand Up @@ -222,7 +220,7 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )
<div
className={ cx(
'w-full min-h-40 max-h-64 rounded-sm border border-a8c-gray-5 bg-a8c-gray-0 mb-2 flex justify-center',
loading && `h-64 ${ skeletonBg }`,
loading && 'h-64 skeleton-bg',
isThumbnailError && 'border-none',
! loading && 'hover:border-a8c-blue-50 duration-300'
) }
Expand Down Expand Up @@ -259,7 +257,7 @@ export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps )
) }
</div>
<div className="flex justify-between items-center w-full">
{ loading && <div className={ `w-[100px] min-h-4 ${ skeletonBg }` }></div> }
{ loading && <div className="w-[100px] min-h-4 skeleton-bg"></div> }
{ ! loading && ! isThumbnailError && <p>{ themeDetails?.name }</p> }
</div>
</div>
Expand Down
106 changes: 87 additions & 19 deletions apps/studio/src/hooks/sync-sites/use-listen-deep-link-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +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,
connectedSitesApi,
useConnectSiteMutation,
useGetConnectedSitesForLocalSiteQuery,
} from 'src/stores/sync/connected-sites';
import { useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites';
import { wpcomSitesApi, useGetWpComSitesQuery } from 'src/stores/sync/wpcom-sites';

export function useListenDeepLinkConnection() {
const dispatch = useAppDispatch();
Expand All @@ -28,26 +31,91 @@ 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,
}: {
remoteSiteId: number;
studioSiteId: string;
autoOpenPush?: boolean;
}
) => {
// Create minimal site object optimistically to connect immediately
const minimalSite: SyncSite = {
id: remoteSiteId,
localSiteId: studioSiteId,
name: '',
url: '',
isStaging: false,
isPressable: false,
environmentType: null,
syncSupport: 'already-connected',
lastPullTimestamp: null,
lastPushTimestamp: null,
};

// 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' );
}

// 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)
if ( autoOpenPush ) {
dispatch(
connectedSitesActions.setSelectedRemoteSiteId( {
remoteSiteId,
localSiteId: studioSiteId,
} )
);
}

const fetchSingleSitePromise = dispatch(
wpcomSitesApi.endpoints.getSingleWpComSite.initiate( {
siteId: remoteSiteId,
userId: user?.id,
} )
);

// Wait for both operations to complete
try {
const [ , singleSiteResult ] = await Promise.all( [
connectPromise,
fetchSingleSitePromise,
] );

if ( singleSiteResult.data ) {
const fullSiteData: SyncSite = {
...singleSiteResult.data,
localSiteId: studioSiteId,
syncSupport: 'already-connected',
};
await getIpcApi().updateSingleConnectedWpcomSite( fullSiteData );
dispatch( connectedSitesApi.util.invalidateTags( [ 'ConnectedSites' ] ) );
}
} catch ( error ) {
console.error( 'Error during site connection:', error );
} finally {
fetchSingleSitePromise.unsubscribe();
dispatch( connectedSitesActions.removeLoadingSiteId( remoteSiteId ) );
}

// Refetch all sites to update syncSites (used by the push/pull modal).
// Fired after clearing the loading state to avoid stale closure issues
// where the captured refetch references an outdated query subscription.
void refetchWpComSites();
}
);
}
5 changes: 5 additions & 0 deletions apps/studio/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion apps/studio/src/ipc-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ 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;
},
];
'test-render-failure': [ void ];
'theme-details-loading': [ { id: string } ];
'theme-details-loaded': [ { id: string; details: StartedSiteDetails[ 'themeDetails' ] } ];
Expand Down
54 changes: 39 additions & 15 deletions apps/studio/src/modules/sync/components/sync-connected-sites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from 'src/stores/sync';
import {
connectedSitesActions,
connectedSitesSelectors,
useGetConnectedSitesForLocalSiteQuery,
} from 'src/stores/sync/connected-sites';
import type { SyncSite } from 'src/modules/sync/types';
Expand Down Expand Up @@ -214,6 +215,9 @@ const SyncConnectedSitesSectionItem = ( {
const { __ } = useI18n();
const dispatch = useAppDispatch();
const isOffline = useOffline();
const isSiteLoading = useRootSelector(
connectedSitesSelectors.selectIsLoadingSiteId( connectedSite.id )
);
const getLastSyncTimeText = useLastSyncTimeText();
const { importState, clearImportState } = useImportExport();
const { getPushUploadPercentage, getPushUploadMessage } = useSyncStatesProgressInfo();
Expand Down Expand Up @@ -297,19 +301,30 @@ const SyncConnectedSitesSectionItem = ( {
key={ connectedSite.id }
>
<div className="shrink-0">
<EnvironmentBadge type={ getSiteEnvironment( connectedSite ) } />
{ isSiteLoading ? (
<div
className="h-5 w-20 rounded skeleton-bg"
aria-label={ __( 'Loading environment' ) }
/>
) : (
<EnvironmentBadge type={ getSiteEnvironment( connectedSite ) } />
) }
</div>

<Button
variant="link"
className="!text-a8c-gray-70 hover:!text-a8c-blue-50 max-w-full overflow-hidden"
onClick={ () => {
getIpcApi().openURL( connectedSite.url );
} }
>
<span className="truncate">{ connectedSite.url.replace( /^https?:\/\//, '' ) }</span>{ ' ' }
<ArrowIcon />
</Button>
{ isSiteLoading ? (
<div className="h-5 w-48 rounded skeleton-bg" aria-label={ __( 'Loading site URL' ) } />
) : (
<Button
variant="link"
className="!text-a8c-gray-70 hover:!text-a8c-blue-50 max-w-full overflow-hidden"
onClick={ () => {
getIpcApi().openURL( connectedSite.url );
} }
>
<span className="truncate">{ connectedSite.url.replace( /^https?:\/\//, '' ) }</span>{ ' ' }
<ArrowIcon />
</Button>
) }

<div className="flex shrink-0 justify-self-end justify-end items-center min-h-[26px] w-80">
{ isPulling && (
Expand Down Expand Up @@ -643,6 +658,9 @@ const SyncConnectedSiteSection = ( {
}
};

const isSiteLoading = useRootSelector(
connectedSitesSelectors.selectIsLoadingSiteId( connectedSite.id )
);
const hasConnectionErrors = connectedSite?.syncSupport !== 'already-connected';
const isPulling = useRootSelector(
syncOperationsSelectors.selectIsSiteIdPulling( selectedSite.id, connectedSite.id )
Expand All @@ -652,7 +670,9 @@ const SyncConnectedSiteSection = ( {
);

let logo = <WordPressLogoCircle />;
if ( hasConnectionErrors ) {
if ( isSiteLoading ) {
logo = <div className="w-5 h-5 rounded-full skeleton-bg" aria-label={ __( 'Loading' ) } />;
} else if ( hasConnectionErrors ) {
logo = <CircleRedCrossIcon />;
} else if ( connectedSite.isPressable ) {
logo = <PressableLogo />;
Expand All @@ -662,9 +682,13 @@ const SyncConnectedSiteSection = ( {
<div key={ connectedSite.id } className="flex flex-col gap-2 border-b border-a8c-gray-0 py-5">
<div className="flex items-center gap-2 ps-8 pe-5">
{ logo }
<div className={ cx( 'a8c-label-semibold', hasConnectionErrors && 'error-message' ) }>
{ connectedSite.name }
</div>
{ isSiteLoading ? (
<div className="h-5 w-40 rounded skeleton-bg" aria-label={ __( 'Loading site name' ) } />
) : (
<div className={ cx( 'a8c-label-semibold', hasConnectionErrors && 'error-message' ) }>
{ connectedSite.name }
</div>
) }
<div className="ms-auto">
<Tooltip
text={ __(
Expand Down
Loading