diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index f9f5c18a82..5343a0dad0 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -87,7 +87,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, @@ -137,6 +137,8 @@ export { saveUserLocale, saveUserTerminal, showUserSettings, + getDefaultSiteDirectory, + saveDefaultSiteDirectory, } from 'src/modules/user-settings/lib/ipc-handlers'; export async function getAgentInstructionsStatus( @@ -608,9 +610,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, @@ -645,9 +649,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 @@ -691,7 +696,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 }'` ); @@ -889,7 +895,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 { @@ -924,9 +931,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 ); } @@ -935,10 +943,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/user-settings/components/default-directory-picker.tsx b/apps/studio/src/modules/user-settings/components/default-directory-picker.tsx new file mode 100644 index 0000000000..f6960cd13e --- /dev/null +++ b/apps/studio/src/modules/user-settings/components/default-directory-picker.tsx @@ -0,0 +1,39 @@ +import { useI18n } from '@wordpress/react-i18n'; +import Button from 'src/components/button'; +import { SettingsFormField } from './settings-form-field'; + +interface DefaultDirectoryPickerProps { + directory?: string; + isLoading: boolean; + isSelecting: boolean; + onPick: () => void; +} + +export const DefaultDirectoryPicker = ( { + directory, + isLoading, + isSelecting, + onPick, +}: DefaultDirectoryPickerProps ) => { + const { __ } = useI18n(); + + return ( + +
+

+ { isLoading ? __( 'Loading...' ) : directory ?? '' } +

+
+ +
+
+
+ ); +}; 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..c328d11be6 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -1,10 +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 { useFeatureFlags } from 'src/hooks/use-feature-flags'; import { isWindowsStore } from 'src/lib/app-globals'; import { McpSettings } from 'src/modules/mcp/components/mcp-settings'; +import { getIpcApi } from 'src/lib/get-ipc-api'; import { EditorPicker } from 'src/modules/user-settings/components/editor-picker'; import { LanguagePicker } from 'src/modules/user-settings/components/language-picker'; import { StudioCliToggle } from 'src/modules/user-settings/components/studio-cli-toggle'; @@ -21,6 +22,7 @@ import { useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, } from 'src/stores/installed-apps-api'; +import { DefaultDirectoryPicker } from './default-directory-picker'; export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const { __ } = useI18n(); @@ -40,6 +42,10 @@ 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 [ isSelectingDefaultDirectory, setIsSelectingDefaultDirectory ] = useState( false ); 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,45 @@ 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 () => { + setIsSelectingDefaultDirectory( true ); + try { + const response = await getIpcApi().showOpenFolderDialog( + __( 'Select default site directory' ), + defaultSiteDirectory ?? '' + ); + if ( response?.path ) { + setDefaultSiteDirectory( response.path ); + } + } finally { + setIsSelectingDefaultDirectory( false ); + } + }; + return ( <> @@ -84,6 +134,12 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { { enableAgentSuite && } ) } +