Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/components/content-tab-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { DropdownMenu, MenuGroup, Button } from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useEffect, useRef } from 'react';
import { CopyTextButton } from 'src/components/copy-text-button';
import DeleteSite from 'src/components/delete-site';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { decodePassword } from 'src/lib/passwords';
import EditSiteDetails from 'src/modules/site-settings/edit-site-details';
import EditSiteDetails, { EditSiteDetailsRef } from 'src/modules/site-settings/edit-site-details';
import { useAppDispatch } from 'src/stores';
import {
certificateTrustApi,
Expand All @@ -16,6 +16,8 @@ import {

interface ContentTabSettingsProps {
selectedSite: SiteDetails;
shouldOpenEditModal?: boolean;
onEditModalOpened?: () => void;
}

function SettingsRow( { children, label }: PropsWithChildren< { label: string } > ) {
Expand All @@ -29,10 +31,15 @@ function SettingsRow( { children, label }: PropsWithChildren< { label: string }
);
}

export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) {
export function ContentTabSettings( {
selectedSite,
shouldOpenEditModal,
onEditModalOpened,
}: ContentTabSettingsProps ) {
const dispatch = useAppDispatch();
const { __ } = useI18n();
const { data: isCertificateTrusted } = useCheckCertificateTrustQuery();
const editSiteRef = useRef< EditSiteDetailsRef >( null );
const username = 'admin';
// Empty strings account for legacy sites lacking a stored password.
const storedPassword = decodePassword( selectedSite.adminPassword ?? '' );
Expand All @@ -50,14 +57,29 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
await dispatch( certificateTrustApi.util.invalidateTags( [ 'CertificateTrust' ] ) );
};

// Open edit modal when requested from context menu
useEffect( () => {
if ( shouldOpenEditModal ) {
// Open edit modal after a brief delay to ensure tab is fully rendered
setTimeout( () => {
editSiteRef.current?.openModal();
onEditModalOpened?.();
}, 100 );
}
}, [ shouldOpenEditModal, onEditModalOpened ] );

return (
<div className="p-8 ltr:pr-4 rtl:pl-4">
<div className="flex justify-between items-center mb-4">
<h3 role="heading" className="text-black text-sm font-semibold">
{ __( 'Site details' ) }
</h3>
<div className="flex items-center gap-1">
<EditSiteDetails currentWpVersion={ wpVersion } onSave={ refreshWpVersion } />
<EditSiteDetails
ref={ editSiteRef }
currentWpVersion={ wpVersion }
onSave={ refreshWpVersion }
/>
<DropdownMenu
icon={ moreVertical }
label={ __( 'More options' ) }
Expand Down
41 changes: 39 additions & 2 deletions src/components/site-content-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TabPanel } from '@wordpress/components';
import { useI18n } from '@wordpress/react-i18n';
import { useEffect, useState } from 'react';
import { ContentTabAssistant } from 'src/components/content-tab-assistant';
import { ContentTabImportExport } from 'src/components/content-tab-import-export';
import { ContentTabOverview } from 'src/components/content-tab-overview';
Expand All @@ -16,10 +17,40 @@ import { cx } from 'src/lib/cx';
import { ContentTabSync } from 'src/modules/sync';

export function SiteContentTabs() {
const { selectedSite, data: localSites } = useSiteDetails();
const { selectedSite, data: localSites, setSelectedSiteId } = useSiteDetails();
const { importState } = useImportExport();
const { tabs, selectedTab, setSelectedTab } = useContentTabs();
const { __ } = useI18n();
const [ shouldOpenEditModal, setShouldOpenEditModal ] = useState( false );

// Listen for edit site requests from context menu
useEffect( () => {
const handleEditSiteRequest = ( event: CustomEvent ) => {
const { siteId } = event.detail;

// Find the site in the local sites list
const targetSite = localSites.find( ( site ) => site.id === siteId );
if ( ! targetSite ) {
return;
}

// If this is not the currently selected site, switch to it first
if ( siteId !== selectedSite?.id ) {
// Switch to the target site
setSelectedSiteId( siteId );
}

// Switch to settings tab
setSelectedTab( 'settings' );
// Set flag to open modal once settings tab is rendered
setShouldOpenEditModal( true );
};

window.addEventListener( 'edit-site-request', handleEditSiteRequest as EventListener );
return () => {
window.removeEventListener( 'edit-site-request', handleEditSiteRequest as EventListener );
};
}, [ selectedSite?.id, localSites, setSelectedTab, setSelectedSiteId ] );

if ( ! localSites.length ) {
return <EmptyStudio />;
Expand Down Expand Up @@ -62,7 +93,13 @@ export function SiteContentTabs() {
{ name === 'overview' && <ContentTabOverview selectedSite={ selectedSite } /> }
{ name === 'previews' && <ContentTabPreviews selectedSite={ selectedSite } /> }
{ name === 'sync' && <ContentTabSync selectedSite={ selectedSite } /> }
{ name === 'settings' && <ContentTabSettings selectedSite={ selectedSite } /> }
{ name === 'settings' && (
<ContentTabSettings
selectedSite={ selectedSite }
shouldOpenEditModal={ shouldOpenEditModal }
onEditModalOpened={ () => setShouldOpenEditModal( false ) }
/>
) }
{ name === 'assistant' && <ContentTabAssistant selectedSite={ selectedSite } /> }
{ name === 'import-export' && <ContentTabImportExport selectedSite={ selectedSite } /> }
</div>
Expand Down
135 changes: 133 additions & 2 deletions src/components/site-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/electron/renderer';
import { speak } from '@wordpress/a11y';
import { Spinner } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
Expand All @@ -6,8 +7,12 @@ import { Tooltip } from 'src/components/tooltip';
import { useSyncSites } from 'src/hooks/sync-sites';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { isMac } from 'src/lib/app-globals';
import { isMac, isWindows } from 'src/lib/app-globals';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { supportedEditorConfig } from 'src/modules/user-settings/lib/editor';
import { getTerminalName } from 'src/modules/user-settings/lib/terminal';
import { useGetUserEditorQuery, useGetUserTerminalQuery } from 'src/stores/installed-apps-api';

interface SiteMenuProps {
className?: string;
Expand Down Expand Up @@ -108,10 +113,13 @@ function ButtonToRun( { running, id, name }: Pick< SiteDetails, 'running' | 'id'
);
}
function SiteItem( { site }: { site: SiteDetails } ) {
const { selectedSite, setSelectedSiteId } = useSiteDetails();
const { selectedSite, setSelectedSiteId, startServer, stopServer, deleteSite, loadingServer } =
useSiteDetails();
const isSelected = site === selectedSite;
const { isSiteImporting, isSiteExporting } = useImportExport();
const { isSiteIdPulling } = useSyncSites();
const { data: editor } = useGetUserEditorQuery();
const { data: terminal } = useGetUserTerminalQuery();
const isImporting = isSiteImporting( site.id );
const isExporting = isSiteExporting( site.id );
const isPulling = isSiteIdPulling( site.id );
Expand All @@ -128,13 +136,136 @@ function SiteItem( { site }: { site: SiteDetails } ) {
tooltipText = __( 'Loading' );
}

// Handle context menu
const handleContextMenu = ( e: React.MouseEvent ) => {
e.preventDefault();
const ipcApi = getIpcApi();
const isLoading = loadingServer[ site.id ] || false;
const isAddingSite = site.isAddingSite || false;

// Get labels for the menu items
const finderLabel = isWindows() ? __( 'File Explorer' ) : __( 'Finder' );
const editorLabel =
editor && supportedEditorConfig[ editor ] ? supportedEditorConfig[ editor ].label : null;
const terminalLabel = getTerminalName( terminal );

ipcApi.showSiteContextMenu(
site.id,
site.name,
site.path,
site.running,
isLoading,
isAddingSite,
finderLabel,
editorLabel,
terminalLabel
);
};

// Listen for context menu actions
useEffect( () => {
const unsubscribe = window.ipcListener.subscribe(
'site-context-menu-action',
async ( _, data: { action: string; siteId: string } ) => {
if ( data.siteId === site.id ) {
const ipcApi = getIpcApi();
switch ( data.action ) {
case 'start':
void startServer( site.id );
break;
case 'stop':
void stopServer( site.id );
break;
case 'open-site':
ipcApi.openSiteURL( site.id );
break;
case 'open-admin':
ipcApi.openSiteURL( site.id, '/wp-admin/' );
break;
case 'open-finder':
ipcApi.openLocalPath( site.path );
break;
case 'open-editor':
if ( editor ) {
void ipcApi.openAppAtPath( editor, site.path );
}
break;
case 'open-terminal':
void ( async () => {
try {
await ipcApi.openTerminalAtPath( site.path );
} catch ( error ) {
Sentry.captureException( error );
alert( __( 'Could not open the terminal.' ) );
}
} )();
break;
case 'edit-site':
// Trigger edit site modal by sending a custom event
window.dispatchEvent(
new CustomEvent( 'edit-site-request', {
detail: { siteId: site.id },
} )
);
break;
case 'delete': {
// Handle delete with confirmation dialog
const DELETE_BUTTON_INDEX = 0;
const CANCEL_BUTTON_INDEX = 1;
const MAX_LENGTH_SITE_TITLE = 35;

const trimmedSiteTitle =
site.name.length > MAX_LENGTH_SITE_TITLE
? `${ site.name.substring( 0, MAX_LENGTH_SITE_TITLE - 3 ) }…`
: site.name;

const { response, checkboxChecked } = await ipcApi.showMessageBox( {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to be repeating a bunch of code from delete-site.tsx. Do you think we should extract it at this point?

Copy link
Member

Choose a reason for hiding this comment

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

I extracted the common code into a new hook: bb8cf40

type: 'warning',
message: sprintf( __( 'Delete %s' ), trimmedSiteTitle ),
detail: __(
"The site's database will be lost. Including all posts, pages, comments, and media."
),
buttons: [ __( 'Delete site' ), __( 'Cancel' ) ],
cancelId: CANCEL_BUTTON_INDEX,
checkboxLabel: __( 'Delete site files from my computer' ),
checkboxChecked: true,
} );

if ( response === DELETE_BUTTON_INDEX ) {
try {
await deleteSite( site.id, checkboxChecked );
} catch ( error ) {
ipcApi.showErrorMessageBox( {
title: __( 'Deletion failed' ),
message: sprintf(
__( "We couldn't delete the site '%s'. Please try again" ),
trimmedSiteTitle
),
error,
} );
Sentry.captureException( error );
}
}
break;
}
}
}
}
);

return () => {
unsubscribe();
};
}, [ site.id, site.name, site.path, startServer, stopServer, deleteSite, editor ] );

return (
<li
className={ cx(
'flex flex-row min-w-[168px] h-8 hover:bg-[#ffffff0C] rounded transition-all ms-1',
isMac() ? 'me-5' : 'me-4',
isSelected && 'bg-[#ffffff19] hover:bg-[#ffffff19]'
) }
onContextMenu={ handleContextMenu }
>
<button
className="p-2 text-xs rounded-tl rounded-bl whitespace-nowrap overflow-hidden text-ellipsis w-full text-left rtl:text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-a8c-blue-50"
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const IPC_VOID_HANDLERS = < const >[
'openURL',
'popupAppMenu',
'showErrorMessageBox',
'showSiteContextMenu',
'showItemInFolder',
'showNotification',
'authenticate',
Expand Down
Loading