From a71be71f09d0c7b31644aba9bef64b87effec5c9 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 11:14:40 +0100 Subject: [PATCH 01/19] Reduce props --- src/hooks/use-add-site.ts | 2 + .../add-site/components/create-site-form.tsx | 220 +++++++++++++----- .../add-site/components/create-site.tsx | 101 ++++---- src/modules/add-site/components/index.ts | 11 + src/modules/add-site/index.tsx | 168 +++++++++---- 5 files changed, 342 insertions(+), 160 deletions(-) create mode 100644 src/modules/add-site/components/index.ts diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 53afe155ed..111ef2bcfe 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -300,6 +300,7 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { setCustomDomain: handleCustomDomainChange, customDomainError, setCustomDomainError, + existingDomainNames, enableHttps, setEnableHttps, loadAllCustomDomains, @@ -335,6 +336,7 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { handleCustomDomainChange, customDomainError, setCustomDomainError, + existingDomainNames, enableHttps, setEnableHttps, loadAllCustomDomains, diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 51d9b08248..59d1cdec10 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 } 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,45 @@ 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 PathSelectionFeature { + enabled: true; + onSelectPath: () => void; + doesPathContainWordPress?: boolean; + error?: string; +} + +export interface CustomDomainFeature { + enabled: true; + existingDomainNames?: string[]; +} + +export interface BlueprintVersionsFeature { + enabled: true; + preferredVersions: { php?: string; wp?: string }; +} + +export interface CreateSiteFormFeatures { + pathSelection?: PathSelectionFeature; + customDomain?: CustomDomainFeature; + blueprintVersions?: BlueprintVersionsFeature; +} + +export interface CreateSiteFormProps { + features?: CreateSiteFormFeatures; + initialValues?: Partial< CreateSiteFormValues >; + onSubmit: ( values: CreateSiteFormValues ) => void; +} + interface FormPathInputComponentProps { value: string; onClick: () => void; @@ -34,30 +73,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 +97,7 @@ const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErro ) ); }; + function FormPathInputComponent( { value, onClick, @@ -139,43 +155,106 @@ function FormPathInputComponent( { } export const CreateSiteForm = ( { - siteName, - setSiteName, - phpVersion, - setPhpVersion, - wpVersion, - setWpVersion, - sitePath = '', - onSelectPath, - error, + features = {}, + initialValues = {}, onSubmit, - doesPathContainWordPress = false, - useCustomDomain, - setUseCustomDomain, - customDomain = null, - setCustomDomain, - customDomainError, - enableHttps, - setEnableHttps, - blueprintPreferredVersions, -}: SiteFormProps ) => { +}: CreateSiteFormProps ) => { const { __, isRTL } = useI18n(); const locale = useI18nLocale(); const { data: isCertificateTrusted } = useCheckCertificateTrustQuery(); const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion ); const allowedPhpVersions = useRootSelector( selectAllowedPhpVersions ); + // Internal form state + const [ siteName, setSiteName ] = useState( initialValues.siteName ?? '' ); + const [ sitePath, setSitePath ] = useState( initialValues.sitePath ?? '' ); + const [ phpVersion, setPhpVersion ] = useState< AllowedPHPVersion >( + ( initialValues.phpVersion as AllowedPHPVersion ) ?? ( allowedPhpVersions[ 0 ] || '8.2' ) + ); + const [ wpVersion, setWpVersion ] = useState( + initialValues.wpVersion ?? defaultWordPressVersion + ); + const [ useCustomDomain, setUseCustomDomain ] = useState( + initialValues.useCustomDomain ?? false + ); + const [ customDomain, setCustomDomain ] = useState< string | null >( + initialValues.customDomain ?? null + ); + const [ enableHttps, setEnableHttps ] = useState( initialValues.enableHttps ?? false ); + const [ customDomainError, setCustomDomainError ] = useState( '' ); + const [ isAdvancedSettingsVisible, setAdvancedSettingsVisible ] = useState( false ); + + // Sync with initialValues changes + useEffect( () => { + if ( initialValues.siteName !== undefined ) { + setSiteName( initialValues.siteName ); + } + }, [ initialValues.siteName ] ); + + useEffect( () => { + if ( initialValues.sitePath !== undefined ) { + setSitePath( initialValues.sitePath ); + } + }, [ initialValues.sitePath ] ); + + useEffect( () => { + if ( initialValues.phpVersion !== undefined ) { + setPhpVersion( initialValues.phpVersion ); + } + }, [ initialValues.phpVersion ] ); + + useEffect( () => { + if ( initialValues.wpVersion !== undefined ) { + setWpVersion( initialValues.wpVersion ); + } + }, [ initialValues.wpVersion ] ); + // If the custom domain is enabled and the root certificate is trusted, enable HTTPS useEffect( () => { - if ( useCustomDomain && isCertificateTrusted && setEnableHttps ) { + if ( useCustomDomain && isCertificateTrusted ) { setEnableHttps( true ); } - }, [ useCustomDomain, isCertificateTrusted, setEnableHttps ] ); + }, [ useCustomDomain, isCertificateTrusted ] ); - const shouldShowCustomDomainError = useCustomDomain && customDomainError; - const errorCount = [ error, shouldShowCustomDomainError ].filter( Boolean ).length; + const existingDomainNames = useMemo( + () => features.customDomain?.existingDomainNames ?? [], + [ features.customDomain?.existingDomainNames ] + ); - const [ isAdvancedSettingsVisible, setAdvancedSettingsVisible ] = useState( false ); + const handleCustomDomainChange = useCallback( + ( value: string ) => { + setCustomDomain( value ); + setCustomDomainError( + getDomainNameValidationError( useCustomDomain, value, existingDomainNames ) + ); + }, + [ useCustomDomain, existingDomainNames ] + ); + + const formValues = useMemo< CreateSiteFormValues >( + () => ( { + siteName, + sitePath, + phpVersion, + wpVersion, + useCustomDomain, + customDomain, + enableHttps, + } ), + [ siteName, sitePath, phpVersion, wpVersion, useCustomDomain, customDomain, enableHttps ] + ); + + const handleFormSubmit = useCallback( + ( event: FormEvent ) => { + event.preventDefault(); + onSubmit( formValues ); + }, + [ onSubmit, formValues ] + ); + + const pathSelectionError = features.pathSelection?.error; + const shouldShowCustomDomainError = useCustomDomain && customDomainError; + const errorCount = [ pathSelectionError, shouldShowCustomDomainError ].filter( Boolean ).length; const handleAdvancedSettingsClick = () => { setAdvancedSettingsVisible( ! isAdvancedSettingsVisible ); @@ -192,6 +271,8 @@ export const CreateSiteForm = ( { } const generatedDomainName = generateCustomDomainFromSiteName( siteName ); + const blueprintPreferredVersions = features.blueprintVersions?.preferredVersions; + // Check if current versions differ from blueprint recommendations const showBlueprintVersionWarning = blueprintPreferredVersions && @@ -199,7 +280,7 @@ export const CreateSiteForm = ( { ( blueprintPreferredVersions.wp && blueprintPreferredVersions.wp !== wpVersion ) ); return ( -
+
- { onSelectPath && ( + { features.pathSelection?.enabled && ( <>
@@ -268,10 +358,12 @@ export const CreateSiteForm = ( { ) }
@@ -337,7 +429,7 @@ export const CreateSiteForm = ( { ) } - { setUseCustomDomain && setCustomDomain && ( + { features.customDomain?.enabled && (
) } - { setUseCustomDomain && setCustomDomain && ( + { features.customDomain?.enabled && (
{ __( 'Your system password will be required to set up the domain.' ) }
) } - { useCustomDomain && setCustomDomain && ( + { useCustomDomain && features.customDomain?.enabled && (
) } - { useCustomDomain && setEnableHttps && ( + { useCustomDomain && features.customDomain?.enabled && (
) } - { ! isCertificateTrusted && useCustomDomain && setEnableHttps && ( + { ! isCertificateTrusted && useCustomDomain && features.customDomain?.enabled && (
{ __( '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 0f57a01b61..2ad85f62fb 100644 --- a/src/modules/add-site/components/create-site.tsx +++ b/src/modules/add-site/components/create-site.tsx @@ -3,81 +3,86 @@ 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 { useMemo } from 'react'; +import { AllowedPHPVersion } from 'src/lib/wordpress-provider/constants'; +import { + CreateSiteForm, + CreateSiteFormValues, + CreateSiteFormFeatures, +} from 'src/modules/add-site/components/create-site-form'; -interface CreateSiteProps { +export interface CreateSiteProps { siteName: string | null; - handleSiteNameChange: ( name: string ) => Promise< void >; + sitePath: string; phpVersion: string; - setPhpVersion: ( version: string ) => void; wpVersion: string; - setWpVersion: ( version: string ) => void; - sitePath: string; + onSubmit: ( values: CreateSiteFormValues ) => void; 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; + existingDomainNames?: string[]; blueprintPreferredVersions?: { php?: string; wp?: string }; } export default function CreateSite( { siteName, - handleSiteNameChange, + sitePath, phpVersion, - setPhpVersion, wpVersion, - setWpVersion, - sitePath, + onSubmit, handlePathSelectorClick, error, - handleSubmit, doesPathContainWordPress, - useCustomDomain, - setUseCustomDomain, - customDomain, - setCustomDomain, - customDomainError, - enableHttps, - setEnableHttps, + existingDomainNames = [], blueprintPreferredVersions, }: CreateSiteProps ) { const { __ } = useI18n(); + const features = useMemo< CreateSiteFormFeatures >( + () => ( { + pathSelection: { + enabled: true, + onSelectPath: handlePathSelectorClick, + doesPathContainWordPress, + error, + }, + customDomain: { + enabled: true, + existingDomainNames, + }, + ...( blueprintPreferredVersions && { + blueprintVersions: { + enabled: true, + preferredVersions: blueprintPreferredVersions, + }, + } ), + } ), + [ + handlePathSelectorClick, + doesPathContainWordPress, + error, + existingDomainNames, + blueprintPreferredVersions, + ] + ); + + const initialValues = useMemo( + () => ( { + siteName: siteName || '', + sitePath, + phpVersion: phpVersion as AllowedPHPVersion, + wpVersion, + } ), + [ siteName, sitePath, phpVersion, wpVersion ] + ); + return ( { __( 'Add a site' ) } - 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 } - blueprintPreferredVersions={ blueprintPreferredVersions } - /> + ); } diff --git a/src/modules/add-site/components/index.ts b/src/modules/add-site/components/index.ts new file mode 100644 index 0000000000..1967aff28f --- /dev/null +++ b/src/modules/add-site/components/index.ts @@ -0,0 +1,11 @@ +export { CreateSiteForm } from './create-site-form'; +export type { + CreateSiteFormValues, + CreateSiteFormFeatures, + CreateSiteFormProps, + PathSelectionFeature, + CustomDomainFeature, + BlueprintVersionsFeature, +} from './create-site-form'; +export { default as CreateSite } from './create-site'; +export type { CreateSiteProps } from './create-site'; diff --git a/src/modules/add-site/index.tsx b/src/modules/add-site/index.tsx index 9d13b582bf..a2c868dd1f 100644 --- a/src/modules/add-site/index.tsx +++ b/src/modules/add-site/index.tsx @@ -2,7 +2,7 @@ import { speak } from '@wordpress/a11y'; import { Navigator, useNavigator } from '@wordpress/components'; import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; -import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { BlueprintValidationWarning } from 'common/lib/blueprint-validation'; import Button from 'src/components/button'; import { FullscreenModal } from 'src/components/fullscreen-modal'; @@ -25,35 +25,26 @@ import AddSiteOptions, { type AddSiteFlowType } from './components/options'; import { PullRemoteSite } from './components/pull-remote-site'; import Stepper from './components/stepper'; import { useFindAvailableSiteName } from './hooks/use-find-available-site-name'; +import type { CreateSiteFormValues } from './components/create-site-form'; type BlueprintsData = ReturnType< typeof useGetBlueprints >[ 'data' ]; interface NavigationContentProps { blueprintsData: BlueprintsData; isLoadingBlueprints: boolean; + blueprintsErrorMessage?: string | undefined; siteName: string | null; - handleSiteNameChange: ( name: string ) => Promise< void >; + sitePath: string; 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; + existingDomainNames: string[]; fileForImport: File | null; setFileForImport: ( file: File | null ) => void; setSelectedBlueprint: ( blueprint?: Blueprint ) => void; selectedBlueprint?: Blueprint; - blueprintsErrorMessage?: string | undefined; blueprintPreferredVersions?: { php?: string; wp?: string }; setBlueprintPreferredVersions?: ( versions: { php?: string; wp?: string } | undefined ) => void; blueprintDeeplinkWarnings?: BlueprintValidationWarning[]; @@ -61,6 +52,10 @@ interface NavigationContentProps { setSelectedRemoteSite: ( site?: SyncSite ) => void; isDeeplinkFlow: boolean; setIsDeeplinkFlow: ( isDeeplink: boolean ) => void; + setPhpVersion: ( version: string ) => void; + setWpVersion: ( version: string ) => void; + handleSiteNameChange: ( name: string ) => Promise< void >; + handleFormSubmit: ( values: CreateSiteFormValues ) => void; } function NavigationContent( props: NavigationContentProps ) { @@ -77,11 +72,24 @@ function NavigationContent( props: NavigationContentProps ) { setSelectedRemoteSite, isDeeplinkFlow, setIsDeeplinkFlow, - ...createSiteProps + siteName, + sitePath, + phpVersion, + wpVersion, + handlePathSelectorClick, + error, + doesPathContainWordPress, + existingDomainNames, + fileForImport, + setFileForImport, + selectedBlueprint, + setSelectedBlueprint, + setPhpVersion, + setWpVersion, + handleSiteNameChange, + handleFormSubmit, } = props; - const { selectedBlueprint, setSelectedBlueprint, setPhpVersion, setWpVersion } = createSiteProps; - useEffect( () => { if ( isDeeplinkFlow && selectedBlueprint ) { goTo( '/blueprint/deeplink' ); @@ -112,25 +120,25 @@ function NavigationContent( props: NavigationContentProps ) { const handleBackupFileSelect = useCallback( ( file?: File ) => { - createSiteProps.setFileForImport( file || null ); + setFileForImport( file || null ); }, - [ createSiteProps ] + [ setFileForImport ] ); const handleBackupContinue = useCallback( () => { - if ( createSiteProps.fileForImport ) { + if ( fileForImport ) { goTo( '/backup/create' ); } - }, [ createSiteProps, goTo ] ); + }, [ fileForImport, goTo ] ); const findAvailableSiteName = useFindAvailableSiteName(); const handlePullRemoteContinue = useCallback( async () => { if ( selectedRemoteSite ) { const availableName = await findAvailableSiteName( selectedRemoteSite.name ); - await createSiteProps.handleSiteNameChange( availableName ); + await handleSiteNameChange( availableName ); goTo( '/pullRemote/create' ); } - }, [ createSiteProps, findAvailableSiteName, goTo, selectedRemoteSite ] ); + }, [ handleSiteNameChange, findAvailableSiteName, goTo, selectedRemoteSite ] ); const blueprints = useMemo( () => blueprintsData?.blueprints.slice().reverse() || [], @@ -143,11 +151,7 @@ function NavigationContent( props: NavigationContentProps ) { location.path === '/blueprint/deeplink/create' || location.path === '/backup/create' || location.path === '/pullRemote/create'; - const canSubmit = - isOnCreatePath && - createSiteProps.siteName?.trim() && - ! createSiteProps.error && - ( ! createSiteProps.useCustomDomain || ! createSiteProps.customDomainError ); + const canSubmit = isOnCreatePath && siteName?.trim() && ! error; const handleBlueprintDeeplinkContinue = useCallback( () => { goTo( '/blueprint/deeplink/create' ); @@ -170,10 +174,10 @@ function NavigationContent( props: NavigationContentProps ) { location.path === '/pullRemote' ) { if ( location.path === '/backup' ) { - createSiteProps.setFileForImport( null ); + setFileForImport( null ); } if ( location.path === '/blueprint/select' || location.path === '/blueprint/deeplink' ) { - createSiteProps.setSelectedBlueprint(); + setSelectedBlueprint(); setBlueprintPreferredVersions?.( undefined ); } if ( location.path === '/pullRemote' ) { @@ -186,7 +190,8 @@ function NavigationContent( props: NavigationContentProps ) { }, [ location.path, goTo, - createSiteProps, + setFileForImport, + setSelectedBlueprint, setBlueprintPreferredVersions, setSelectedRemoteSite, ] ); @@ -233,6 +238,19 @@ function NavigationContent( props: NavigationContentProps ) { [ setSelectedBlueprint, applyBlueprintVersions ] ); + // Common props for CreateSite component + const createSiteCommonProps = { + siteName, + sitePath, + phpVersion, + wpVersion, + handlePathSelectorClick, + error, + doesPathContainWordPress, + existingDomainNames, + onSubmit: handleFormSubmit, + }; + return ( <> @@ -250,12 +268,12 @@ function NavigationContent( props: NavigationContentProps ) { - + - + - + - + + handleFormSubmit( { + siteName: siteName || '', + sitePath, + phpVersion: + phpVersion as import('src/lib/wordpress-provider/constants').AllowedPHPVersion, + wpVersion, + useCustomDomain: false, + customDomain: null, + enableHttps: false, + } ) + } canSubmitBlueprint={ !! selectedBlueprint } canSubmitBlueprintDeeplink={ !! selectedBlueprint } - canSubmitBackup={ !! createSiteProps.fileForImport } + canSubmitBackup={ !! fileForImport } canSubmitPullRemote={ !! selectedRemoteSite } canSubmitCreate={ !! canSubmit } /> @@ -348,6 +374,20 @@ export function AddSiteModalContent( { blueprintDeeplinkWarnings, isDeeplinkFlow, setIsDeeplinkFlow, + sitePath, + phpVersion, + setPhpVersion, + wpVersion, + handlePathSelectorClick, + error, + doesPathContainWordPress, + fileForImport, + setFileForImport, + handleSiteNameChange, + existingDomainNames, + setUseCustomDomain, + setCustomDomain, + setEnableHttps, } = addSiteProps; const minimumWordPressVersion = useRootSelector( selectMinimumWordPressVersion ); @@ -392,15 +432,35 @@ export function AddSiteModalContent( { } }, [ isOpen, nameSuggested, loadingSites, initializeForm ] ); - const handleSubmit = useCallback( - async ( event: FormEvent ) => { - event.preventDefault(); + const handleFormSubmit = useCallback( + async ( values: CreateSiteFormValues ) => { + // Sync form values back to hook state before submitting + if ( values.siteName !== siteName ) { + await handleSiteNameChange( values.siteName ); + } + setPhpVersion( values.phpVersion ); + setWpVersion( values.wpVersion ); + setUseCustomDomain( values.useCustomDomain ); + setCustomDomain( values.customDomain ); + setEnableHttps( values.enableHttps ); + onSubmit?.(); await handleAddSiteClick(); speak( siteAddedMessage ); setNameSuggested( false ); }, - [ handleAddSiteClick, siteAddedMessage, onSubmit ] + [ + handleAddSiteClick, + siteAddedMessage, + onSubmit, + siteName, + handleSiteNameChange, + setPhpVersion, + setWpVersion, + setUseCustomDomain, + setCustomDomain, + setEnableHttps, + ] ); return ( @@ -409,13 +469,22 @@ export function AddSiteModalContent( { initialPath={ initialNavigatorPath } > ); From ff2abf82b9c4ef82fa2af8ae55fe789a2f6d94a4 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 11:59:52 +0100 Subject: [PATCH 02/19] Refactor --- src/hooks/tests/use-add-site.test.tsx | 108 ++-- src/hooks/use-add-site.ts | 460 +++++++++--------- .../add-site/components/create-site-form.tsx | 312 ++++++------ .../add-site/components/create-site.tsx | 79 +-- src/modules/add-site/components/index.ts | 5 +- src/modules/add-site/components/stepper.tsx | 38 +- src/modules/add-site/hooks/use-stepper.ts | 9 + src/modules/add-site/index.tsx | 349 ++++++------- 8 files changed, 675 insertions(+), 685 deletions(-) diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index c56433daf6..d1f24d416a 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 111ef2bcfe..20bec38bd3 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,234 +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; - } - if ( ! isEmpty && ! isWordPress ) { - setError( - __( - 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' - ) ); - return; + } catch ( e ) { + Sentry.captureException( e ); } - 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, - existingDomainNames, - 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, - existingDomainNames, - 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 59d1cdec10..0d74b799b4 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -30,32 +30,31 @@ export interface CreateSiteFormValues { enableHttps: boolean; } -export interface PathSelectionFeature { - enabled: true; - onSelectPath: () => void; - doesPathContainWordPress?: boolean; +export interface PathValidationResult { + path: string; + name?: string; + isEmpty: boolean; + isWordPress: boolean; error?: string; } -export interface CustomDomainFeature { - enabled: true; - existingDomainNames?: string[]; -} - -export interface BlueprintVersionsFeature { - enabled: true; - preferredVersions: { php?: string; wp?: string }; -} - -export interface CreateSiteFormFeatures { - pathSelection?: PathSelectionFeature; - customDomain?: CustomDomainFeature; - blueprintVersions?: BlueprintVersionsFeature; -} - export interface CreateSiteFormProps { - features?: CreateSiteFormFeatures; - initialValues?: Partial< CreateSiteFormValues >; + /** 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; } @@ -110,14 +109,6 @@ function FormPathInputComponent( {
-
+ { useCustomDomain && ( + <> +
+ + + { customDomainError && } +
+ +
+ setEnableHttps( e.target.checked ) } + /> + +
+ + { ! 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 2ad85f62fb..266794f5fa 100644 --- a/src/modules/add-site/components/create-site.tsx +++ b/src/modules/add-site/components/create-site.tsx @@ -3,86 +3,51 @@ import { __experimentalHeading as Heading, } from '@wordpress/components'; import { useI18n } from '@wordpress/react-i18n'; -import { useMemo } from 'react'; import { AllowedPHPVersion } from 'src/lib/wordpress-provider/constants'; import { CreateSiteForm, CreateSiteFormValues, - CreateSiteFormFeatures, + PathValidationResult, } from 'src/modules/add-site/components/create-site-form'; export interface CreateSiteProps { - siteName: string | null; - sitePath: string; - phpVersion: string; - wpVersion: string; - onSubmit: ( values: CreateSiteFormValues ) => void; - handlePathSelectorClick: () => void; - error: string; - doesPathContainWordPress: boolean; + 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; } export default function CreateSite( { - siteName, - sitePath, - phpVersion, - wpVersion, - onSubmit, - handlePathSelectorClick, - error, - doesPathContainWordPress, + defaultValues, + onSelectPath, + onSiteNameChange, existingDomainNames = [], blueprintPreferredVersions, + onSubmit, }: CreateSiteProps ) { const { __ } = useI18n(); - const features = useMemo< CreateSiteFormFeatures >( - () => ( { - pathSelection: { - enabled: true, - onSelectPath: handlePathSelectorClick, - doesPathContainWordPress, - error, - }, - customDomain: { - enabled: true, - existingDomainNames, - }, - ...( blueprintPreferredVersions && { - blueprintVersions: { - enabled: true, - preferredVersions: blueprintPreferredVersions, - }, - } ), - } ), - [ - handlePathSelectorClick, - doesPathContainWordPress, - error, - existingDomainNames, - blueprintPreferredVersions, - ] - ); - - const initialValues = useMemo( - () => ( { - siteName: siteName || '', - sitePath, - phpVersion: phpVersion as AllowedPHPVersion, - wpVersion, - } ), - [ siteName, sitePath, phpVersion, wpVersion ] - ); - return ( { __( 'Add a site' ) } - + ); } diff --git a/src/modules/add-site/components/index.ts b/src/modules/add-site/components/index.ts index 1967aff28f..283bc4a6cb 100644 --- a/src/modules/add-site/components/index.ts +++ b/src/modules/add-site/components/index.ts @@ -1,11 +1,8 @@ export { CreateSiteForm } from './create-site-form'; export type { CreateSiteFormValues, - CreateSiteFormFeatures, CreateSiteFormProps, - PathSelectionFeature, - CustomDomainFeature, - BlueprintVersionsFeature, + 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 cd1260ee70..e886b4df35 100644 --- a/src/modules/add-site/components/stepper.tsx +++ b/src/modules/add-site/components/stepper.tsx @@ -18,6 +18,8 @@ interface StepperProps { canSubmitBackup?: boolean; canSubmitPullRemote?: boolean; canSubmitCreate?: boolean; + /** Form ID to use for submit button on create paths */ + createFormId?: string; } export default function Stepper( { @@ -33,9 +35,10 @@ export default function Stepper( { canSubmitBackup, canSubmitPullRemote, canSubmitCreate, + createFormId, }: StepperProps ) { const { __ } = useI18n(); - const { steps, isVisible, actionButton, onSubmit, canSubmit } = useStepper( { + const { steps, isVisible, actionButton, onSubmit, canSubmit, isCreatePath } = useStepper( { onBlueprintContinue, onBlueprintDeeplinkContinue, onBackupContinue, @@ -96,16 +99,29 @@ export default function Stepper( { { __( 'Back' ) } ) } - { actionButton?.isVisible && onSubmit && ( - - ) } + { actionButton?.isVisible && + ( isCreatePath && createFormId ? ( + + ) : ( + onSubmit && ( + + ) + ) ) }
); diff --git a/src/modules/add-site/hooks/use-stepper.ts b/src/modules/add-site/hooks/use-stepper.ts index e3f7241107..4c0ea1e070 100644 --- a/src/modules/add-site/hooks/use-stepper.ts +++ b/src/modules/add-site/hooks/use-stepper.ts @@ -42,6 +42,7 @@ interface UseStepper { onSubmit: () => void; canSubmit: boolean; goTo: ( path: string ) => void; + isCreatePath: boolean; } export function useStepper( config?: StepperConfig ): UseStepper { @@ -239,6 +240,13 @@ export function useStepper( config?: StepperConfig ): UseStepper { } }, [ location.path, config ] ); + const isCreatePath = + location.path === '/create' || + location.path === '/blueprint/select/create' || + location.path === '/blueprint/deeplink/create' || + location.path === '/backup/create' || + location.path === '/pullRemote/create'; + return { steps, isVisible, @@ -246,5 +254,6 @@ export function useStepper( config?: StepperConfig ): UseStepper { onSubmit, canSubmit, goTo, + isCreatePath, }; } diff --git a/src/modules/add-site/index.tsx b/src/modules/add-site/index.tsx index a2c868dd1f..eed9aef9d6 100644 --- a/src/modules/add-site/index.tsx +++ b/src/modules/add-site/index.tsx @@ -3,14 +3,14 @@ import { Navigator, useNavigator } from '@wordpress/components'; import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { BlueprintValidationWarning } from 'common/lib/blueprint-validation'; import Button from 'src/components/button'; import { FullscreenModal } from 'src/components/fullscreen-modal'; -import { useAddSite } from 'src/hooks/use-add-site'; +import { useAddSite, CreateSiteFormValues } from 'src/hooks/use-add-site'; import { useIpcListener } from 'src/hooks/use-ipc-listener'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { generateSiteName } from 'src/lib/generate-site-name'; import { getIpcApi } from 'src/lib/get-ipc-api'; +import { AllowedPHPVersion } from 'src/lib/wordpress-provider/constants'; import { SyncSite } from 'src/modules/sync/types'; import { useRootSelector } from 'src/stores'; import { formatRtkError } from 'src/stores/format-rtk-error'; @@ -25,37 +25,46 @@ import AddSiteOptions, { type AddSiteFlowType } from './components/options'; import { PullRemoteSite } from './components/pull-remote-site'; import Stepper from './components/stepper'; import { useFindAvailableSiteName } from './hooks/use-find-available-site-name'; -import type { CreateSiteFormValues } from './components/create-site-form'; type BlueprintsData = ReturnType< typeof useGetBlueprints >[ 'data' ]; interface NavigationContentProps { blueprintsData: BlueprintsData; isLoadingBlueprints: boolean; - blueprintsErrorMessage?: string | undefined; - siteName: string | null; - sitePath: string; - phpVersion: string; - wpVersion: string; - handlePathSelectorClick: () => void; - error: string; - doesPathContainWordPress: boolean; + blueprintsErrorMessage?: string; + defaultValues: { + siteName: string; + sitePath: string; + phpVersion: AllowedPHPVersion; + wpVersion: string; + }; + onSelectPath: ( currentPath: string ) => Promise< { + path: string; + name?: string; + isEmpty: boolean; + isWordPress: boolean; + error?: string; + } | null >; + onSiteNameChange: ( name: string ) => Promise< { + path: string; + isEmpty: boolean; + isWordPress: boolean; + error?: string; + } >; existingDomainNames: string[]; + onFormSubmit: ( values: CreateSiteFormValues ) => void; + canSubmit: boolean; fileForImport: File | null; setFileForImport: ( file: File | null ) => void; setSelectedBlueprint: ( blueprint?: Blueprint ) => void; selectedBlueprint?: Blueprint; blueprintPreferredVersions?: { php?: string; wp?: string }; setBlueprintPreferredVersions?: ( versions: { php?: string; wp?: string } | undefined ) => void; - blueprintDeeplinkWarnings?: BlueprintValidationWarning[]; + blueprintDeeplinkWarnings?: import('common/lib/blueprint-validation').BlueprintValidationWarning[]; selectedRemoteSite?: SyncSite; setSelectedRemoteSite: ( site?: SyncSite ) => void; isDeeplinkFlow: boolean; setIsDeeplinkFlow: ( isDeeplink: boolean ) => void; - setPhpVersion: ( version: string ) => void; - setWpVersion: ( version: string ) => void; - handleSiteNameChange: ( name: string ) => Promise< void >; - handleFormSubmit: ( values: CreateSiteFormValues ) => void; } function NavigationContent( props: NavigationContentProps ) { @@ -65,6 +74,16 @@ function NavigationContent( props: NavigationContentProps ) { blueprintsData, isLoadingBlueprints, blueprintsErrorMessage, + defaultValues, + onSelectPath, + onSiteNameChange, + existingDomainNames, + onFormSubmit, + canSubmit, + fileForImport, + setFileForImport, + selectedBlueprint, + setSelectedBlueprint, blueprintPreferredVersions, setBlueprintPreferredVersions, blueprintDeeplinkWarnings, @@ -72,22 +91,6 @@ function NavigationContent( props: NavigationContentProps ) { setSelectedRemoteSite, isDeeplinkFlow, setIsDeeplinkFlow, - siteName, - sitePath, - phpVersion, - wpVersion, - handlePathSelectorClick, - error, - doesPathContainWordPress, - existingDomainNames, - fileForImport, - setFileForImport, - selectedBlueprint, - setSelectedBlueprint, - setPhpVersion, - setWpVersion, - handleSiteNameChange, - handleFormSubmit, } = props; useEffect( () => { @@ -132,27 +135,21 @@ function NavigationContent( props: NavigationContentProps ) { }, [ fileForImport, goTo ] ); const findAvailableSiteName = useFindAvailableSiteName(); + const [ remoteSiteName, setRemoteSiteName ] = useState( '' ); + const handlePullRemoteContinue = useCallback( async () => { if ( selectedRemoteSite ) { const availableName = await findAvailableSiteName( selectedRemoteSite.name ); - await handleSiteNameChange( availableName ); + setRemoteSiteName( availableName ); goTo( '/pullRemote/create' ); } - }, [ handleSiteNameChange, findAvailableSiteName, goTo, selectedRemoteSite ] ); + }, [ findAvailableSiteName, goTo, selectedRemoteSite ] ); const blueprints = useMemo( () => blueprintsData?.blueprints.slice().reverse() || [], [ blueprintsData ] ); - const isOnCreatePath = - location.path === '/create' || - location.path === '/blueprint/select/create' || - location.path === '/blueprint/deeplink/create' || - location.path === '/backup/create' || - location.path === '/pullRemote/create'; - const canSubmit = isOnCreatePath && siteName?.trim() && ! error; - const handleBlueprintDeeplinkContinue = useCallback( () => { goTo( '/blueprint/deeplink/create' ); }, [ goTo ] ); @@ -196,59 +193,55 @@ function NavigationContent( props: NavigationContentProps ) { setSelectedRemoteSite, ] ); - const applyBlueprintVersions = useCallback( - ( blueprint?: Blueprint ) => { - if ( blueprint?.blueprint?.preferredVersions ) { - const preferredVersions = blueprint.blueprint.preferredVersions as { - php?: string; - wp?: string; - }; - setBlueprintPreferredVersions?.( preferredVersions ); - - // Apply the preferred versions to the form - if ( preferredVersions.php && preferredVersions.php !== 'latest' ) { - setPhpVersion( preferredVersions.php ); - } - if ( preferredVersions.wp && preferredVersions.wp !== 'latest' ) { - setWpVersion( preferredVersions.wp ); - } - } else { - setBlueprintPreferredVersions?.( undefined ); - } - }, - [ setBlueprintPreferredVersions, setPhpVersion, setWpVersion ] - ); - const handleBlueprintChange = useCallback( ( blueprintId: string ) => { const blueprint = blueprintsData?.blueprints.find( ( b: Blueprint ) => b.slug === blueprintId ); setSelectedBlueprint( blueprint ); - applyBlueprintVersions( blueprint ); + if ( blueprint?.blueprint?.preferredVersions ) { + setBlueprintPreferredVersions?.( + blueprint.blueprint.preferredVersions as { php?: string; wp?: string } + ); + } else { + setBlueprintPreferredVersions?.( undefined ); + } }, - [ blueprintsData?.blueprints, setSelectedBlueprint, applyBlueprintVersions ] + [ blueprintsData?.blueprints, setSelectedBlueprint, setBlueprintPreferredVersions ] ); const handleFileBlueprintSelect = useCallback( ( blueprint: Blueprint ) => { setSelectedBlueprint( blueprint ); - applyBlueprintVersions( blueprint ); + if ( blueprint?.blueprint?.preferredVersions ) { + setBlueprintPreferredVersions?.( + blueprint.blueprint.preferredVersions as { php?: string; wp?: string } + ); + } else { + setBlueprintPreferredVersions?.( undefined ); + } }, - [ setSelectedBlueprint, applyBlueprintVersions ] + [ setSelectedBlueprint, setBlueprintPreferredVersions ] ); - // Common props for CreateSite component - const createSiteCommonProps = { - siteName, - sitePath, - phpVersion, - wpVersion, - handlePathSelectorClick, - error, - doesPathContainWordPress, + // Build default values with blueprint preferred versions applied + const getDefaultValuesWithBlueprint = useCallback( () => { + const values = { ...defaultValues }; + if ( blueprintPreferredVersions?.php && blueprintPreferredVersions.php !== 'latest' ) { + values.phpVersion = blueprintPreferredVersions.php as AllowedPHPVersion; + } + if ( blueprintPreferredVersions?.wp && blueprintPreferredVersions.wp !== 'latest' ) { + values.wpVersion = blueprintPreferredVersions.wp; + } + return values; + }, [ defaultValues, blueprintPreferredVersions ] ); + + // Common props for CreateSite + const createSiteProps = { + onSelectPath, + onSiteNameChange, existingDomainNames, - onSubmit: handleFormSubmit, + onSubmit: onFormSubmit, }; return ( @@ -268,12 +261,13 @@ function NavigationContent( props: NavigationContentProps ) { - + @@ -291,18 +286,19 @@ function NavigationContent( props: NavigationContentProps ) { - + { - setSelectedRemoteSite( remoteSite ); - } } + setSelectedRemoteSite={ setSelectedRemoteSite } /> - + - handleFormSubmit( { - siteName: siteName || '', - sitePath, - phpVersion: - phpVersion as import('src/lib/wordpress-provider/constants').AllowedPHPVersion, - wpVersion, - useCustomDomain: false, - customDomain: null, - enableHttps: false, - } ) - } + createFormId="create-site-form" canSubmitBlueprint={ !! selectedBlueprint } canSubmitBlueprintDeeplink={ !! selectedBlueprint } canSubmitBackup={ !! fileForImport } canSubmitPullRemote={ !! selectedRemoteSite } - canSubmitCreate={ !! canSubmit } + canSubmitCreate={ canSubmit } /> ); @@ -347,7 +332,9 @@ export function AddSiteModalContent( { addSiteProps, }: AddSiteModalContentProps ) { const { __ } = useI18n(); - const [ nameSuggested, setNameSuggested ] = useState( false ); + const [ formInitialized, setFormInitialized ] = useState( false ); + const [ defaultSiteName, setDefaultSiteName ] = useState( '' ); + const [ defaultSitePath, setDefaultSitePath ] = useState( '' ); const { data: blueprintsData, @@ -358,36 +345,26 @@ export function AddSiteModalContent( { const { sites, loadingSites } = useSiteDetails(); const { - handleAddSiteClick, - siteName, - setSiteName, - setWpVersion, - setProposedSitePath, - setDoesPathContainWordPress, - loadAllCustomDomains, + handleCreateSite, + selectPath, + generateProposedPath, + defaultPhpVersion, + defaultWpVersion, + deeplinkPhpVersion, + deeplinkWpVersion, + fileForImport, + setFileForImport, selectedBlueprint, setSelectedBlueprint, - selectedRemoteSite, - setSelectedRemoteSite, blueprintPreferredVersions, setBlueprintPreferredVersions, blueprintDeeplinkWarnings, + selectedRemoteSite, + setSelectedRemoteSite, + existingDomainNames, + loadAllCustomDomains, isDeeplinkFlow, setIsDeeplinkFlow, - sitePath, - phpVersion, - setPhpVersion, - wpVersion, - handlePathSelectorClick, - error, - doesPathContainWordPress, - fileForImport, - setFileForImport, - handleSiteNameChange, - existingDomainNames, - setUseCustomDomain, - setCustomDomain, - setEnableHttps, } = addSiteProps; const minimumWordPressVersion = useRootSelector( selectMinimumWordPressVersion ); @@ -398,93 +375,88 @@ export function AddSiteModalContent( { const initialNavigatorPath = selectedBlueprint ? '/blueprint/deeplink' : '/'; - const siteAddedMessage = sprintf( - // translators: %s is the site name. - __( '%s site added.' ), - siteName || '' - ); + // Initialize form with generated site name and path + useEffect( () => { + const initializeForm = async () => { + if ( ! isOpen || formInitialized || loadingSites ) return; - const initializeForm = useCallback( async () => { - const generatedSiteName = await generateSiteName( sites ); - const { path, name, isWordPress } = - await getIpcApi().generateProposedSitePath( generatedSiteName ); - if ( latestStableVersion ) { - setWpVersion( latestStableVersion.value ); - } - setNameSuggested( true ); - setSiteName( name ); - setProposedSitePath( path ); - setDoesPathContainWordPress( isWordPress ); - loadAllCustomDomains(); - }, [ - sites, - setSiteName, - setProposedSitePath, - setDoesPathContainWordPress, - setWpVersion, - latestStableVersion, - loadAllCustomDomains, - ] ); + const generatedSiteName = await generateSiteName( sites ); + const { path } = await getIpcApi().generateProposedSitePath( generatedSiteName ); + setDefaultSiteName( generatedSiteName ); + setDefaultSitePath( path ); + setFormInitialized( true ); + loadAllCustomDomains(); + }; + + void initializeForm(); + }, [ isOpen, formInitialized, loadingSites, sites, loadAllCustomDomains ] ); + + // Reset form initialized state when modal closes useEffect( () => { - if ( isOpen && ! nameSuggested && ! loadingSites ) { - void initializeForm(); + if ( ! isOpen ) { + setFormInitialized( false ); } - }, [ isOpen, nameSuggested, loadingSites, initializeForm ] ); + }, [ isOpen ] ); + + const defaultValues = useMemo( + () => ( { + siteName: defaultSiteName, + sitePath: defaultSitePath, + phpVersion: isDeeplinkFlow ? deeplinkPhpVersion : defaultPhpVersion, + wpVersion: isDeeplinkFlow + ? deeplinkWpVersion + : latestStableVersion?.value ?? defaultWpVersion, + } ), + [ + defaultSiteName, + defaultSitePath, + defaultPhpVersion, + defaultWpVersion, + deeplinkPhpVersion, + deeplinkWpVersion, + isDeeplinkFlow, + latestStableVersion, + ] + ); const handleFormSubmit = useCallback( async ( values: CreateSiteFormValues ) => { - // Sync form values back to hook state before submitting - if ( values.siteName !== siteName ) { - await handleSiteNameChange( values.siteName ); - } - setPhpVersion( values.phpVersion ); - setWpVersion( values.wpVersion ); - setUseCustomDomain( values.useCustomDomain ); - setCustomDomain( values.customDomain ); - setEnableHttps( values.enableHttps ); + const siteAddedMessage = sprintf( + // translators: %s is the site name. + __( '%s site added.' ), + values.siteName + ); onSubmit?.(); - await handleAddSiteClick(); + await handleCreateSite( values ); speak( siteAddedMessage ); - setNameSuggested( false ); }, - [ - handleAddSiteClick, - siteAddedMessage, - onSubmit, - siteName, - handleSiteNameChange, - setPhpVersion, - setWpVersion, - setUseCustomDomain, - setCustomDomain, - setEnableHttps, - ] + [ __, handleCreateSite, onSubmit ] ); + // canSubmit is true if the form is initialized (we have default values) + const canSubmit = formInitialized && defaultSiteName.trim().length > 0; + return ( ); From 782aa40e4a2a869e69347c0f0def7c27fc1d45db Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 15:12:42 +0100 Subject: [PATCH 03/19] Fix stepper --- .../add-site/components/create-site-form.tsx | 7 +++- .../add-site/components/create-site.tsx | 4 ++ src/modules/add-site/components/stepper.tsx | 39 ++++++------------- src/modules/add-site/hooks/use-stepper.ts | 9 ----- src/modules/add-site/index.tsx | 10 ++++- 5 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 0d74b799b4..d6f5459b76 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -3,7 +3,7 @@ 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, useCallback, useMemo } from 'react'; +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'; @@ -56,6 +56,8 @@ export interface CreateSiteFormProps { 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 { @@ -152,6 +154,7 @@ export const CreateSiteForm = ( { existingDomainNames = [], blueprintPreferredVersions, onSubmit, + formRef, }: CreateSiteFormProps ) => { const { __, isRTL } = useI18n(); const locale = useI18nLocale(); @@ -290,7 +293,7 @@ export const CreateSiteForm = ( { const showAdvancedSettings = onSelectPath !== undefined; return ( - +
); diff --git a/src/modules/add-site/hooks/use-stepper.ts b/src/modules/add-site/hooks/use-stepper.ts index 4c0ea1e070..e3f7241107 100644 --- a/src/modules/add-site/hooks/use-stepper.ts +++ b/src/modules/add-site/hooks/use-stepper.ts @@ -42,7 +42,6 @@ interface UseStepper { onSubmit: () => void; canSubmit: boolean; goTo: ( path: string ) => void; - isCreatePath: boolean; } export function useStepper( config?: StepperConfig ): UseStepper { @@ -240,13 +239,6 @@ export function useStepper( config?: StepperConfig ): UseStepper { } }, [ location.path, config ] ); - const isCreatePath = - location.path === '/create' || - location.path === '/blueprint/select/create' || - location.path === '/blueprint/deeplink/create' || - location.path === '/backup/create' || - location.path === '/pullRemote/create'; - return { steps, isVisible, @@ -254,6 +246,5 @@ export function useStepper( config?: StepperConfig ): UseStepper { onSubmit, canSubmit, goTo, - isCreatePath, }; } diff --git a/src/modules/add-site/index.tsx b/src/modules/add-site/index.tsx index eed9aef9d6..c85bd54a70 100644 --- a/src/modules/add-site/index.tsx +++ b/src/modules/add-site/index.tsx @@ -2,7 +2,7 @@ import { speak } from '@wordpress/a11y'; import { Navigator, useNavigator } from '@wordpress/components'; import { sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Button from 'src/components/button'; import { FullscreenModal } from 'src/components/fullscreen-modal'; import { useAddSite, CreateSiteFormValues } from 'src/hooks/use-add-site'; @@ -236,12 +236,16 @@ function NavigationContent( props: NavigationContentProps ) { return values; }, [ defaultValues, blueprintPreferredVersions ] ); + // Ref for programmatic form submission + const formRef = useRef< HTMLFormElement >( null ); + // Common props for CreateSite const createSiteProps = { onSelectPath, onSiteNameChange, existingDomainNames, onSubmit: onFormSubmit, + formRef, }; return ( @@ -307,7 +311,9 @@ function NavigationContent( props: NavigationContentProps ) { onBlueprintDeeplinkContinue={ handleBlueprintDeeplinkContinue } onBackupContinue={ handleBackupContinue } onPullRemoteContinue={ handlePullRemoteContinue } - createFormId="create-site-form" + onCreateSubmit={ () => { + formRef.current?.requestSubmit(); + } } canSubmitBlueprint={ !! selectedBlueprint } canSubmitBlueprintDeeplink={ !! selectedBlueprint } canSubmitBackup={ !! fileForImport } From 745d7a44aa3e580634c7b6d41bb03a58fd2a459b Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 15:14:40 +0100 Subject: [PATCH 04/19] Ensure we reset the setRemoteSiteName --- src/modules/add-site/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/add-site/index.tsx b/src/modules/add-site/index.tsx index c85bd54a70..6538fb14fa 100644 --- a/src/modules/add-site/index.tsx +++ b/src/modules/add-site/index.tsx @@ -162,6 +162,7 @@ function NavigationContent( props: NavigationContentProps ) { } else if ( location.path === '/backup/create' ) { goTo( '/backup' ); } else if ( location.path === '/pullRemote/create' ) { + setRemoteSiteName( '' ); goTo( '/pullRemote' ); } else if ( location.path === '/backup' || @@ -179,6 +180,7 @@ function NavigationContent( props: NavigationContentProps ) { } if ( location.path === '/pullRemote' ) { setSelectedRemoteSite( undefined ); + setRemoteSiteName( '' ); } goTo( '/' ); } else { From be048ba8428f8defe23da600fb40b066e820dc49 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 15:17:24 +0100 Subject: [PATCH 05/19] Add useEffect --- .../add-site/components/create-site-form.tsx | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index d6f5459b76..577a4ea091 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -162,7 +162,6 @@ export const CreateSiteForm = ( { const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion ); const allowedPhpVersions = useRootSelector( selectAllowedPhpVersions ); - // Form state - fully owned by this component const [ siteName, setSiteName ] = useState( defaultValues.siteName ?? '' ); const [ sitePath, setSitePath ] = useState( defaultValues.sitePath ?? '' ); const [ phpVersion, setPhpVersion ] = useState< AllowedPHPVersion >( @@ -175,14 +174,39 @@ export const CreateSiteForm = ( { const [ customDomain, setCustomDomain ] = useState< string | null >( null ); const [ enableHttps, setEnableHttps ] = useState( false ); - // Validation state const [ pathError, setPathError ] = useState( '' ); const [ doesPathContainWordPress, setDoesPathContainWordPress ] = useState( false ); const [ customDomainError, setCustomDomainError ] = useState( '' ); - // UI state const [ isAdvancedSettingsVisible, setAdvancedSettingsVisible ] = useState( false ); + // Sync form state with defaultValues when they change + // This handles cases where defaultValues are updated after initial mount + // (e.g., blueprint preferred versions loading after form mount) + useEffect( () => { + if ( defaultValues.siteName !== undefined ) { + setSiteName( defaultValues.siteName ); + } + }, [ defaultValues.siteName ] ); + + useEffect( () => { + if ( defaultValues.sitePath !== undefined ) { + setSitePath( defaultValues.sitePath ); + } + }, [ defaultValues.sitePath ] ); + + useEffect( () => { + if ( defaultValues.phpVersion !== undefined ) { + setPhpVersion( defaultValues.phpVersion ); + } + }, [ defaultValues.phpVersion ] ); + + useEffect( () => { + if ( defaultValues.wpVersion !== undefined ) { + setWpVersion( defaultValues.wpVersion ); + } + }, [ defaultValues.wpVersion ] ); + // If the custom domain is enabled and the root certificate is trusted, enable HTTPS useEffect( () => { if ( useCustomDomain && isCertificateTrusted ) { @@ -190,7 +214,6 @@ export const CreateSiteForm = ( { } }, [ useCustomDomain, isCertificateTrusted ] ); - // Handle site name change - generate proposed path const handleSiteNameChange = useCallback( async ( name: string ) => { setSiteName( name ); @@ -211,7 +234,6 @@ export const CreateSiteForm = ( { [ onSiteNameChange, sitePath ] ); - // Handle path selection const handleSelectPath = useCallback( async () => { if ( ! onSelectPath ) return; @@ -226,13 +248,11 @@ export const CreateSiteForm = ( { } setDoesPathContainWordPress( ! result.isEmpty && result.isWordPress ); - // If we got a name from the path and don't have one, use it if ( result.name && ! siteName ) { setSiteName( result.name ); } }, [ onSelectPath, sitePath, siteName ] ); - // Handle custom domain change const handleCustomDomainChange = useCallback( ( value: string ) => { setCustomDomain( value ); @@ -243,7 +263,6 @@ export const CreateSiteForm = ( { [ useCustomDomain, existingDomainNames ] ); - // Build form values const formValues = useMemo< CreateSiteFormValues >( () => ( { siteName, @@ -257,7 +276,6 @@ export const CreateSiteForm = ( { [ siteName, sitePath, phpVersion, wpVersion, useCustomDomain, customDomain, enableHttps ] ); - // Handle form submit const handleFormSubmit = useCallback( ( event: FormEvent ) => { event.preventDefault(); From 1515dfbce35918bdc89d39529b02de78789e6d19 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Tue, 23 Dec 2025 15:42:06 +0100 Subject: [PATCH 06/19] Adjust path auto-generation --- src/modules/add-site/components/create-site-form.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 577a4ea091..1a7b50d3bc 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -177,6 +177,8 @@ export const CreateSiteForm = ( { const [ pathError, setPathError ] = useState( '' ); const [ doesPathContainWordPress, setDoesPathContainWordPress ] = useState( false ); const [ customDomainError, setCustomDomainError ] = useState( '' ); + // Track if user has manually selected a path via folder picker + const [ hasCustomPath, setHasCustomPath ] = useState( false ); const [ isAdvancedSettingsVisible, setAdvancedSettingsVisible ] = useState( false ); @@ -218,8 +220,8 @@ export const CreateSiteForm = ( { async ( name: string ) => { setSiteName( name ); - // Only generate path if we don't have a custom path set - if ( onSiteNameChange && ! sitePath ) { + // Only generate path if user hasn't manually selected a custom path + if ( onSiteNameChange && ! hasCustomPath ) { const result = await onSiteNameChange( name ); if ( result.error ) { setPathError( result.error ); @@ -227,11 +229,10 @@ export const CreateSiteForm = ( { setPathError( '' ); } setDoesPathContainWordPress( ! result.isEmpty && result.isWordPress ); - // Store the proposed path setSitePath( result.path ); } }, - [ onSiteNameChange, sitePath ] + [ onSiteNameChange, hasCustomPath ] ); const handleSelectPath = useCallback( async () => { @@ -240,6 +241,8 @@ export const CreateSiteForm = ( { const result = await onSelectPath( sitePath ); if ( ! result ) return; + // Mark that user has manually selected a path + setHasCustomPath( true ); setSitePath( result.path ); if ( result.error ) { setPathError( result.error ); From 14cf07a5932f39eceae561d481165e4302be2b9d Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 13:09:49 +0100 Subject: [PATCH 07/19] Fix comments --- src/modules/add-site/components/create-site-form.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 1a7b50d3bc..3393616d6f 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -39,7 +39,11 @@ export interface PathValidationResult { } export interface CreateSiteFormProps { - /** Default values for form initialization (only used once on mount) */ + /** + * Default values for form initialization. + * The form will sync with these values when they change after mount + * (e.g., when blueprint preferred versions load asynchronously). + */ defaultValues?: { siteName?: string; sitePath?: string; From cf4e5230ed0f77a71653d7ccd0187062268ec0a0 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 13:13:16 +0100 Subject: [PATCH 08/19] Move form values to single source of truth --- .../add-site/components/create-site-form.tsx | 19 +------------------ .../add-site/components/create-site.tsx | 7 ++----- src/modules/add-site/components/index.ts | 7 ++----- 3 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 3393616d6f..f5b9dedff5 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -19,24 +19,7 @@ import { selectDefaultWordPressVersion, 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; -} +import type { CreateSiteFormValues, PathValidationResult } from 'src/hooks/use-add-site'; export interface CreateSiteFormProps { /** diff --git a/src/modules/add-site/components/create-site.tsx b/src/modules/add-site/components/create-site.tsx index 0f65a5f5a9..4962e2f5a6 100644 --- a/src/modules/add-site/components/create-site.tsx +++ b/src/modules/add-site/components/create-site.tsx @@ -5,11 +5,8 @@ import { import { useI18n } from '@wordpress/react-i18n'; 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'; +import { CreateSiteForm } from 'src/modules/add-site/components/create-site-form'; +import type { CreateSiteFormValues, PathValidationResult } from 'src/hooks/use-add-site'; export interface CreateSiteProps { defaultValues?: { diff --git a/src/modules/add-site/components/index.ts b/src/modules/add-site/components/index.ts index 283bc4a6cb..3fe8b0fdd2 100644 --- a/src/modules/add-site/components/index.ts +++ b/src/modules/add-site/components/index.ts @@ -1,8 +1,5 @@ export { CreateSiteForm } from './create-site-form'; -export type { - CreateSiteFormValues, - CreateSiteFormProps, - PathValidationResult, -} from './create-site-form'; +export type { CreateSiteFormProps } from './create-site-form'; +export type { CreateSiteFormValues, PathValidationResult } from 'src/hooks/use-add-site'; export { default as CreateSite } from './create-site'; export type { CreateSiteProps } from './create-site'; From f0122c064b198efa61b2186988260d133d6d4b0d Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 13:15:23 +0100 Subject: [PATCH 09/19] Apply useMemo --- src/modules/add-site/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/add-site/index.tsx b/src/modules/add-site/index.tsx index 6538fb14fa..2d94367959 100644 --- a/src/modules/add-site/index.tsx +++ b/src/modules/add-site/index.tsx @@ -227,7 +227,7 @@ function NavigationContent( props: NavigationContentProps ) { ); // Build default values with blueprint preferred versions applied - const getDefaultValuesWithBlueprint = useCallback( () => { + const defaultValuesWithBlueprint = useMemo( () => { const values = { ...defaultValues }; if ( blueprintPreferredVersions?.php && blueprintPreferredVersions.php !== 'latest' ) { values.phpVersion = blueprintPreferredVersions.php as AllowedPHPVersion; @@ -268,7 +268,7 @@ function NavigationContent( props: NavigationContentProps ) { @@ -284,7 +284,7 @@ function NavigationContent( props: NavigationContentProps ) { From e89260807046596a991d050effd6165c3fc68b98 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 13:42:07 +0100 Subject: [PATCH 10/19] Ensure we always display custom domains --- .../add-site/components/create-site-form.tsx | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index f5b9dedff5..1853ad5ad3 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -442,23 +442,19 @@ export const CreateSiteForm = ( { ) } - { existingDomainNames.length >= 0 && ( - <> -
- setUseCustomDomain( e.target.checked ) } - /> - -
+
+ setUseCustomDomain( e.target.checked ) } + /> + +
-
- { __( 'Your system password will be required to set up the domain.' ) } -
- - ) } +
+ { __( 'Your system password will be required to set up the domain.' ) } +
{ useCustomDomain && ( <> From df7087c4e93347e23010ed74e70f14b6f3ff75f0 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 14:46:23 +0100 Subject: [PATCH 11/19] Fix domains --- src/modules/add-site/components/create-site-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 1853ad5ad3..64c2503d89 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -245,7 +245,7 @@ export const CreateSiteForm = ( { const handleCustomDomainChange = useCallback( ( value: string ) => { - setCustomDomain( value ); + setCustomDomain( value || null ); setCustomDomainError( getDomainNameValidationError( useCustomDomain, value, existingDomainNames ) ); From a8efdf6c4b245073bd2197d16cbdafcf88fa40c4 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:11:00 +0100 Subject: [PATCH 12/19] Do custom domain validation --- src/modules/add-site/components/create-site-form.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 64c2503d89..849cf71c0f 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -203,6 +203,17 @@ export const CreateSiteForm = ( { } }, [ useCustomDomain, isCertificateTrusted ] ); + // Validate custom domain when useCustomDomain changes + useEffect( () => { + if ( useCustomDomain ) { + setCustomDomainError( + getDomainNameValidationError( useCustomDomain, customDomain, existingDomainNames ) + ); + } else { + setCustomDomainError( '' ); + } + }, [ useCustomDomain, customDomain, existingDomainNames ] ); + const handleSiteNameChange = useCallback( async ( name: string ) => { setSiteName( name ); From 09dca268f647068713d1541bc006b0c45d501bab Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:15:21 +0100 Subject: [PATCH 13/19] Fix tests --- src/modules/add-site/tests/add-site.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/modules/add-site/tests/add-site.test.tsx b/src/modules/add-site/tests/add-site.test.tsx index df356f45f0..9b7763b30d 100644 --- a/src/modules/add-site/tests/add-site.test.tsx +++ b/src/modules/add-site/tests/add-site.test.tsx @@ -225,7 +225,10 @@ describe( 'AddSite', () => { await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); await user.click( screen.getByTestId( 'select-path-button' ) ); - expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' ); + expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( + 'Choose folder for site', + '/default_path/my-wordpress-website' + ); const dialog = screen.getByRole( 'dialog' ); const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } ); await user.click( addSiteButton ); @@ -270,7 +273,10 @@ describe( 'AddSite', () => { await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); await user.click( screen.getByTestId( 'select-path-button' ) ); - expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' ); + expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( + 'Choose folder for site', + '/default_path/my-wordpress-website' + ); await waitFor( () => { const dialog = screen.getByRole( 'dialog' ); @@ -309,7 +315,10 @@ describe( 'AddSite', () => { await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); await user.click( screen.getByTestId( 'select-path-button' ) ); - expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' ); + expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( + 'Choose folder for site', + '/default_path/my-wordpress-website' + ); await waitFor( () => { const dialog = screen.getByRole( 'dialog' ); From afcf9c922c2d857f88f1ed02e4ca33107a67e1af Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:19:47 +0100 Subject: [PATCH 14/19] Remove existing domains from dependency array --- src/modules/add-site/components/create-site-form.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 849cf71c0f..5b4e6559d1 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -203,7 +203,8 @@ export const CreateSiteForm = ( { } }, [ useCustomDomain, isCertificateTrusted ] ); - // Validate custom domain when useCustomDomain changes + // Validate custom domain when useCustomDomain or customDomain changes + // Note: existingDomainNames is intentionally not in deps to avoid re-validation when the list loads useEffect( () => { if ( useCustomDomain ) { setCustomDomainError( @@ -212,7 +213,8 @@ export const CreateSiteForm = ( { } else { setCustomDomainError( '' ); } - }, [ useCustomDomain, customDomain, existingDomainNames ] ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ useCustomDomain, customDomain ] ); const handleSiteNameChange = useCallback( async ( name: string ) => { From 7d5c342607d1a497b6b55df603b49c5f05240e55 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:25:43 +0100 Subject: [PATCH 15/19] Cleanup --- src/hooks/use-add-site.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 20bec38bd3..b5513a05d4 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -59,7 +59,6 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion ); const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion ); - // Only keep state that's NOT part of the form const [ fileForImport, setFileForImport ] = useState< File | null >( null ); const [ selectedBlueprint, setSelectedBlueprint ] = useState< Blueprint | undefined >(); const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | undefined >(); @@ -294,46 +293,27 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { 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, - - // Blueprint selection selectedBlueprint, setSelectedBlueprint, blueprintPreferredVersions, setBlueprintPreferredVersions, blueprintDeeplinkWarnings, - - // Remote site selection selectedRemoteSite, setSelectedRemoteSite, - - // Custom domain helpers existingDomainNames, loadAllCustomDomains, - - // Flow state isDeeplinkFlow, setIsDeeplinkFlow, isAnySiteProcessing, - - // Reset resetForm, clearDeeplinkState, } ), From e28bc181984e5f5aec91d6db9c8d8aab86256a00 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:27:07 +0100 Subject: [PATCH 16/19] Simplify useEffect --- .../add-site/components/create-site-form.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index 5b4e6559d1..c140136f25 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -176,25 +176,21 @@ export const CreateSiteForm = ( { if ( defaultValues.siteName !== undefined ) { setSiteName( defaultValues.siteName ); } - }, [ defaultValues.siteName ] ); - - useEffect( () => { if ( defaultValues.sitePath !== undefined ) { setSitePath( defaultValues.sitePath ); } - }, [ defaultValues.sitePath ] ); - - useEffect( () => { if ( defaultValues.phpVersion !== undefined ) { setPhpVersion( defaultValues.phpVersion ); } - }, [ defaultValues.phpVersion ] ); - - useEffect( () => { if ( defaultValues.wpVersion !== undefined ) { setWpVersion( defaultValues.wpVersion ); } - }, [ defaultValues.wpVersion ] ); + }, [ + defaultValues.siteName, + defaultValues.sitePath, + defaultValues.phpVersion, + defaultValues.wpVersion, + ] ); // If the custom domain is enabled and the root certificate is trusted, enable HTTPS useEffect( () => { From c4d0b679b356cfd8641e49db2caf31733e13e26f Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 29 Dec 2025 15:28:50 +0100 Subject: [PATCH 17/19] Fix removed comment --- src/modules/add-site/components/create-site-form.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/add-site/components/create-site-form.tsx b/src/modules/add-site/components/create-site-form.tsx index c140136f25..228fdbeb95 100644 --- a/src/modules/add-site/components/create-site-form.tsx +++ b/src/modules/add-site/components/create-site-form.tsx @@ -98,6 +98,14 @@ function FormPathInputComponent( {