Skip to content

Commit 11271d6

Browse files
authored
Implement new site launch and import buttons (#2002)
* Implement new site launch and import buttons * Fix linting errors * Use connect button when there is a site connected * Simplify state, update button text and fix tests * Update wording, reuse ConnectButton component * Add a new type and simplify code * Fix types * Simplify logic * Remove superfluous comment and simplify * Use strict equality operator * Use local variable to avoid non-null assertion * Simplify logic for selecting sites, fix typo * Move SyncModalMode piece of state to Redux * Apply code review suggestion, use Next for Sync dialog button * Do not connect if cancelling the Push or Pull dialog * Connect site before calling push/pull * Add border and text color from design to secondary button * Disable buttons when there is another site syncing
1 parent e879c4e commit 11271d6

File tree

6 files changed

+194
-57
lines changed

6 files changed

+194
-57
lines changed

src/modules/sync/components/connect-button.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@ import Button, { ButtonVariant } from 'src/components/button';
33
import offlineIcon from 'src/components/offline-icon';
44
import { Tooltip } from 'src/components/tooltip';
55
import { useOffline } from 'src/hooks/use-offline';
6-
import { cx } from 'src/lib/cx';
76

87
interface ConnectButtonProps {
98
variant: ButtonVariant;
109
connectSite?: () => void;
11-
disableConnectButtonStyle?: boolean;
10+
disabled?: boolean;
1211
className?: string;
1312
children?: React.ReactNode;
13+
tooltipText?: string;
1414
}
1515

1616
export const ConnectButton = ( {
1717
variant,
1818
connectSite,
19-
disableConnectButtonStyle,
19+
disabled,
2020
className,
2121
children,
22+
tooltipText,
2223
}: ConnectButtonProps ) => {
2324
const isOffline = useOffline();
25+
const tooltipContent = tooltipText ?? __( 'Connecting a site requires an internet connection.' );
26+
const isDisabled = disabled || isOffline;
2427
return (
2528
<Tooltip
26-
disabled={ ! isOffline }
27-
text={ __( 'Connecting a site requires an internet connection.' ) }
29+
disabled={ ! isDisabled }
30+
text={ tooltipContent }
2831
icon={ offlineIcon }
2932
placement="top-start"
3033
>
3134
<Button
3235
onClick={ connectSite }
33-
disabled={ isOffline }
34-
aria-disabled={ isOffline }
36+
disabled={ isDisabled }
37+
aria-disabled={ isDisabled }
3538
variant={ variant }
36-
className={ cx(
37-
! disableConnectButtonStyle && ! isOffline && '!text-a8c-blue-50 !shadow-a8c-blue-50',
38-
className
39-
) }
39+
className={ className }
4040
>
4141
{ children }
4242
</Button>

src/modules/sync/components/sync-sites-modal-selector.tsx

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { EnvironmentBadge } from 'src/modules/sync/components/environment-badge'
1717
import { getSiteEnvironment } from 'src/modules/sync/lib/environment-utils';
1818
import { useI18nLocale } from 'src/stores';
1919
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
20+
import type { SyncModalMode } from 'src/modules/sync/types';
2021

2122
const SearchControl = process.env.NODE_ENV === 'test' ? () => null : SearchControlWp;
2223

@@ -32,13 +33,15 @@ export function SyncSitesModalSelector( {
3233
syncSites,
3334
onInitialRender,
3435
selectedSite,
36+
mode = 'connect',
3537
}: {
3638
isLoading?: boolean;
3739
onRequestClose: () => void;
3840
syncSites: SyncSite[];
3941
onConnect: ( siteId: number ) => void;
4042
onInitialRender?: () => void;
4143
selectedSite: SiteDetails;
44+
mode?: SyncModalMode;
4245
} ) {
4346
const { __ } = useI18n();
4447
const [ selectedSiteId, setSelectedSiteId ] = useState< number | null >( null );
@@ -53,6 +56,18 @@ export function SyncSitesModalSelector( {
5356
} );
5457
const isEmpty = filteredSites.length === 0;
5558

59+
const getModalTitle = () => {
60+
switch ( mode ) {
61+
case 'push':
62+
return __( 'Publish your site' );
63+
case 'pull':
64+
return __( 'Select a site to import' );
65+
case 'connect':
66+
default:
67+
return __( 'Connect your site' );
68+
}
69+
};
70+
5671
useEffect( () => {
5772
if ( onInitialRender ) {
5873
onInitialRender();
@@ -63,7 +78,7 @@ export function SyncSitesModalSelector( {
6378
<Modal
6479
className="w-3/5 min-w-[550px] h-full max-h-[84vh] [&>div]:!p-0"
6580
onRequestClose={ onRequestClose }
66-
title={ __( 'Connect your site' ) }
81+
title={ getModalTitle() }
6782
>
6883
<div className="relative" data-testid="sync-sites-modal-selector">
6984
<SearchSites searchQuery={ searchQuery } setSearchQuery={ setSearchQuery } />
@@ -100,11 +115,12 @@ export function SyncSitesModalSelector( {
100115
} }
101116
disabled={ ! selectedSiteId }
102117
selectedSite={ selectedSite }
118+
mode={ mode }
103119
/>
104120

105121
{ isOffline && (
106122
<div className="absolute inset-0 bg-white/80 z-10 flex items-center justify-center">
107-
<SyncSitesOfflineView />
123+
<SyncSitesOfflineView mode={ mode } />
108124
</div>
109125
) }
110126
</div>
@@ -335,14 +351,27 @@ function Footer( {
335351
onConnect,
336352
disabled,
337353
selectedSite,
354+
mode = 'connect',
338355
}: {
339356
onRequestClose: () => void;
340357
onConnect: () => void;
341358
disabled: boolean;
342359
selectedSite: SiteDetails;
360+
mode?: SyncModalMode;
343361
} ) {
344362
const { __ } = useI18n();
345363

364+
const getButtonText = () => {
365+
switch ( mode ) {
366+
case 'push':
367+
case 'pull':
368+
return __( 'Next' );
369+
case 'connect':
370+
default:
371+
return __( 'Connect' );
372+
}
373+
};
374+
346375
useEffect( () => {
347376
if ( ! disabled ) {
348377
focusConnectButton();
@@ -361,20 +390,31 @@ function Footer( {
361390
{ __( 'Cancel' ) }
362391
</Button>
363392
<Button id="connect-button" variant="primary" disabled={ disabled } onClick={ onConnect }>
364-
{ __( 'Connect' ) }
393+
{ getButtonText() }
365394
</Button>
366395
</div>
367396
</div>
368397
);
369398
}
370399

371-
const SyncSitesOfflineView = () => {
372-
const offlineMessage = __( 'Connecting a site requires an internet connection.' );
400+
const SyncSitesOfflineView = ( { mode = 'connect' }: { mode?: SyncModalMode } ) => {
401+
const { __ } = useI18n();
402+
const getOfflineMessage = () => {
403+
switch ( mode ) {
404+
case 'push':
405+
return __( 'Publishing your site requires an internet connection.' );
406+
case 'pull':
407+
return __( 'Importing a remote site requires an internet connection.' );
408+
case 'connect':
409+
default:
410+
return __( 'Connecting a site requires an internet connection.' );
411+
}
412+
};
373413

374414
return (
375415
<div className="flex items-center justify-center h-12 px-2 pt-4 text-a8c-gray-70 gap-1">
376416
<Icon className="m-1 fill-a8c-gray-70" size={ 24 } icon={ offlineIcon } />
377-
<span className="text-[13px] leading-[16px]">{ offlineMessage }</span>
417+
<span className="text-[13px] leading-[16px]">{ getOfflineMessage() }</span>
378418
</div>
379419
);
380420
};

src/modules/sync/index.tsx

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import { check, Icon } from '@wordpress/icons';
22
import { useI18n } from '@wordpress/react-i18n';
3-
import { PropsWithChildren, useEffect } from 'react';
3+
import { PropsWithChildren, useEffect, useState } from 'react';
44
import { ArrowIcon } from 'src/components/arrow-icon';
55
import Button from 'src/components/button';
66
import offlineIcon from 'src/components/offline-icon';
77
import { Tooltip } from 'src/components/tooltip';
8+
import { useSyncSites } from 'src/hooks/sync-sites';
89
import { useAuth } from 'src/hooks/use-auth';
910
import { useOffline } from 'src/hooks/use-offline';
1011
import { getIpcApi } from 'src/lib/get-ipc-api';
1112
import { ConnectButton } from 'src/modules/sync/components/connect-button';
1213
import { SyncConnectedSites } from 'src/modules/sync/components/sync-connected-sites';
14+
import { SyncDialog } from 'src/modules/sync/components/sync-dialog';
1315
import { SyncSitesModalSelector } from 'src/modules/sync/components/sync-sites-modal-selector';
1416
import { SyncTabImage } from 'src/modules/sync/components/sync-tab-image';
17+
import {
18+
convertTreeToPullOptions,
19+
convertTreeToPushOptions,
20+
} from 'src/modules/sync/lib/convert-tree-to-sync-options';
1521
import { useAppDispatch, useRootSelector } from 'src/stores';
1622
import {
1723
useConnectedSitesData,
@@ -21,6 +27,7 @@ import {
2127
connectedSitesActions,
2228
} from 'src/stores/sync';
2329
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
30+
import type { SyncModalMode } from 'src/modules/sync/types';
2431

2532
function SiteSyncDescription( { children }: PropsWithChildren ) {
2633
const { __ } = useI18n();
@@ -34,13 +41,13 @@ function SiteSyncDescription( { children }: PropsWithChildren ) {
3441
</div>
3542
<div className="max-w-[40ch] text-a8c-gray-70 a8c-body">
3643
{ __(
37-
'Connect your existing WordPress.com or Pressable sites with Jetpack activated, or create a new one. Then share your work with the world.'
44+
'Launch your existing WordPress.com or Jetpack-activated Pressable sites, or import an existing one. Then, share your work with the world.'
3845
) }
3946
</div>
4047
<div className="mt-6">
4148
{ [
4249
__( 'Push and pull changes from your live site.' ),
43-
__( 'Connect multiple environments.' ),
50+
__( 'Supports staging and production sites.' ),
4451
__( 'Sync database and file changes.' ),
4552
].map( ( text ) => (
4653
<div key={ text } className="text-a8c-gray-70 a8c-body flex items-center">
@@ -120,9 +127,14 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
120127
const { __ } = useI18n();
121128
const dispatch = useAppDispatch();
122129
const isModalOpen = useRootSelector( connectedSitesSelectors.selectIsModalOpen );
130+
const reduxModalMode = useRootSelector( connectedSitesSelectors.selectModalMode );
123131
const { connectedSites } = useConnectedSitesData();
124132
const { syncSites, isFetching, refetchSites } = useSyncSitesData();
125133
const { connectSite, disconnectSite } = useConnectedSitesOperations();
134+
const { pushSite, pullSite, isAnySitePulling, isAnySitePushing } = useSyncSites();
135+
const isAnySiteSyncing = isAnySitePulling || isAnySitePushing;
136+
137+
const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null );
126138

127139
const { isAuthenticated } = useAuth();
128140

@@ -147,6 +159,41 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
147159
}
148160
};
149161

162+
const handleLaunchSite = () => {
163+
dispatch( connectedSitesActions.openModal( 'push' ) );
164+
};
165+
166+
const handleImportSite = () => {
167+
dispatch( connectedSitesActions.openModal( 'pull' ) );
168+
};
169+
170+
const handleSiteSelection = async ( siteId: number, mode: SyncModalMode | null ) => {
171+
const disconnectSiteId =
172+
typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined;
173+
174+
if ( disconnectSiteId ) {
175+
await disconnectSite( disconnectSiteId );
176+
}
177+
178+
const selectedSiteFromList = syncSites.find( ( site ) => site.id === siteId );
179+
if ( ! selectedSiteFromList ) {
180+
getIpcApi().showErrorMessageBox( {
181+
title: __( 'Failed to select site' ),
182+
message: __( 'Please try again.' ),
183+
} );
184+
return;
185+
}
186+
187+
if ( mode === 'push' || mode === 'pull' ) {
188+
dispatch( connectedSitesActions.setModalMode( mode ) );
189+
setSelectedRemoteSite( selectedSiteFromList );
190+
} else {
191+
await handleConnect( selectedSiteFromList );
192+
dispatch( connectedSitesActions.setModalMode( null ) );
193+
dispatch( connectedSitesActions.closeModal() );
194+
}
195+
};
196+
150197
return (
151198
<div className="flex flex-col h-full overflow-y-auto">
152199
{ connectedSites.length > 0 ? (
@@ -159,54 +206,79 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
159206
<div className="sticky bottom-0 bg-white/[0.8] backdrop-blur-sm w-full px-8 py-6 mt-auto">
160207
<ConnectButton
161208
variant="primary"
162-
connectSite={ () => dispatch( connectedSitesActions.openModal() ) }
163-
disableConnectButtonStyle={ true }
209+
connectSite={ () => dispatch( connectedSitesActions.openModal( 'connect' ) ) }
164210
>
165211
{ __( 'Connect another site' ) }
166212
</ConnectButton>
167213
</div>
168214
</div>
169215
) : (
170216
<SiteSyncDescription>
171-
<div className="mt-8">
217+
<div className="mt-8 flex flex-wrap gap-4">
172218
<ConnectButton
173219
variant="primary"
174-
connectSite={ () => dispatch( connectedSitesActions.openModal() ) }
175-
disableConnectButtonStyle={ true }
220+
connectSite={ handleLaunchSite }
221+
disabled={ isAnySiteSyncing }
222+
tooltipText={
223+
isAnySiteSyncing
224+
? __(
225+
'Another site is syncing. Please wait for the sync to finish before you publish your site.'
226+
)
227+
: __( 'Publishing your site requires an internet connection.' )
228+
}
229+
>
230+
{ __( 'Publish site' ) }
231+
</ConnectButton>
232+
<ConnectButton
233+
variant="secondary"
234+
connectSite={ handleImportSite }
235+
className={ isAnySiteSyncing ? '' : '!text-a8c-blue-50 !shadow-a8c-blue-50' }
236+
disabled={ isAnySiteSyncing }
237+
tooltipText={ __( 'Importing a remote site requires an internet connection.' ) }
176238
>
177-
{ __( 'Connect site' ) }
239+
{ __( 'Pull site' ) }
178240
</ConnectButton>
179241
</div>
180242
</SiteSyncDescription>
181243
) }
182244

183245
{ isModalOpen && (
184246
<SyncSitesModalSelector
247+
mode={ reduxModalMode || 'connect' }
185248
isLoading={ isFetching }
186-
onRequestClose={ () => dispatch( connectedSitesActions.closeModal() ) }
249+
onRequestClose={ () => {
250+
dispatch( connectedSitesActions.closeModal() );
251+
} }
187252
syncSites={ syncSites }
188253
onInitialRender={ refetchSites }
189-
onConnect={ async ( siteId ) => {
190-
const disconnectSiteId =
191-
typeof isModalOpen === 'object' ? isModalOpen.disconnectSiteId : undefined;
192-
193-
if ( disconnectSiteId ) {
194-
await disconnectSite( disconnectSiteId );
195-
}
196-
197-
const newConnectedSite = syncSites.find( ( site ) => site.id === siteId );
198-
if ( ! newConnectedSite ) {
199-
getIpcApi().showErrorMessageBox( {
200-
title: __( 'Failed to connect to site' ),
201-
message: __( 'Please try again.' ),
202-
} );
203-
return;
204-
}
205-
void handleConnect( newConnectedSite );
254+
onConnect={ async ( siteId: number ) => {
255+
await handleSiteSelection( siteId, reduxModalMode );
206256
} }
207257
selectedSite={ selectedSite }
208258
/>
209259
) }
260+
261+
{ reduxModalMode && reduxModalMode !== 'connect' && selectedRemoteSite && (
262+
<SyncDialog
263+
type={ reduxModalMode }
264+
localSite={ selectedSite }
265+
remoteSite={ selectedRemoteSite }
266+
onPush={ async ( tree ) => {
267+
await handleConnect( selectedRemoteSite );
268+
const pushOptions = convertTreeToPushOptions( tree );
269+
void pushSite( selectedRemoteSite, selectedSite, pushOptions );
270+
} }
271+
onPull={ async ( tree ) => {
272+
await handleConnect( selectedRemoteSite );
273+
const pullOptions = convertTreeToPullOptions( tree );
274+
void pullSite( selectedRemoteSite, selectedSite, pullOptions );
275+
} }
276+
onRequestClose={ () => {
277+
setSelectedRemoteSite( null );
278+
dispatch( connectedSitesActions.setModalMode( null ) );
279+
} }
280+
/>
281+
) }
210282
</div>
211283
);
212284
}

0 commit comments

Comments
 (0)