diff --git a/apps/studio/src/components/form-path-input.tsx b/apps/studio/src/components/form-path-input.tsx
new file mode 100644
index 0000000000..f340e77c20
--- /dev/null
+++ b/apps/studio/src/components/form-path-input.tsx
@@ -0,0 +1,66 @@
+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 (
+
+
+
+
+
+ );
+}
diff --git a/apps/studio/src/components/site-form-error.tsx b/apps/studio/src/components/site-form-error.tsx
new file mode 100644
index 0000000000..9650971507
--- /dev/null
+++ b/apps/studio/src/components/site-form-error.tsx
@@ -0,0 +1,37 @@
+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;
+}
+
+export const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => {
+ return (
+ ( error || tipMessage ) && (
+
+
+
{ error ? error : tipMessage }
+
+ )
+ );
+};
diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts
index 17f3cfc9c9..6c75ec9fc1 100644
--- a/apps/studio/src/ipc-handlers.ts
+++ b/apps/studio/src/ipc-handlers.ts
@@ -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,
@@ -149,6 +149,8 @@ export {
saveUserLocale,
saveUserTerminal,
showUserSettings,
+ getDefaultSiteDirectory,
+ saveDefaultSiteDirectory,
} from 'src/modules/user-settings/lib/ipc-handlers';
export async function getAgentInstructionsStatus(
@@ -635,10 +637,14 @@ export async function showSaveAsDialog( event: IpcMainInvokeEvent, options: Save
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 resolveDefaultSiteDirectory();
+ defaultPath = nodePath.join( defaultSiteDirectory, options.defaultPath );
+ }
const { canceled, filePath } = await dialog.showSaveDialog( parentWindow, {
defaultPath,
...options,
@@ -672,9 +678,11 @@ export async function showOpenFolderDialog(
};
}
+ const defaultPath =
+ defaultDialogPath !== '' ? defaultDialogPath : await resolveDefaultSiteDirectory();
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
@@ -718,7 +726,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 }'` );
@@ -891,7 +900,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 {
@@ -926,9 +936,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
);
}
@@ -937,10 +948,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
);
}
diff --git a/apps/studio/src/modules/add-site/components/create-site-form.tsx b/apps/studio/src/modules/add-site/components/create-site-form.tsx
index 14eb78b332..1ed191ec60 100644
--- a/apps/studio/src/modules/add-site/components/create-site-form.tsx
+++ b/apps/studio/src/modules/add-site/components/create-site-form.tsx
@@ -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';
@@ -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 ) && (
-
-
-
{ error ? error : __( tipMessage ) }
-
- )
- );
-};
-
-function FormPathInputComponent( {
- value,
- onClick,
- error,
- doesPathContainWordPress,
- id,
-}: FormPathInputComponentProps ) {
- const { __ } = useI18n();
- return (
-
-
-
-
-
- );
-}
-
export const CreateSiteForm = ( {
defaultValues = {},
onSelectPath,
diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx
index b676efaa34..69aa05dd57 100644
--- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx
+++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx
@@ -2,6 +2,7 @@ import { SupportedLocale } from '@studio/common/lib/locale';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import Button from 'src/components/button';
+import { FormPathInputComponent } from 'src/components/form-path-input';
import { isWindowsStore } from 'src/lib/app-globals';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { ColorSchemePicker } from 'src/modules/user-settings/components/color-scheme-picker';
@@ -23,6 +24,7 @@ import {
useGetStudioCliIsInstalledQuery,
useSaveStudioCliIsInstalledMutation,
} from 'src/stores/installed-apps-api';
+import { SettingsFormField } from './settings-form-field';
export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
const { __ } = useI18n();
@@ -44,6 +46,9 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
const [ dirtyEditor, setDirtyEditor ] = useState< SupportedEditor | null >();
const [ dirtyTerminal, setDirtyTerminal ] = useState< SupportedTerminal >();
const [ dirtyIsCliInstalled, setDirtyIsCliInstalled ] = useState< boolean >();
+ const [ storedDefaultSiteDirectory, setStoredDefaultSiteDirectory ] = useState< string >();
+ const [ defaultSiteDirectory, setDefaultSiteDirectory ] = useState< string >();
+ const [ isLoadingDefaultSiteDirectory, setIsLoadingDefaultSiteDirectory ] = useState( true );
const wasSavedRef = useRef( false );
const dirtyColorSchemeRef = useRef( dirtyColorScheme );
@@ -88,6 +93,13 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
if ( dirtyIsCliInstalled !== undefined ) {
await saveCliIsInstalled( dirtyIsCliInstalled );
}
+ const isDefaultDirectoryDirty =
+ storedDefaultSiteDirectory !== undefined &&
+ defaultSiteDirectory !== undefined &&
+ storedDefaultSiteDirectory !== defaultSiteDirectory;
+ if ( isDefaultDirectoryDirty && defaultSiteDirectory ) {
+ await getIpcApi().saveDefaultSiteDirectory( defaultSiteDirectory );
+ }
onClose();
};
@@ -103,8 +115,40 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
[ dirtyEditor, editor ],
[ dirtyTerminal, terminal ],
[ dirtyIsCliInstalled, isCliInstalled ],
+ [ defaultSiteDirectory, storedDefaultSiteDirectory ],
].some( ( [ a, b ] ) => a !== undefined && a !== b );
+ useEffect( () => {
+ let isMounted = true;
+ void ( async () => {
+ try {
+ const directory = await getIpcApi().getDefaultSiteDirectory();
+ if ( ! isMounted ) {
+ return;
+ }
+ setStoredDefaultSiteDirectory( directory );
+ setDefaultSiteDirectory( directory );
+ } finally {
+ if ( isMounted ) {
+ setIsLoadingDefaultSiteDirectory( false );
+ }
+ }
+ } )();
+ return () => {
+ isMounted = false;
+ };
+ }, [] );
+
+ const handleChangeDefaultDirectory = async () => {
+ const response = await getIpcApi().showOpenFolderDialog(
+ __( 'Select default site directory' ),
+ defaultSiteDirectory ?? ''
+ );
+ if ( response?.path ) {
+ setDefaultSiteDirectory( response.path );
+ }
+ };
+
return (
<>
@@ -120,6 +164,13 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
{ ! isWindowsStore() && (
) }
+
+
+