Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions apps/studio/src/components/form-path-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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;
tipMessage?: string;
id?: string;
}

export function FormPathInputComponent( {
value,
onClick,
error,
tipMessage,
id = 'site-path',
}: FormPathInputComponentProps ) {
const { __ } = useI18n();
const errorId = `${ id }-error`;

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 ? errorId : undefined }
type="button"
aria-label={ `${ value }, ${ __( 'Select different local path' ) }` }
className={ cx(
'flex flex-row items-stretch rounded-sm border border-frame-border focus:border-frame-theme focus:shadow-[0_0_0_0.5px] focus:shadow-a8c-blue-50 outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-frame-theme [&: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">
{ value }
</div>
<div
aria-hidden="true"
className="local-path-icon flex items-center py-[9px] px-2.5 self-center"
>
<FolderIcon className="text-frame-text-secondary" />
</div>
</button>
<SiteFormError error={ error } tipMessage={ tipMessage } id={ errorId } />
<input type="hidden" data-testid="local-path-input" value={ value } />
</div>
);
}
43 changes: 43 additions & 0 deletions apps/studio/src/components/site-form-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Icon } from '@wordpress/components';
import { cautionFilled, tip } from '@wordpress/icons';
import { cx } from 'src/lib/cx';

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

export const SiteFormError = ( {
error,
tipMessage = '',
className = '',
id = 'site-path-error',
}: SiteFormErrorProps ) => {
return (
( error || tipMessage ) && (
<div
id={ id }
role="alert"
aria-atomic="true"
className={ cx(
'flex items-start gap-1 text-xs',
error ? 'text-red-500' : 'text-frame-text-secondary',
className
) }
>
<Icon
className={ cx(
'shrink-0 basis-4',
error ? 'fill-red-500' : 'fill-frame-text-secondary'
) }
icon={ error ? cautionFilled : tip }
width={ 16 }
height={ 16 }
/>
<p>{ error ? error : tipMessage }</p>
</div>
)
);
};
37 changes: 27 additions & 10 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,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 @@ -149,6 +149,8 @@ export {
saveUserLocale,
saveUserTerminal,
showUserSettings,
getDefaultSiteDirectory,
saveDefaultSiteDirectory,
} from 'src/modules/user-settings/lib/ipc-handlers';

export async function getAgentInstructionsStatus(
Expand Down Expand Up @@ -629,16 +631,25 @@ export interface FolderDialogResponse {
isNameTooLong?: boolean;
}

async function getDefaultSiteDirectory(): Promise< string > {
const userData = await loadUserData();
return resolveDefaultSiteDirectory( userData.defaultSiteDirectory );
}

export async function showSaveAsDialog( event: IpcMainInvokeEvent, options: SaveDialogOptions ) {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( ! parentWindow ) {
throw new Error( `No window found for sender of showSaveAsDialog message: ${ event.frameId }` );
}

const defaultPath =
options.defaultPath === nodePath.basename( options.defaultPath ?? '' )
? nodePath.join( DEFAULT_SITE_PATH, options.defaultPath )
: options.defaultPath;
let defaultPath = options.defaultPath;
if (
typeof options.defaultPath === 'string' &&
options.defaultPath === nodePath.basename( options.defaultPath )
) {
const defaultSiteDirectory = await getDefaultSiteDirectory();
defaultPath = nodePath.join( defaultSiteDirectory, options.defaultPath );
}
const { canceled, filePath } = await dialog.showSaveDialog( parentWindow, {
defaultPath,
...options,
Expand Down Expand Up @@ -672,9 +683,11 @@ export async function showOpenFolderDialog(
};
}

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

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

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

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

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

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

Expand Down
112 changes: 8 additions & 104 deletions apps/studio/src/modules/add-site/components/create-site-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { SupportedPHPVersion, SupportedPHPVersions } from '@studio/common/types/
import { Icon, SelectControl, Notice } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf, _n } from '@wordpress/i18n';
import { tip, cautionFilled, chevronRight, chevronDown, chevronLeft } from '@wordpress/icons';
import { cautionFilled, chevronRight, chevronDown, chevronLeft } from '@wordpress/icons';
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,107 +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-frame-text-secondary',
className
) }
>
<Icon
className={ cx(
'shrink-0 basis-4',
error ? 'fill-red-500' : 'fill-frame-text-secondary'
) }
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-frame-border focus:border-frame-theme focus:shadow-[0_0_0_0.5px] focus:shadow-frame-theme outline-none transition-shadow transition-linear duration-100 [&_.local-path-icon]:focus:border-l-frame-theme [&: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-frame-text-secondary" />
</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 Expand Up @@ -519,7 +419,11 @@ export const CreateSiteForm = ( {
) }
</span>
<FormPathInputComponent
doesPathContainWordPress={ doesPathContainWordPress }
tipMessage={
doesPathContainWordPress
? __( 'The existing WordPress site at this path will be added.' )
: ''
}
error={ pathError }
value={ sitePath }
onClick={ handleSelectPath }
Expand Down
Loading