diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index c56433daf..d1f24d416 100644 --- a/src/hooks/tests/use-add-site.test.tsx +++ b/src/hooks/tests/use-add-site.test.tsx @@ -3,10 +3,9 @@ import { renderHook, act } from '@testing-library/react'; import nock from 'nock'; import { Provider } from 'react-redux'; import { useSyncSites } from 'src/hooks/sync-sites'; -import { useAddSite } from 'src/hooks/use-add-site'; +import { useAddSite, CreateSiteFormValues } from 'src/hooks/use-add-site'; import { useContentTabs } from 'src/hooks/use-content-tabs'; import { useSiteDetails } from 'src/hooks/use-site-details'; -import { getWordPressProvider } from 'src/lib/wordpress-provider'; import { store } from 'src/stores'; import { setProviderConstants } from 'src/stores/provider-constants-slice'; import type { SyncSite } from 'src/modules/sync/types'; @@ -19,22 +18,24 @@ jest.mock( 'src/hooks/use-import-export', () => ( { useImportExport: () => ( { importFile: jest.fn(), clearImportState: jest.fn(), + importState: {}, } ), } ) ); const mockConnectWpcomSites = jest.fn().mockResolvedValue( undefined ); +const mockShowOpenFolderDialog = jest.fn(); +const mockGenerateProposedSitePath = jest.fn(); +const mockComparePaths = jest.fn().mockResolvedValue( false ); + jest.mock( 'src/lib/get-ipc-api', () => ( { getIpcApi: () => ( { - generateProposedSitePath: jest.fn().mockResolvedValue( { - path: '/default/path', - name: 'Default Site', - isEmpty: true, - isWordPress: false, - } ), + generateProposedSitePath: mockGenerateProposedSitePath, + showOpenFolderDialog: mockShowOpenFolderDialog, showNotification: jest.fn(), getAllCustomDomains: jest.fn().mockResolvedValue( [] ), connectWpcomSites: mockConnectWpcomSites, getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ), + comparePaths: mockComparePaths, } ), } ) ); @@ -64,6 +65,13 @@ describe( 'useAddSite', () => { } ) ); + mockGenerateProposedSitePath.mockResolvedValue( { + path: '/default/path', + name: 'Default Site', + isEmpty: true, + isWordPress: false, + } ); + ( useSiteDetails as jest.Mock ).mockReturnValue( { createSite: mockCreateSite, updateSite: mockUpdateSite, @@ -126,39 +134,19 @@ describe( 'useAddSite', () => { nock.cleanAll(); } ); - it( 'should initialize with default WordPress version', () => { + it( 'should provide default PHP version', () => { const { result } = renderHookWithProvider( () => useAddSite() ); - expect( result.current.wpVersion ).toBe( getWordPressProvider().DEFAULT_WORDPRESS_VERSION ); + expect( result.current.defaultPhpVersion ).toBe( '8.3' ); } ); - it( 'should initialize with default PHP version', () => { + it( 'should provide default WordPress version', () => { const { result } = renderHookWithProvider( () => useAddSite() ); - expect( result.current.phpVersion ).toBe( '8.3' ); - } ); - - it( 'should update WordPress version when setWpVersion is called', () => { - const { result } = renderHookWithProvider( () => useAddSite() ); - - act( () => { - result.current.setWpVersion( '6.1.7' ); - } ); - - expect( result.current.wpVersion ).toBe( '6.1.7' ); - } ); - - it( 'should update PHP version when setPhpVersion is called', () => { - const { result } = renderHookWithProvider( () => useAddSite() ); - - act( () => { - result.current.setPhpVersion( '8.2' ); - } ); - - expect( result.current.phpVersion ).toBe( '8.2' ); + expect( result.current.defaultWpVersion ).toBe( 'latest' ); } ); - it( 'should pass WordPress version to createSite when handleAddSiteClick is called', async () => { + it( 'should create site with provided form values', async () => { mockCreateSite.mockImplementation( ( path, name, wpVersion, customDomain, enableHttps, blueprint, phpVersion, callback ) => { callback( { @@ -174,27 +162,54 @@ describe( 'useAddSite', () => { const { result } = renderHookWithProvider( () => useAddSite() ); - act( () => { - result.current.setWpVersion( '6.1.7' ); - result.current.setSitePath( '/test/path' ); - } ); + const formValues: CreateSiteFormValues = { + siteName: 'My Test Site', + sitePath: '/test/path', + phpVersion: '8.2', + wpVersion: '6.1.7', + useCustomDomain: false, + customDomain: null, + enableHttps: false, + }; await act( async () => { - await result.current.handleAddSiteClick(); + await result.current.handleCreateSite( formValues ); } ); expect( mockCreateSite ).toHaveBeenCalledWith( '/test/path', - '', + 'My Test Site', '6.1.7', undefined, false, undefined, // blueprint parameter - '8.3', + '8.2', expect.any( Function ) ); } ); + it( 'should generate proposed path for site name', async () => { + mockGenerateProposedSitePath.mockResolvedValue( { + path: '/studio/my-site', + isEmpty: true, + isWordPress: false, + } ); + + const { result } = renderHookWithProvider( () => useAddSite() ); + + let pathResult; + await act( async () => { + pathResult = await result.current.generateProposedPath( 'My Site' ); + } ); + + expect( mockGenerateProposedSitePath ).toHaveBeenCalledWith( 'My Site' ); + expect( pathResult ).toEqual( { + path: '/studio/my-site', + isEmpty: true, + isWordPress: false, + } ); + } ); + it( 'should connect and start pulling when a remote site is selected', async () => { const remoteSite: SyncSite = { id: 123, @@ -228,11 +243,20 @@ describe( 'useAddSite', () => { act( () => { result.current.setSelectedRemoteSite( remoteSite ); - result.current.setSitePath( createdSite.path ); } ); + const formValues: CreateSiteFormValues = { + siteName: createdSite.name, + sitePath: createdSite.path, + phpVersion: '8.3', + wpVersion: 'latest', + useCustomDomain: false, + customDomain: null, + enableHttps: false, + }; + await act( async () => { - await result.current.handleAddSiteClick(); + await result.current.handleCreateSite( formValues ); } ); expect( mockConnectWpcomSites ).toHaveBeenCalledWith( [ diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 53afe155e..20bec38bd 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/electron/renderer'; import { useI18n } from '@wordpress/react-i18n'; import { useCallback, useMemo, useState } from 'react'; import { BlueprintValidationWarning } from 'common/lib/blueprint-validation'; -import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'common/lib/domains'; +import { generateCustomDomainFromSiteName } from 'common/lib/domains'; import { useSyncSites } from 'src/hooks/sync-sites'; import { useContentTabs } from 'src/hooks/use-content-tabs'; import { useImportExport } from 'src/hooks/use-import-export'; @@ -20,6 +20,30 @@ import type { SyncSite } from 'src/modules/sync/types'; import type { Blueprint } from 'src/stores/wpcom-api'; import type { SyncOption } from 'src/types'; +/** + * Form values passed when creating a site + */ +export interface CreateSiteFormValues { + siteName: string; + sitePath: string; + phpVersion: AllowedPHPVersion; + wpVersion: string; + useCustomDomain: boolean; + customDomain: string | null; + enableHttps: boolean; +} + +/** + * Result from path selection or site name change validation + */ +export interface PathValidationResult { + path: string; + name?: string; + isEmpty: boolean; + isWordPress: boolean; + error?: string; +} + interface UseAddSiteOptions { openModal?: () => void; } @@ -34,21 +58,9 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { const { setSelectedTab } = useContentTabs(); const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion ); const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion ); - const [ error, setError ] = useState( '' ); - const [ siteName, setSiteName ] = useState< string | null >( null ); - const [ sitePath, setSitePath ] = useState( '' ); - const [ proposedSitePath, setProposedSitePath ] = useState( '' ); - const [ doesPathContainWordPress, setDoesPathContainWordPress ] = useState( false ); + + // Only keep state that's NOT part of the form const [ fileForImport, setFileForImport ] = useState< File | null >( null ); - const [ phpVersion, setPhpVersion ] = useState< AllowedPHPVersion >( - defaultPhpVersion as AllowedPHPVersion - ); - const [ wpVersion, setWpVersion ] = useState( defaultWordPressVersion ); - const [ useCustomDomain, setUseCustomDomain ] = useState( false ); - const [ customDomain, setCustomDomain ] = useState< string | null >( null ); - const [ customDomainError, setCustomDomainError ] = useState( '' ); - const [ existingDomainNames, setExistingDomainNames ] = useState< string[] >( [] ); - const [ enableHttps, setEnableHttps ] = useState( false ); const [ selectedBlueprint, setSelectedBlueprint ] = useState< Blueprint | undefined >(); const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | undefined >(); const [ blueprintPreferredVersions, setBlueprintPreferredVersions ] = useState< @@ -58,6 +70,7 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { BlueprintValidationWarning[] | undefined >(); const [ isDeeplinkFlow, setIsDeeplinkFlow ] = useState( false ); + const [ existingDomainNames, setExistingDomainNames ] = useState< string[] >( [] ); const isAnySiteProcessing = sites.some( ( site ) => site.isAddingSite || importState[ site.id ]?.isNewSite @@ -70,32 +83,31 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { setBlueprintDeeplinkWarnings( undefined ); }, [] ); + // For blueprint deeplinks - we need temporary state for PHP/WP versions + const [ deeplinkPhpVersion, setDeeplinkPhpVersion ] = useState< AllowedPHPVersion >( + defaultPhpVersion as AllowedPHPVersion + ); + const [ deeplinkWpVersion, setDeeplinkWpVersion ] = useState( defaultWordPressVersion ); + useBlueprintDeeplink( { isAnySiteProcessing, openModal, setSelectedBlueprint, - setPhpVersion, - setWpVersion, + setPhpVersion: setDeeplinkPhpVersion, + setWpVersion: setDeeplinkWpVersion, setBlueprintPreferredVersions, setBlueprintDeeplinkWarnings, navigateToBlueprintDeeplink: () => setIsDeeplinkFlow( true ), } ); const resetForm = useCallback( () => { - setSitePath( '' ); - setError( '' ); - setDoesPathContainWordPress( false ); - setWpVersion( defaultWordPressVersion ); - setPhpVersion( defaultPhpVersion ); - setUseCustomDomain( false ); - setCustomDomain( null ); - setCustomDomainError( '' ); - setEnableHttps( false ); setFileForImport( null ); setSelectedBlueprint( undefined ); setBlueprintPreferredVersions( undefined ); setBlueprintDeeplinkWarnings( undefined ); setSelectedRemoteSite( undefined ); + setDeeplinkPhpVersion( defaultPhpVersion as AllowedPHPVersion ); + setDeeplinkWpVersion( defaultWordPressVersion ); clearDeeplinkState(); }, [ clearDeeplinkState, defaultPhpVersion, defaultWordPressVersion ] ); @@ -110,8 +122,11 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { } ); }, [] ); - const siteWithPathAlreadyExists = useCallback( - async ( path: string ) => { + /** + * Check if a path is already associated with an existing site + */ + const checkPathExists = useCallback( + async ( path: string ): Promise< boolean > => { const results = await Promise.all( sites.map( ( site ) => getIpcApi().comparePaths( site.path, path ) ) ); @@ -120,232 +135,227 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { [ sites ] ); - const handleCustomDomainChange = useCallback( - ( value: string | null ) => { - setCustomDomain( value ); - setCustomDomainError( - getDomainNameValidationError( useCustomDomain, value, existingDomainNames ) + /** + * Open folder picker and validate the selected path + * Returns the result for the form to use + */ + const selectPath = useCallback( + async ( currentPath: string ): Promise< PathValidationResult | null > => { + const response = await getIpcApi().showOpenFolderDialog( + __( 'Choose folder for site' ), + currentPath ); - }, - [ useCustomDomain, setCustomDomain, setCustomDomainError, existingDomainNames ] - ); - const handlePathSelectorClick = useCallback( async () => { - const response = await getIpcApi().showOpenFolderDialog( - __( 'Choose folder for site' ), - sitePath - ); - if ( response?.path ) { + if ( ! response?.path ) { + return null; + } + const { path, name, isEmpty, isWordPress } = response; - setDoesPathContainWordPress( false ); - setError( '' ); - const pathResetToDefaultSitePath = - path === proposedSitePath.substring( 0, proposedSitePath.lastIndexOf( '/' ) ); - - setSitePath( pathResetToDefaultSitePath ? '' : path ); - if ( await siteWithPathAlreadyExists( path ) ) { - setError( - __( + + if ( await checkPathExists( path ) ) { + return { + path, + name: name ?? undefined, + isEmpty, + isWordPress, + error: __( 'The directory is already associated with another Studio site. Please choose a different custom local path.' - ) - ); - return; + ), + }; } - if ( ! isEmpty && ! isWordPress && ! pathResetToDefaultSitePath ) { - setError( - __( + + if ( ! isEmpty && ! isWordPress ) { + return { + path, + name: name ?? undefined, + isEmpty, + isWordPress, + error: __( 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' - ) - ); - return; + ), + }; } - setDoesPathContainWordPress( ! isEmpty && isWordPress ); - if ( ! siteName ) { - setSiteName( name ?? null ); + + return { + path, + name: name ?? undefined, + isEmpty, + isWordPress, + }; + }, + [ __, checkPathExists ] + ); + + /** + * Generate a proposed path for a site name and validate it + */ + const generateProposedPath = useCallback( + async ( siteName: string ): Promise< PathValidationResult > => { + const { path, isEmpty, isWordPress } = await getIpcApi().generateProposedSitePath( siteName ); + + if ( await checkPathExists( path ) ) { + return { + path, + isEmpty, + isWordPress, + error: __( + 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' + ), + }; } - } - }, [ __, siteWithPathAlreadyExists, siteName, proposedSitePath, sitePath ] ); - - const handleAddSiteClick = useCallback( async () => { - try { - const path = sitePath ? sitePath : proposedSitePath; - let usedCustomDomain = useCustomDomain && customDomain ? customDomain : undefined; - if ( useCustomDomain && ! customDomain ) { - usedCustomDomain = generateCustomDomainFromSiteName( siteName ?? '' ); + + if ( ! isEmpty && ! isWordPress ) { + return { + path, + isEmpty, + isWordPress, + error: __( + 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' + ), + }; } - await createSite( - path, - siteName ?? '', - wpVersion, - usedCustomDomain, - useCustomDomain ? enableHttps : false, - selectedBlueprint, - phpVersion, - async ( newSite ) => { - if ( fileForImport ) { - await importFile( fileForImport, newSite, { - showImportNotification: false, - isNewSite: true, - } ); - clearImportState( newSite.id ); - - getIpcApi().showNotification( { - title: newSite.name, - body: __( 'Your new site was imported' ), - } ); - } else { - if ( selectedRemoteSite ) { - await connectSite( { site: selectedRemoteSite, localSiteId: newSite.id } ); - const pullOptions: SyncOption[] = [ 'all' ]; - pullSite( selectedRemoteSite, newSite, { - optionsToSync: pullOptions, + + return { path, isEmpty, isWordPress }; + }, + [ __, checkPathExists ] + ); + + /** + * Create a new site with the given form values + */ + const handleCreateSite = useCallback( + async ( formValues: CreateSiteFormValues ) => { + try { + let usedCustomDomain = + formValues.useCustomDomain && formValues.customDomain + ? formValues.customDomain + : undefined; + if ( formValues.useCustomDomain && ! formValues.customDomain ) { + usedCustomDomain = generateCustomDomainFromSiteName( formValues.siteName ); + } + + await createSite( + formValues.sitePath, + formValues.siteName, + formValues.wpVersion, + usedCustomDomain, + formValues.useCustomDomain ? formValues.enableHttps : false, + selectedBlueprint, + formValues.phpVersion, + async ( newSite ) => { + if ( fileForImport ) { + await importFile( fileForImport, newSite, { + showImportNotification: false, + isNewSite: true, } ); - setSelectedTab( 'sync' ); - } else { - await startServer( newSite.id ); + clearImportState( newSite.id ); getIpcApi().showNotification( { title: newSite.name, - body: __( 'Your new site was created' ), + body: __( 'Your new site was imported' ), } ); + } else { + if ( selectedRemoteSite ) { + await connectSite( { site: selectedRemoteSite, localSiteId: newSite.id } ); + const pullOptions: SyncOption[] = [ 'all' ]; + pullSite( selectedRemoteSite, newSite, { + optionsToSync: pullOptions, + } ); + setSelectedTab( 'sync' ); + } else { + await startServer( newSite.id ); + + getIpcApi().showNotification( { + title: newSite.name, + body: __( 'Your new site was created' ), + } ); + } } } - } - ); - } catch ( e ) { - Sentry.captureException( e ); - } - }, [ - __, - clearImportState, - createSite, - fileForImport, - importFile, - proposedSitePath, - siteName, - sitePath, - startServer, - wpVersion, - phpVersion, - customDomain, - useCustomDomain, - enableHttps, - selectedBlueprint, - selectedRemoteSite, - pullSite, - connectSite, - setSelectedTab, - ] ); - - const handleSiteNameChange = useCallback( - async ( name: string ) => { - setSiteName( name ); - if ( sitePath ) { - return; - } - setError( '' ); - const { - path: proposedPath, - isEmpty, - isWordPress, - } = await getIpcApi().generateProposedSitePath( name ); - setProposedSitePath( proposedPath ); - - if ( await siteWithPathAlreadyExists( proposedPath ) ) { - setError( - __( - 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' - ) ); - return; + } catch ( e ) { + Sentry.captureException( e ); } - if ( ! isEmpty && ! isWordPress ) { - setError( - __( - 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' - ) - ); - return; - } - setDoesPathContainWordPress( ! isEmpty && isWordPress ); }, - [ __, sitePath, siteWithPathAlreadyExists ] + [ + __, + clearImportState, + createSite, + fileForImport, + importFile, + startServer, + selectedBlueprint, + selectedRemoteSite, + pullSite, + connectSite, + setSelectedTab, + ] ); - return useMemo( () => { - return { - resetForm, - handleAddSiteClick, - handlePathSelectorClick, - handleSiteNameChange, - error, - sitePath: sitePath ? sitePath : proposedSitePath, - siteName, - doesPathContainWordPress, - setSiteName, - proposedSitePath, - setProposedSitePath, - setSitePath, - setError, - setDoesPathContainWordPress, + return useMemo( + () => ( { + // Site creation + handleCreateSite, + + // Path helpers (for form to use) + selectPath, + generateProposedPath, + + // Default values (for form initialization) + defaultPhpVersion: defaultPhpVersion as AllowedPHPVersion, + defaultWpVersion: defaultWordPressVersion, + + // Blueprint deeplink values (set by deeplink handler) + deeplinkPhpVersion, + deeplinkWpVersion, + + // Import file fileForImport, setFileForImport, - phpVersion, - setPhpVersion, - wpVersion, - setWpVersion, - useCustomDomain, - setUseCustomDomain, - customDomain, - setCustomDomain: handleCustomDomainChange, - customDomainError, - setCustomDomainError, - enableHttps, - setEnableHttps, - loadAllCustomDomains, + + // Blueprint selection selectedBlueprint, setSelectedBlueprint, - selectedRemoteSite, - setSelectedRemoteSite, blueprintPreferredVersions, setBlueprintPreferredVersions, blueprintDeeplinkWarnings, - setBlueprintDeeplinkWarnings, + + // Remote site selection + selectedRemoteSite, + setSelectedRemoteSite, + + // Custom domain helpers + existingDomainNames, + loadAllCustomDomains, + + // Flow state isDeeplinkFlow, setIsDeeplinkFlow, isAnySiteProcessing, + + // Reset + resetForm, clearDeeplinkState, - }; - }, [ - resetForm, - doesPathContainWordPress, - error, - handleAddSiteClick, - handlePathSelectorClick, - handleSiteNameChange, - siteName, - sitePath, - proposedSitePath, - fileForImport, - phpVersion, - wpVersion, - useCustomDomain, - setUseCustomDomain, - customDomain, - handleCustomDomainChange, - customDomainError, - setCustomDomainError, - enableHttps, - setEnableHttps, - loadAllCustomDomains, - selectedBlueprint, - setSelectedBlueprint, - selectedRemoteSite, - setSelectedRemoteSite, - blueprintPreferredVersions, - blueprintDeeplinkWarnings, - isDeeplinkFlow, - isAnySiteProcessing, - clearDeeplinkState, - ] ); + } ), + [ + handleCreateSite, + selectPath, + generateProposedPath, + defaultPhpVersion, + defaultWordPressVersion, + deeplinkPhpVersion, + deeplinkWpVersion, + fileForImport, + selectedBlueprint, + blueprintPreferredVersions, + blueprintDeeplinkWarnings, + selectedRemoteSite, + existingDomainNames, + loadAllCustomDomains, + isDeeplinkFlow, + isAnySiteProcessing, + resetForm, + clearDeeplinkState, + ] + ); } diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 51d9b0824..1a7b50d3b 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -3,8 +3,8 @@ import { createInterpolateElement } from '@wordpress/element'; import { __, sprintf, _n } from '@wordpress/i18n'; import { tip, cautionFilled, chevronRight, chevronDown, chevronLeft } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; -import { FormEvent, useState, useEffect } from 'react'; -import { generateCustomDomainFromSiteName } from 'common/lib/domains'; +import { FormEvent, useState, useEffect, useCallback, useMemo, RefObject } from 'react'; +import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'common/lib/domains'; import Button from 'src/components/button'; import FolderIcon from 'src/components/folder-icon'; import TextControlComponent from 'src/components/text-control'; @@ -20,6 +20,46 @@ import { selectAllowedPhpVersions, } from 'src/stores/provider-constants-slice'; +export interface CreateSiteFormValues { + siteName: string; + sitePath: string; + phpVersion: AllowedPHPVersion; + wpVersion: string; + useCustomDomain: boolean; + customDomain: string | null; + enableHttps: boolean; +} + +export interface PathValidationResult { + path: string; + name?: string; + isEmpty: boolean; + isWordPress: boolean; + error?: string; +} + +export interface CreateSiteFormProps { + /** Default values for form initialization (only used once on mount) */ + defaultValues?: { + siteName?: string; + sitePath?: string; + phpVersion?: AllowedPHPVersion; + wpVersion?: string; + }; + /** Callback to select a path via folder picker. Returns validation result. */ + onSelectPath?: ( currentPath: string ) => Promise< PathValidationResult | null >; + /** Callback when site name changes to generate proposed path. Returns validation result. */ + onSiteNameChange?: ( name: string ) => Promise< PathValidationResult >; + /** Existing domain names for validation */ + existingDomainNames?: string[]; + /** Blueprint preferred versions for warning display */ + blueprintPreferredVersions?: { php?: string; wp?: string }; + /** Called when form is submitted with all form values */ + onSubmit: ( values: CreateSiteFormValues ) => void; + /** Optional ref to the form element for programmatic submission */ + formRef?: RefObject< HTMLFormElement >; +} + interface FormPathInputComponentProps { value: string; onClick: () => void; @@ -34,30 +74,6 @@ interface SiteFormErrorProps { className?: string; } -interface SiteFormProps { - siteName: string; - setSiteName: ( name: string ) => void; - sitePath?: string; - onSelectPath?: () => void; - error: string; - doesPathContainWordPress?: boolean; - onSubmit: ( event: FormEvent ) => void; - useCustomDomain?: boolean; - setUseCustomDomain?: ( use: boolean ) => void; - customDomain?: string | null; - setCustomDomain?: ( domain: string ) => void; - customDomainError?: string; - phpVersion: AllowedPHPVersion; - setPhpVersion: ( version: AllowedPHPVersion ) => void; - useHttps?: boolean; - setUseHttps?: ( use: boolean ) => void; - enableHttps?: boolean; - setEnableHttps?: ( use: boolean ) => void; - wpVersion: string; - setWpVersion: ( version: string ) => void; - blueprintPreferredVersions?: { php?: string; wp?: string }; -} - const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => { return ( ( error || tipMessage ) && ( @@ -82,6 +98,7 @@ const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErro ) ); }; + function FormPathInputComponent( { value, onClick, @@ -94,14 +111,6 @@ function FormPathInputComponent( {
@@ -269,9 +387,9 @@ export const CreateSiteForm = ( {
@@ -286,7 +404,7 @@ export const CreateSiteForm = ( { label: version, value: version, } ) ) } - onChange={ setPhpVersion as ( value: string ) => void } + onChange={ ( value: string ) => setPhpVersion( value as AllowedPHPVersion ) } __next40pxDefaultSize __nextHasNoMarginBottom /> @@ -337,67 +455,67 @@ export const CreateSiteForm = ( { ) } - { setUseCustomDomain && setCustomDomain && ( -
- setUseCustomDomain( e.target.checked ) } - /> - -
- ) } + { existingDomainNames.length >= 0 && ( + <> +
+ setUseCustomDomain( e.target.checked ) } + /> + +
- { setUseCustomDomain && setCustomDomain && ( -
- { __( 'Your system password will be required to set up the domain.' ) } -
+
+ { __( 'Your system password will be required to set up the domain.' ) } +
+ ) } - { useCustomDomain && setCustomDomain && ( -
- - - { customDomainError && } -
- ) } + { useCustomDomain && ( + <> +
+ + + { customDomainError && } +
- { useCustomDomain && setEnableHttps && ( -
- setEnableHttps( e.target.checked ) } - /> - -
- ) } +
+ setEnableHttps( e.target.checked ) } + /> + +
- { ! isCertificateTrusted && useCustomDomain && setEnableHttps && ( -
- { __( - 'You need to manually add the Studio root certificate authority to your keychain and trust it to enable HTTPS.' - ) }{ ' ' } - -
+ { ! isCertificateTrusted && ( +
+ { __( + 'You need to manually add the Studio root certificate authority to your keychain and trust it to enable HTTPS.' + ) }{ ' ' } + +
+ ) } + ) }
diff --git a/src/modules/add-site/components/create-site.tsx b/src/modules/add-site/components/create-site.tsx index 0f57a01b6..0f65a5f5a 100644 --- a/src/modules/add-site/components/create-site.tsx +++ b/src/modules/add-site/components/create-site.tsx @@ -3,51 +3,37 @@ import { __experimentalHeading as Heading, } from '@wordpress/components'; import { useI18n } from '@wordpress/react-i18n'; -import { FormEvent } from 'react'; -import { CreateSiteForm } from 'src/modules/add-site/components/create-site-form'; +import { RefObject } from 'react'; +import { AllowedPHPVersion } from 'src/lib/wordpress-provider/constants'; +import { + CreateSiteForm, + CreateSiteFormValues, + PathValidationResult, +} from 'src/modules/add-site/components/create-site-form'; -interface CreateSiteProps { - siteName: string | null; - handleSiteNameChange: ( name: string ) => Promise< void >; - phpVersion: string; - setPhpVersion: ( version: string ) => void; - wpVersion: string; - setWpVersion: ( version: string ) => void; - sitePath: string; - handlePathSelectorClick: () => void; - error: string; - handleSubmit: ( event: FormEvent ) => void; - doesPathContainWordPress: boolean; - useCustomDomain: boolean; - setUseCustomDomain: ( use: boolean ) => void; - customDomain: string | null; - setCustomDomain: ( domain: string | null ) => void; - customDomainError: string; - enableHttps: boolean; - setEnableHttps: ( enable: boolean ) => void; +export interface CreateSiteProps { + defaultValues?: { + siteName?: string; + sitePath?: string; + phpVersion?: AllowedPHPVersion; + wpVersion?: string; + }; + onSelectPath: ( currentPath: string ) => Promise< PathValidationResult | null >; + onSiteNameChange: ( name: string ) => Promise< PathValidationResult >; + existingDomainNames?: string[]; blueprintPreferredVersions?: { php?: string; wp?: string }; + onSubmit: ( values: CreateSiteFormValues ) => void; + formRef?: RefObject< HTMLFormElement >; } export default function CreateSite( { - siteName, - handleSiteNameChange, - phpVersion, - setPhpVersion, - wpVersion, - setWpVersion, - sitePath, - handlePathSelectorClick, - error, - handleSubmit, - doesPathContainWordPress, - useCustomDomain, - setUseCustomDomain, - customDomain, - setCustomDomain, - customDomainError, - enableHttps, - setEnableHttps, + defaultValues, + onSelectPath, + onSiteNameChange, + existingDomainNames = [], blueprintPreferredVersions, + onSubmit, + formRef, }: CreateSiteProps ) { const { __ } = useI18n(); @@ -58,25 +44,13 @@ export default function CreateSite( { void handleSiteNameChange( name ) } - phpVersion={ phpVersion } - setPhpVersion={ setPhpVersion } - wpVersion={ wpVersion } - setWpVersion={ setWpVersion } - sitePath={ sitePath } - onSelectPath={ handlePathSelectorClick } - error={ error } - onSubmit={ handleSubmit } - doesPathContainWordPress={ doesPathContainWordPress } - useCustomDomain={ useCustomDomain } - setUseCustomDomain={ setUseCustomDomain } - customDomain={ customDomain } - setCustomDomain={ setCustomDomain } - customDomainError={ customDomainError } - enableHttps={ enableHttps } - setEnableHttps={ setEnableHttps } + defaultValues={ defaultValues } + onSelectPath={ onSelectPath } + onSiteNameChange={ onSiteNameChange } + existingDomainNames={ existingDomainNames } blueprintPreferredVersions={ blueprintPreferredVersions } + onSubmit={ onSubmit } + formRef={ formRef } /> ); diff --git a/src/modules/add-site/components/index.ts b/src/modules/add-site/components/index.ts new file mode 100644 index 000000000..283bc4a6c --- /dev/null +++ b/src/modules/add-site/components/index.ts @@ -0,0 +1,8 @@ +export { CreateSiteForm } from './create-site-form'; +export type { + CreateSiteFormValues, + CreateSiteFormProps, + PathValidationResult, +} from './create-site-form'; +export { default as CreateSite } from './create-site'; +export type { CreateSiteProps } from './create-site'; diff --git a/src/modules/add-site/components/stepper.tsx b/src/modules/add-site/components/stepper.tsx index cd1260ee7..39bd50b33 100644 --- a/src/modules/add-site/components/stepper.tsx +++ b/src/modules/add-site/components/stepper.tsx @@ -99,6 +99,7 @@ export default function Stepper( { { actionButton?.isVisible && onSubmit && (