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 ) && ( + + ) + ); +}; 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 ) && ( - - ) - ); -}; - -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 && } ) } + + +