Skip to content
72 changes: 72 additions & 0 deletions apps/studio/src/components/form-path-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n';
import { useI18n } from '@wordpress/react-i18n';
import FolderIcon from 'src/components/folder-icon';
import { cx } from 'src/lib/cx';
import { SiteFormError } from './site-form-error';

export interface FormPathInputComponentProps {
value: string;
onClick: () => void;
error?: string;
doesPathContainWordPress: boolean;
id?: string;
}

export function FormPathInputComponent( {
value,
onClick,
error,
doesPathContainWordPress,
id,
}: FormPathInputComponentProps ) {
const { __ } = useI18n();
return (
<div className="flex flex-col gap-2">
<button
aria-invalid={ !! error }
/**
* The below `aria-describedby` presumes the error message always
* relates to the local path input, which is true currently as it is the
* only data validation in place. If we ever introduce additional data
* validation we need to expand the robustness of this
* `aria-describedby` attribute so that it only targets relevant error
* messages.
*/
aria-describedby={ error ? 'site-path-error' : undefined }
type="button"
aria-label={ `${ value }, ${ __( 'Select different local path' ) }` }
className={ cx(
'flex flex-row items-stretch rounded-sm border border-[#949494] focus:border-a8c-blue-50 focus:shadow-[0_0_0_0.5px_black] focus:shadow-a8c-blue-50 outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-a8c-blue-50 [&:disabled]:cursor-not-allowed',
error && 'border-red-500 [&_.local-path-icon]:border-l-red-500'
) }
data-testid="select-path-button"
onClick={ onClick }
id={ id }
>
<div
aria-hidden="true"
tabIndex={ -1 }
className="w-full text-left pl-3 py-3 min-h-10"
onChange={ () => {} }
>
{ value }
</div>
<div
aria-hidden="true"
className="local-path-icon flex items-center py-[9px] px-2.5 self-center"
>
<FolderIcon className="text-[#3C434A]" />
</div>
</button>
<SiteFormError
error={ error }
tipMessage={
doesPathContainWordPress
? __( 'The existing WordPress site at this path will be added.' )
: ''
}
/>
<input type="hidden" data-testid="local-path-input" value={ value } />
</div>
);
}
35 changes: 35 additions & 0 deletions apps/studio/src/components/site-form-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Icon } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { cautionFilled, tip } from '@wordpress/icons';
import { cx } from 'src/lib/cx';

export interface SiteFormErrorProps {
error?: string;
tipMessage?: string;
className?: string;
}

