Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bbe81e0
add absolute url to global type and persistent user data
sejas Dec 19, 2024
ca30645
create edit absolute url component
sejas Dec 19, 2024
2929446
Add absolute url form and update node and react state
sejas Dec 19, 2024
a1d34c2
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Jan 22, 2025
081454b
Add default port to URL
sejas Jan 22, 2025
906ab8c
fix state for different sites
sejas Jan 22, 2025
b14f325
Add basic instructions
sejas Jan 22, 2025
ec06fae
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Jan 23, 2025
792b8a7
do not show absoluteUrl when no hostname and only port exits
sejas Jan 23, 2025
52aac3a
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Jan 29, 2025
3927955
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Feb 6, 2025
eb00b30
Update site url
sejas Feb 6, 2025
c7297c2
Move port checkbox below the final url
sejas Feb 6, 2025
e1e75eb
Move the sentence to the second line
sejas Feb 6, 2025
95a3420
add permanent information about domains
sejas Feb 6, 2025
e869fd0
remove explicit instructions about etc hosts in favor of link to the …
sejas Feb 6, 2025
68c1b60
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Feb 10, 2025
47b7fda
Merge branch 'trunk' of github.com:Automattic/studio into add/custom-…
sejas Feb 13, 2025
88d7094
Update checkbox to skip port
sejas Feb 13, 2025
f9529f6
use the new docs hook
sejas Feb 13, 2025
e4eec69
Use a single line for skip port label
sejas Feb 13, 2025
363f21a
fix tests
sejas Feb 13, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/components/content-tab-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren } from 'react';
import { CopyTextButton } from 'src/components/copy-text-button';
import DeleteSite from 'src/components/delete-site';
import EditAbsoluteUrl from 'src/components/edit-absolute-url';
import EditPhpVersion from 'src/components/edit-php-version';
import EditSite from 'src/components/edit-site';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
Expand Down Expand Up @@ -29,6 +30,8 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
const storedPassword = decodePassword( selectedSite.adminPassword ?? '' );
const password = storedPassword === '' ? 'password' : storedPassword;
const wpVersion = useGetWpVersion( selectedSite );
const url = selectedSite.absoluteUrl || `http://localhost:${ selectedSite.port }`;
const urlWihtoutProtocol = url.replace( /http(s)?:\/\//, '' );
return (
<div className="p-8">
<table className="mb-2 m-w-full" cellPadding={ 0 } cellSpacing={ 0 }>
Expand All @@ -44,14 +47,17 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
<EditSite />
</div>
</SettingsRow>
<SettingsRow label={ __( 'Local URL' ) }>
<CopyTextButton
text={ `http://localhost:${ selectedSite.port }` }
label={ `localhost:${ selectedSite.port }, ${ __( 'Copy site url to clipboard' ) }` }
copyConfirmation={ __( 'Copied!' ) }
>
{ `localhost:${ selectedSite.port }` }
</CopyTextButton>
<SettingsRow label={ __( 'URL' ) }>
<div className="flex">
<CopyTextButton
text={ url }
label={ `${ urlWihtoutProtocol }, ${ __( 'Copy site url to clipboard' ) }` }
copyConfirmation={ __( 'Copied!' ) }
>
{ urlWihtoutProtocol }
</CopyTextButton>
<EditAbsoluteUrl key={ selectedSite.id } />
</div>
</SettingsRow>
<SettingsRow label={ __( 'Local path' ) }>
<div className="flex">
Expand Down
150 changes: 150 additions & 0 deletions src/components/edit-absolute-url.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { createInterpolateElement } from '@wordpress/element';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useState } from 'react';
import { useDocsLink } from 'src/hooks/use-docs-link';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { useSiteDetails } from '../hooks/use-site-details';
import Button from './button';
import Modal from './modal';
import TextControlComponent from './text-control';

interface UrlState {
localUrl: string;
skipPort: boolean;
}

export default function EditAbsoluteUrl() {
const { __ } = useI18n();
const getDocsLink = useDocsLink();
const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
const [ isEditingSite, setIsEditingSite ] = useState( false );
const [ showModal, setShowModal ] = useState( false );

// Initialize URL state from selected site
const initialUrlState = {
localUrl:
selectedSite?.absoluteUrl?.replace( `:${ selectedSite?.port }`, '' ) || 'http://localhost',
skipPort:
Boolean( selectedSite?.absoluteUrl ) &&
! selectedSite?.absoluteUrl?.includes( `:${ selectedSite?.port }` ),
};
const [ urlState, setUrlState ] = useState< UrlState >( initialUrlState );

// Compute absolute URL
const absoluteUrl =
! urlState.skipPort && selectedSite?.port
? `${ urlState.localUrl }:${ selectedSite.port }`
: urlState.localUrl;

const handleSubmit = useCallback(
async ( e: React.FormEvent ) => {
e.preventDefault();
if ( ! selectedSite || ! urlState.localUrl.trim() ) return;

setIsEditingSite( true );
try {
await updateSite( {
...selectedSite,
absoluteUrl,
} );

if ( selectedSite.running ) {
await stopServer( selectedSite.id );
await startServer( selectedSite.id );
}
setShowModal( false );
} finally {
setIsEditingSite( false );
}
},
[ absoluteUrl, selectedSite, startServer, stopServer, updateSite, urlState.localUrl ]
);

const closeModal = () => setShowModal( false );

return (
<>
<Button
disabled={ ! selectedSite }
className="!mx-4 shrink-0"
onClick={ () => selectedSite && setShowModal( true ) }
label={ __( 'Edit Local URL' ) }
variant="link"
>
{ __( 'Edit' ) }
</Button>

{ showModal && (
<Modal
size="medium"
title={ __( 'Edit Local URL' ) }
isDismissible
focusOnMount="firstContentElement"
onRequestClose={ closeModal }
>
<form onSubmit={ handleSubmit } className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5 leading-4">
<label className="font-semibold" htmlFor="hostname-input">
{ __( 'Hostname' ) }
</label>
<TextControlComponent
id="hostname-input"
onChange={ ( value ) =>
setUrlState( ( prev ) => ( { ...prev, localUrl: value } ) )
}
value={ urlState.localUrl }
placeholder="http://localhost"
/>
<span className="text-a8c-gray-50 text-xs">
{ createInterpolateElement(
__(
"If you're using a TLD other than .localhost, you may need to update your hosts file. <button>Learn more.</button>"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to the user-facing documentation section that will be linked to from this Learn more link, is this something we are planning to add before the release?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we will have a section there and will update the link to the correct one before we release Studio.

),
{
button: (
<Button
variant="link"
className="text-xs"
onClick={ () => getIpcApi().openURL( getDocsLink( 'sites' ) ) }
/>
),
}
) }
</span>
</div>

<div className="flex flex-col gap-1.5 leading-4">
<span className="font-semibold">{ __( 'Your site URL' ) }</span>
<div className="px-3 py-1.5 bg-gray-100 rounded text-gray-600">{ absoluteUrl }</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={ urlState.skipPort }
onChange={ ( e ) =>
setUrlState( ( prev ) => ( { ...prev, skipPort: e.target.checked } ) )
}
className="form-checkbox"
/>
<span>{ __( 'Skip the port to use external routing like Ngrok.' ) }</span>
</label>
</div>

<div className="flex flex-row justify-end gap-x-5 mt-6">
<Button onClick={ closeModal } disabled={ isEditingSite } variant="tertiary">
{ __( 'Cancel' ) }
</Button>
<Button
type="submit"
variant="primary"
isBusy={ isEditingSite }
disabled={ isEditingSite || ! selectedSite || ! urlState.localUrl.trim() }
>
{ isEditingSite ? __( 'Restarting server…' ) : __( 'Save' ) }
</Button>
</div>
</form>
</Modal>
) }
</>
);
}
1 change: 1 addition & 0 deletions src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface StoppedSiteDetails {
id: string;
name: string;
path: string;
absoluteUrl?: string;
port?: number;
phpVersion: string;
adminPassword?: string;
Expand Down
4 changes: 2 additions & 2 deletions src/site-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ export class SiteServer {
siteTitle: this.details.name,
php: this.details.phpVersion,
} );
const absoluteUrl = `http://localhost:${ port }`;
options.absoluteUrl = absoluteUrl;
options.absoluteUrl = this.details.absoluteUrl || `http://localhost:${ port }`;
options.siteLanguage = await getPreferredSiteLanguage( options.wordPressVersion );

if ( options.mode !== WPNowMode.WORDPRESS ) {
Expand Down Expand Up @@ -123,6 +122,7 @@ export class SiteServer {
name: site.name,
path: site.path,
phpVersion: site.phpVersion,
absoluteUrl: site.absoluteUrl,
};
}

Expand Down
50 changes: 28 additions & 22 deletions src/storage/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,35 @@ export async function saveUserData( data: UserData ): Promise< void > {
function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData {
return {
version: 1,
sites: sites.map( ( { id, path, adminPassword, port, phpVersion, name, themeDetails } ) => {
// No object spreading allowed. TypeScript's structural typing is too permissive and
// will permit us to persist properties that aren't in the type definition.
// Add each property explicitly instead.
const persistedSiteDetails: PersistedUserData[ 'sites' ][ number ] = {
id,
name,
path,
adminPassword,
port,
phpVersion,
themeDetails: {
name: themeDetails?.name || '',
path: themeDetails?.path || '',
slug: themeDetails?.slug || '',
isBlockTheme: themeDetails?.isBlockTheme || false,
supportsWidgets: themeDetails?.supportsWidgets || false,
supportsMenus: themeDetails?.supportsMenus || false,
},
};
sites: sites.map(
( { id, path, adminPassword, port, phpVersion, name, themeDetails, absoluteUrl } ) => {
// No object spreading allowed. TypeScript's structural typing is too permissive and
// will permit us to persist properties that aren't in the type definition.
// Add each property explicitly instead.
const persistedSiteDetails: PersistedUserData[ 'sites' ][ number ] = {
id,
name,
path,
adminPassword,
port,
phpVersion,
themeDetails: {
name: themeDetails?.name || '',
path: themeDetails?.path || '',
slug: themeDetails?.slug || '',
isBlockTheme: themeDetails?.isBlockTheme || false,
supportsWidgets: themeDetails?.supportsWidgets || false,
supportsMenus: themeDetails?.supportsMenus || false,
},
};

return persistedSiteDetails;
} ),
if ( absoluteUrl ) {
persistedSiteDetails.absoluteUrl = absoluteUrl;
}

return persistedSiteDetails;
}
),
...rest,
};
}
Expand Down