Skip to content

Commit 10a6f59

Browse files
authored
Add flow to create a new site from remote site (#1997)
* Add a new flow to create a new site and start pulling immediately.
1 parent 00bcd17 commit 10a6f59

File tree

9 files changed

+414
-20
lines changed

9 files changed

+414
-20
lines changed

src/hooks/tests/use-add-site.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,30 @@ import { configureStore } from '@reduxjs/toolkit';
33
import { renderHook, act } from '@testing-library/react';
44
import nock from 'nock';
55
import { Provider } from 'react-redux';
6+
import { useSyncSites } from 'src/hooks/sync-sites';
67
import { useAddSite } from 'src/hooks/use-add-site';
8+
import { useContentTabs } from 'src/hooks/use-content-tabs';
79
import { useSiteDetails } from 'src/hooks/use-site-details';
810
import { getWordPressProvider } from 'src/lib/wordpress-provider';
911
import providerConstantsReducer from 'src/stores/provider-constants-slice';
12+
import { useConnectedSitesOperations } from 'src/stores/sync';
13+
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
1014

1115
jest.mock( 'src/hooks/use-site-details' );
1216
jest.mock( 'src/hooks/use-feature-flags' );
17+
jest.mock( 'src/hooks/sync-sites' );
18+
jest.mock( 'src/hooks/use-content-tabs' );
1319
jest.mock( 'src/hooks/use-import-export', () => ( {
1420
useImportExport: () => ( {
1521
importFile: jest.fn(),
1622
clearImportState: jest.fn(),
1723
} ),
1824
} ) );
1925

26+
jest.mock( 'src/stores/sync', () => ( {
27+
useConnectedSitesOperations: jest.fn(),
28+
} ) );
29+
2030
jest.mock( 'src/lib/get-ipc-api', () => ( {
2131
getIpcApi: () => ( {
2232
generateProposedSitePath: jest.fn().mockResolvedValue( {
@@ -62,6 +72,9 @@ describe( 'useAddSite', () => {
6272
const mockCreateSite = jest.fn();
6373
const mockUpdateSite = jest.fn();
6474
const mockStartServer = jest.fn();
75+
const mockConnectSite = jest.fn();
76+
const mockPullSite = jest.fn();
77+
const mockSetSelectedTab = jest.fn();
6578

6679
beforeEach( () => {
6780
jest.clearAllMocks();
@@ -74,6 +87,38 @@ describe( 'useAddSite', () => {
7487
startServer: mockStartServer,
7588
} );
7689

90+
mockConnectSite.mockResolvedValue( undefined );
91+
92+
( useConnectedSitesOperations as jest.Mock ).mockReturnValue( {
93+
connectSite: mockConnectSite,
94+
} );
95+
96+
mockPullSite.mockReset();
97+
( useSyncSites as jest.Mock ).mockReturnValue( {
98+
pullSite: mockPullSite,
99+
syncSites: [],
100+
refetchSites: jest.fn(),
101+
isFetching: false,
102+
isAnySitePulling: false,
103+
isSiteIdPulling: jest.fn(),
104+
clearPullState: jest.fn(),
105+
cancelPull: jest.fn(),
106+
getPullState: jest.fn(),
107+
pushSite: jest.fn(),
108+
isAnySitePushing: false,
109+
isSiteIdPushing: jest.fn(),
110+
clearPushState: jest.fn(),
111+
getPushState: jest.fn(),
112+
getLastSyncTimeText: jest.fn(),
113+
} );
114+
115+
mockSetSelectedTab.mockReset();
116+
( useContentTabs as jest.Mock ).mockReturnValue( {
117+
selectedTab: 'overview',
118+
setSelectedTab: mockSetSelectedTab,
119+
tabs: [],
120+
} );
121+
77122
nock( 'https://api.wordpress.org' )
78123
.get( '/core/version-check/1.7/' )
79124
.query( { channel: 'beta', version: '5.9.9' } )
@@ -210,4 +255,51 @@ describe( 'useAddSite', () => {
210255
wpVersion,
211256
} );
212257
} );
258+
259+
it( 'should connect and start pulling when a remote site is selected', async () => {
260+
const remoteSite: SyncSite = {
261+
id: 123,
262+
localSiteId: 'remote-site-id',
263+
name: 'Remote Site',
264+
url: 'https://example.com',
265+
isStaging: false,
266+
isPressable: false,
267+
environmentType: null,
268+
syncSupport: 'syncable',
269+
lastPullTimestamp: null,
270+
lastPushTimestamp: null,
271+
};
272+
273+
const createdSite = {
274+
id: 'local-id',
275+
name: 'New Site',
276+
path: '/test/path',
277+
wpVersion: 'latest',
278+
phpVersion: '8.3',
279+
};
280+
281+
mockCreateSite.mockImplementation(
282+
( path, name, version, customDomain, enableHttps, blueprint, callback ) => {
283+
callback( createdSite );
284+
return Promise.resolve();
285+
}
286+
);
287+
288+
const { result } = renderHookWithProvider( () => useAddSite() );
289+
290+
act( () => {
291+
result.current.setSelectedRemoteSite( remoteSite );
292+
result.current.setSitePath( createdSite.path );
293+
} );
294+
295+
await act( async () => {
296+
await result.current.handleAddSiteClick();
297+
} );
298+
299+
expect( mockConnectSite ).toHaveBeenCalledWith( remoteSite, createdSite.id );
300+
expect( mockPullSite ).toHaveBeenCalledWith( remoteSite, createdSite, {
301+
optionsToSync: [ 'all' ],
302+
} );
303+
expect( mockSetSelectedTab ).toHaveBeenCalledWith( 'sync' );
304+
} );
213305
} );

src/hooks/use-add-site.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as Sentry from '@sentry/electron/renderer';
22
import { useI18n } from '@wordpress/react-i18n';
33
import { useCallback, useMemo, useState } from 'react';
4+
import { useSyncSites } from 'src/hooks/sync-sites';
5+
import { useContentTabs } from 'src/hooks/use-content-tabs';
46
import { useImportExport } from 'src/hooks/use-import-export';
57
import { useSiteDetails } from 'src/hooks/use-site-details';
68
import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'src/lib/domains';
@@ -11,12 +13,18 @@ import {
1113
selectDefaultPhpVersion,
1214
selectDefaultWordPressVersion,
1315
} from 'src/stores/provider-constants-slice';
16+
import { useConnectedSitesOperations } from 'src/stores/sync';
17+
import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types';
1418
import type { Blueprint } from 'src/stores/wpcom-api';
19+
import type { SyncOption } from 'src/types';
1520

1621
export function useAddSite() {
1722
const { __ } = useI18n();
1823
const { createSite, data: sites, loadingSites, startServer, updateSite } = useSiteDetails();
1924
const { importFile, clearImportState } = useImportExport();
25+
const { connectSite } = useConnectedSitesOperations();
26+
const { pullSite } = useSyncSites();
27+
const { setSelectedTab } = useContentTabs();
2028
const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion );
2129
const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion );
2230
const [ error, setError ] = useState( '' );
@@ -35,6 +43,7 @@ export function useAddSite() {
3543
const [ existingDomainNames, setExistingDomainNames ] = useState< string[] >( [] );
3644
const [ enableHttps, setEnableHttps ] = useState( false );
3745
const [ selectedBlueprint, setSelectedBlueprint ] = useState< Blueprint | undefined >();
46+
const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | undefined >();
3847

3948
const loadAllCustomDomains = useCallback( () => {
4049
getIpcApi()
@@ -143,12 +152,21 @@ export function useAddSite() {
143152
body: __( 'Your new site was imported' ),
144153
} );
145154
} else {
146-
await startServer( newSite.id );
155+
if ( selectedRemoteSite ) {
156+
await connectSite( selectedRemoteSite, newSite.id );
157+
const pullOptions: SyncOption[] = [ 'all' ];
158+
pullSite( selectedRemoteSite, newSite, {
159+
optionsToSync: pullOptions,
160+
} );
161+
setSelectedTab( 'sync' );
162+
} else {
163+
await startServer( newSite.id );
147164

148-
getIpcApi().showNotification( {
149-
title: newSite.name,
150-
body: __( 'Your new site was created' ),
151-
} );
165+
getIpcApi().showNotification( {
166+
title: newSite.name,
167+
body: __( 'Your new site was created' ),
168+
} );
169+
}
152170
}
153171
}
154172
);
@@ -172,6 +190,10 @@ export function useAddSite() {
172190
useCustomDomain,
173191
enableHttps,
174192
selectedBlueprint,
193+
selectedRemoteSite,
194+
pullSite,
195+
connectSite,
196+
setSelectedTab,
175197
] );
176198

177199
const handleSiteNameChange = useCallback(
@@ -243,6 +265,8 @@ export function useAddSite() {
243265
loadAllCustomDomains,
244266
selectedBlueprint,
245267
setSelectedBlueprint,
268+
selectedRemoteSite,
269+
setSelectedRemoteSite,
246270
};
247271
}, [
248272
doesPathContainWordPress,
@@ -268,5 +292,8 @@ export function useAddSite() {
268292
setEnableHttps,
269293
loadAllCustomDomains,
270294
selectedBlueprint,
295+
setSelectedBlueprint,
296+
selectedRemoteSite,
297+
setSelectedRemoteSite,
271298
] );
272299
}

src/modules/add-site/components/options.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import {
44
__experimentalHeading as Heading,
55
__experimentalText as Text,
66
} from '@wordpress/components';
7-
import { Icon, plus, backup, chevronRight, chevronLeft } from '@wordpress/icons';
7+
import { Icon, plus, backup, chevronRight, chevronLeft, download } from '@wordpress/icons';
88
import { useI18n } from '@wordpress/react-i18n';
99
import { Tooltip } from 'src/components/tooltip';
1010
import { useFeatureFlags } from 'src/hooks/use-feature-flags';
1111
import { useOffline } from 'src/hooks/use-offline';
1212
import { cx } from 'src/lib/cx';
1313
import { BlueprintIcon } from './blueprint-icon';
1414

15+
export type AddSiteFlowType = 'create' | 'blueprint' | 'backup' | 'pullRemote';
1516
interface AddSiteOptionsProps {
16-
onOptionSelect: ( option: 'create' | 'blueprint' | 'backup' ) => void;
17+
onOptionSelect: ( option: AddSiteFlowType ) => void;
1718
}
1819

1920
interface OptionButtonProps {
@@ -39,12 +40,12 @@ function OptionButton( {
3940
<Tooltip
4041
text={ disabledTooltip }
4142
disabled={ ! disabled }
42-
className={ cx( 'w-full max-w-[422px]' ) }
43+
className={ cx( 'w-full max-w-[460px]' ) }
4344
>
4445
<HStack
4546
as="button"
4647
className={ cx(
47-
'w-full p-[24px] border border-gray-200 rounded-xl text-left',
48+
'w-full p-4 border border-gray-200 rounded-xl text-left',
4849
'rtl:text-right',
4950
'hover:border-gray-300 hover:bg-gray-50',
5051
'disabled:opacity-50 disabled:cursor-not-allowed'
@@ -55,7 +56,7 @@ function OptionButton( {
5556
spacing={ 5 }
5657
>
5758
<div className="mt-[-2px]">{ icon }</div>
58-
<VStack className="flex-1 gap-[8px]">
59+
<VStack className="flex-1 gap-1.5">
5960
<Heading className="text-[15px]" weight="500">
6061
{ title }
6162
</Heading>
@@ -71,7 +72,7 @@ function OptionButton( {
7172

7273
export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps ) {
7374
const { __ } = useI18n();
74-
const { enableBlueprints } = useFeatureFlags();
75+
const { enableBlueprints, streamlineOnboarding } = useFeatureFlags();
7576
const isOffline = useOffline();
7677
const offlineMessage = __( "You're currently offline." );
7778

@@ -80,7 +81,7 @@ export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps
8081
<Heading className="text-[32px] text-gray-900" weight={ 500 }>
8182
{ __( 'Add a site' ) }
8283
</Heading>
83-
<Text className="text-[15px] font-light text-gray-700 w-72 mb-[28px]">
84+
<Text className="text-[15px] font-light text-gray-700 w-72 mb-4">
8485
{ __( 'Add a clean site, start from a Blueprint or import site from a backup' ) }
8586
</Text>
8687
<OptionButton
@@ -99,6 +100,14 @@ export default function AddSiteOptions( { onOptionSelect }: AddSiteOptionsProps
99100
disabledTooltip={ offlineMessage }
100101
/>
101102
) }
103+
{ streamlineOnboarding && (
104+
<OptionButton
105+
icon={ <Icon icon={ download } size={ 24 } fill="#3858E9" /> }
106+
title={ __( 'Import an existing website' ) }
107+
description={ __( 'Download directly from WordPress.com or Pressable' ) }
108+
onClick={ () => onOptionSelect( 'pullRemote' ) }
109+
/>
110+
) }
102111
<OptionButton
103112
icon={ <Icon icon={ backup } size={ 24 } fill="#3858E9" /> }
104113
title={ __( 'Import from a backup' ) }

0 commit comments

Comments
 (0)