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..d46258203b
--- /dev/null
+++ b/apps/studio/src/components/form-path-input.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
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..96380c527f
--- /dev/null
+++ b/apps/studio/src/components/site-form-error.tsx
@@ -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 ) && (
+
+
+
{ error ? error : __( tipMessage ) }
+
+ )
+ );
+};
diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts
index 9e2267e161..66578908e1 100644
--- a/apps/studio/src/ipc-handlers.ts
+++ b/apps/studio/src/ipc-handlers.ts
@@ -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,
@@ -140,6 +140,8 @@ export {
saveUserLocale,
saveUserTerminal,
showUserSettings,
+ getDefaultSiteDirectory,
+ saveDefaultSiteDirectory,
} from 'src/modules/user-settings/lib/ipc-handlers';
export async function getAgentInstructionsStatus(
@@ -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,
@@ -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
@@ -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 }'` );
@@ -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 {
@@ -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
);
}
@@ -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
);
}
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 a72e60e534..34dbe8191c 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
@@ -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';
@@ -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 ) && (
-
-
-
{ 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 9ff501bbfd..75b9e7f55e 100644
--- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx
+++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx
@@ -1,9 +1,11 @@
import { SupportedLocale } from '@studio/common/lib/locale';
import { useI18n } from '@wordpress/react-i18n';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import Button from 'src/components/button';
+import { FormPathInputComponent } from 'src/components/form-path-input';
import { useFeatureFlags } from 'src/hooks/use-feature-flags';
import { isWindowsStore } from 'src/lib/app-globals';
+import { getIpcApi } from 'src/lib/get-ipc-api';
import { McpSettings } from 'src/modules/mcp/components/mcp-settings';
import { EditorPicker } from 'src/modules/user-settings/components/editor-picker';
import { LanguagePicker } from 'src/modules/user-settings/components/language-picker';
@@ -21,6 +23,7 @@ import {
useGetStudioCliIsInstalledQuery,
useSaveStudioCliIsInstalledMutation,
} from 'src/stores/installed-apps-api';
+import { SettingsFormField } from './settings-form-field';
export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
const { __ } = useI18n();
@@ -40,6 +43,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 savePreferences = async () => {
if ( dirtyLocale ) {
@@ -54,6 +60,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();
};
@@ -67,8 +80,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 (
<>
@@ -84,6 +129,13 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
{ enableAgentSuite && }
>
) }
+
+
+