export const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => {
return (
( error || tipMessage ) && (
<div
id={ error ? 'error-message' : 'tip-message' }
role="alert"
aria-atomic="true"
className={ cx(
'flex items-start gap-1 text-xs',
error ? 'text-red-500' : 'text-a8c-gray-50',
className
) }
>
<Icon
className={ cx( 'shrink-0 basis-4', error ? 'fill-red-500' : 'fill-a8c-gray-50' ) }
icon={ error ? cautionFilled : tip }
width={ 16 }
height={ 16 }
/>
<p>{ error ? error : __( tipMessage ) }</p>
</div>
)
);
};
25 changes: 17 additions & 8 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-setting
import { getUserEditor, getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers';
import { winFindEditorPath } from 'src/modules/user-settings/lib/win-editor-path';
import { SiteServer, stopAllServers as triggerStopAllServers } from 'src/site-server';
import { DEFAULT_SITE_PATH, getSiteThumbnailPath } from 'src/storage/paths';
import { getSiteThumbnailPath, resolveDefaultSiteDirectory } from 'src/storage/paths';
import {
loadUserData,
lockAppdata,
Expand Down Expand Up @@ -140,6 +140,8 @@ export {
saveUserLocale,
saveUserTerminal,
showUserSettings,
getDefaultSiteDirectory,
saveDefaultSiteDirectory,
} from 'src/modules/user-settings/lib/ipc-handlers';

export async function getAgentInstructionsStatus(
Expand Down Expand Up @@ -611,9 +613,11 @@ export async function showSaveAsDialog( event: IpcMainInvokeEvent, options: Save
throw new Error( `No window found for sender of showSaveAsDialog message: ${ event.frameId }` );
}

const defaultSiteDirectory = await resolveDefaultSiteDirectory();
const defaultPath =
options.defaultPath === nodePath.basename( options.defaultPath ?? '' )
? nodePath.join( DEFAULT_SITE_PATH, options.defaultPath )
typeof options.defaultPath === 'string' &&
options.defaultPath === nodePath.basename( options.defaultPath )
? nodePath.join( defaultSiteDirectory, options.defaultPath )
: options.defaultPath;
const { canceled, filePath } = await dialog.showSaveDialog( parentWindow, {
defaultPath,
Expand Down Expand Up @@ -648,9 +652,10 @@ export async function showOpenFolderDialog(
};
}

const defaultSiteDirectory = await resolveDefaultSiteDirectory();
const { canceled, filePaths } = await dialog.showOpenDialog( parentWindow, {
title,
defaultPath: defaultDialogPath !== '' ? defaultDialogPath : DEFAULT_SITE_PATH,
defaultPath: defaultDialogPath !== '' ? defaultDialogPath : defaultSiteDirectory,
properties: [
'openDirectory',
'createDirectory', // allow user to create new directories; macOS only
Expand Down Expand Up @@ -694,7 +699,8 @@ export async function copySite(
}
const sourceSite = sourceServer.details;

const finalSitePath = nodePath.join( DEFAULT_SITE_PATH, sanitizeFolderName( siteName ) );
const defaultSiteDirectory = await resolveDefaultSiteDirectory();
const finalSitePath = nodePath.join( defaultSiteDirectory, sanitizeFolderName( siteName ) );

console.log( `Copying site '${ sourceSite.name }' to '${ siteName }'` );

Expand Down Expand Up @@ -892,7 +898,8 @@ export async function generateProposedSitePath(
_event: IpcMainInvokeEvent,
siteName: string
): Promise< FolderDialogResponse > {
const path = nodePath.join( DEFAULT_SITE_PATH, sanitizeFolderName( siteName ) );
const defaultSiteDirectory = await resolveDefaultSiteDirectory();
const path = nodePath.join( defaultSiteDirectory, sanitizeFolderName( siteName ) );

try {
return {
Expand Down Expand Up @@ -927,9 +934,10 @@ export async function generateSiteNameFromList(
_event: IpcMainInvokeEvent,
usedSites: SiteDetails[]
): Promise< string > {
const defaultSiteDirectory = await resolveDefaultSiteDirectory();
return generateSiteName(
usedSites.map( ( s ) => s.name ),
DEFAULT_SITE_PATH
defaultSiteDirectory
);
}

Expand All @@ -938,10 +946,11 @@ export async function generateNumberedNameFromList(
baseName: string,
usedSites: SiteDetails[]
): Promise< string > {
const defaultSiteDirectory = await resolveDefaultSiteDirectory();
return generateNumberedName(
baseName,
usedSites.map( ( s ) => s.name ),
DEFAULT_SITE_PATH
defaultSiteDirectory
);
}

Expand Down
101 changes: 2 additions & 99 deletions apps/studio/src/modules/add-site/components/create-site-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import { tip, cautionFilled, chevronRight, chevronDown, chevronLeft } from '@wor
import { useI18n } from '@wordpress/react-i18n';
import { FormEvent, useState, useEffect, useCallback, useMemo, useRef, RefObject } from 'react';
import Button from 'src/components/button';
import FolderIcon from 'src/components/folder-icon';
import { FormPathInputComponent } from 'src/components/form-path-input';
import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more';
import PasswordControl from 'src/components/password-control';
import { SiteFormError } from 'src/components/site-form-error';
import TextControlComponent from 'src/components/text-control';
import { WPVersionSelector } from 'src/components/wp-version-selector';
import { cx } from 'src/lib/cx';
Expand Down Expand Up @@ -58,104 +59,6 @@ interface CreateSiteFormProps {
formRef?: RefObject< HTMLFormElement >;
}

interface FormPathInputComponentProps {
value: string;
onClick: () => void;
error?: string;
doesPathContainWordPress: boolean;
id?: string;
}

interface SiteFormErrorProps {
error?: string;
tipMessage?: string;
className?: string;
}

const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => {
return (
( error || tipMessage ) && (
<div
id={ error ? 'error-message' : 'tip-message' }
role="alert"
aria-atomic="true"
className={ cx(
'flex items-start gap-1 text-xs',
error ? 'text-red-500' : 'text-a8c-gray-50',
className
) }
>
<Icon
className={ cx( 'shrink-0 basis-4', error ? 'fill-red-500' : 'fill-a8c-gray-50' ) }
icon={ error ? cautionFilled : tip }
width={ 16 }
height={ 16 }
/>
<p>{ error ? error : __( tipMessage ) }</p>
</div>
)
);
};

function FormPathInputComponent( {
value,
onClick,
error,
doesPathContainWordPress,
id,
}: FormPathInputComponentProps ) {
const { __ } = useI18n();
return (
<div className="flex flex-col gap-2">
<button
aria-invalid={ !! error }
/**
* The below `aria-describedby` presumes the error message always
* relates to the local path input, which is true currently as it is the
* only data validation in place. If we ever introduce additional data
* validation we need to expand the robustness of this
* `aria-describedby` attribute so that it only targets relevant error
* messages.
*/
aria-describedby={ error ? 'site-path-error' : undefined }
type="button"
aria-label={ `${ value }, ${ __( 'Select different local path' ) }` }
className={ cx(
'flex flex-row items-stretch rounded-sm border border-[#949494] focus:border-a8c-blue-50 focus:shadow-[0_0_0_0.5px_black] focus:shadow-a8c-blue-50 outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-a8c-blue-50 [&:disabled]:cursor-not-allowed',
error && 'border-red-500 [&_.local-path-icon]:border-l-red-500'
) }
data-testid="select-path-button"
onClick={ onClick }
id={ id }
>
<div
aria-hidden="true"
tabIndex={ -1 }
className="w-full text-left pl-3 py-3 min-h-10"
onChange={ () => {} }
>
{ value }
</div>
<div
aria-hidden="true"
className="local-path-icon flex items-center py-[9px] px-2.5 self-center"
>
<FolderIcon className="text-[#3C434A]" />
</div>
</button>
<SiteFormError
error={ error }
tipMessage={
doesPathContainWordPress
? __( 'The existing WordPress site at this path will be added.' )
: ''
}
/>
<input type="hidden" data-testid="local-path-input" value={ value } />
</div>
);
}

export const CreateSiteForm = ( {
defaultValues = {},
onSelectPath,
Expand Down
Loading
Loading