Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions apps/studio/src/hooks/use-is-multisite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { getIpcApi } from 'src/lib/get-ipc-api';

export function useIsMultisite( site: SiteDetails ) {
const [ isMultisite, setIsMultisite ] = useState( false );
const refresh = useCallback( () => {
void getIpcApi().getIsMultisite( site.id ).then( setIsMultisite );
}, [ site.id ] );
useEffect( () => {
refresh();
}, [ site.running, refresh ] );
return isMultisite;
}
9 changes: 9 additions & 0 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { generateNumberedName, generateSiteName } from '@studio/common/lib/generate-site-name';
import { getWordPressVersion } from '@studio/common/lib/get-wordpress-version';
import { isErrnoException } from '@studio/common/lib/is-errno-exception';
import { isMultisite } from '@studio/common/lib/is-multisite';
import { getAuthenticationUrl } from '@studio/common/lib/oauth';
import { decodePassword, encodePassword } from '@studio/common/lib/passwords';
import { sanitizeFolderName } from '@studio/common/lib/sanitize-folder-name';
Expand Down Expand Up @@ -923,6 +924,14 @@ export function getWpVersion( _event: IpcMainInvokeEvent, id: string ) {
return getWordPressVersion( wordPressPath );
}

export function getIsMultisite( _event: IpcMainInvokeEvent, id: string ) {
const server = SiteServer.get( id );
if ( ! server ) {
return false;
}
return isMultisite( server.details.path );
}

export async function generateProposedSitePath(
_event: IpcMainInvokeEvent,
siteName: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { DEMO_SITE_SIZE_LIMIT_GB } from '@studio/common/constants';
import { Icon } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { cautionFilled } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { AuthContextType } from 'src/components/auth-provider';
import Button from 'src/components/button';
import offlineIcon from 'src/components/offline-icon';
import { Tooltip } from 'src/components/tooltip';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
import { useIsMultisite } from 'src/hooks/use-is-multisite';
import { useOffline } from 'src/hooks/use-offline';
import { useSiteSize } from 'src/hooks/use-site-size';
import { getLatestStableWpVersion } from 'src/lib/version-utils';
Expand Down Expand Up @@ -51,11 +54,12 @@ export function CreatePreviewButton( { onClick, selectedSite, user }: CreatePrev
const isOtherSiteArchiving = isAnySiteArchiving && ! isCurrentSiteArchiving;

const latestWpVersion = getLatestStableWpVersion( wpVersions );
const shouldShowMismatchTooltip = hasVersionMismatch( {
const shouldShowVersionMismatch = hasVersionMismatch( {
wpVersion,
latestWpVersion,
phpVersion: selectedSite.phpVersion,
} );
const isMultisite = useIsMultisite( selectedSite );

const isDisabled =
isAnySiteArchiving || isLimitUsed || isOffline || snapshotCreationBlocked || isOverLimit;
Expand All @@ -81,9 +85,6 @@ export function CreatePreviewButton( { onClick, selectedSite, user }: CreatePrev
),
DEMO_SITE_SIZE_LIMIT_GB
);
const versionMismatchMessage = __(
'Your Studio site is running versions not supported by preview sites. The preview site will automatically switch to the supported WordPress and PHP versions.'
);
const snapshotCreationBlockedMessage = __( 'Preview sites are not available for your account.' );

let tooltipContent;
Expand All @@ -102,25 +103,65 @@ export function CreatePreviewButton( { onClick, selectedSite, user }: CreatePrev
tooltipContent = { text: snapshotCreationBlockedMessage };
} else if ( isOverLimit ) {
tooltipContent = { text: overLimitMessage };
} else if ( shouldShowMismatchTooltip ) {
tooltipContent = { text: versionMismatchMessage };
}

const warnings: string[] = [];
if ( shouldShowVersionMismatch ) {
warnings.push(
__(
'Your Studio site is running versions not supported by preview sites. The preview site will automatically switch to the supported WordPress and PHP versions.'
)
);
}
if ( isMultisite ) {
warnings.push(
__(
'Your Studio site is configured as a multisite network. Preview sites do not support multisite, which may cause unexpected issues.'
)
);
}

const warningsTooltipContent =
warnings.length > 0 ? (
<ul className="list-disc pl-4 flex flex-col gap-1.5">
{ warnings.map( ( warning, index ) => (
<li key={ index }>{ warning }</li>
) ) }
</ul>
) : undefined;

return (
<Tooltip disabled={ ! tooltipContent } { ...tooltipContent } placement="top-start">
<Button
aria-description={ tooltipContent?.text ?? '' }
aria-disabled={ isDisabled }
variant="primary"
onClick={ () => {
if ( isDisabled ) {
return;
}
onClick();
} }
>
{ __( 'Create preview site' ) }
</Button>
</Tooltip>
<div className="flex items-center gap-3">
<Tooltip disabled={ ! tooltipContent } { ...tooltipContent } placement="top-start">
<Button
aria-description={ tooltipContent?.text ?? '' }
aria-disabled={ isDisabled }
variant="primary"
onClick={ () => {
if ( isDisabled ) {
return;
}
onClick();
} }
>
{ __( 'Create preview site' ) }
</Button>
</Tooltip>
{ warnings.length > 0 && (
<Tooltip text={ warningsTooltipContent } placement="top-start">
<div
className="flex items-center gap-1 text-amber-500 text-[13px] leading-[16px] cursor-pointer"
data-testid="preview-warnings-badge"
>
<Icon icon={ cautionFilled } size={ 16 } className="fill-amber-500" />
{ sprintf(
/* translators: %d: number of warnings */
_n( '%d warning', '%d warnings', warnings.length ),
warnings.length
) }
</div>
</Tooltip>
) }
</div>
);
}
90 changes: 68 additions & 22 deletions apps/studio/src/modules/sync/components/sync-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { SelectControl, Notice, __experimentalHeading as Heading } from '@wordpress/components';
import {
Icon,
SelectControl,
Notice,
__experimentalHeading as Heading,
} from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { sprintf, __ } from '@wordpress/i18n';
import { cautionFilled } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { format } from 'date-fns';
import { useState, useEffect, useCallback } from 'react';
Expand All @@ -13,6 +19,7 @@ import { Tooltip } from 'src/components/tooltip';
import { TreeView, TreeNode, updateNodeById } from 'src/components/tree-view';
import { SYNC_PUSH_SIZE_LIMIT_GB } from 'src/constants';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
import { useIsMultisite } from 'src/hooks/use-is-multisite';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { getLocalizedLink } from 'src/lib/get-localized-link';
Expand Down Expand Up @@ -143,7 +150,7 @@ export function SyncDialog( {
onRequestClose,
}: SyncDialogProps ) {
const locale = useI18nLocale();
const { __ } = useI18n();
const { __, _n } = useI18n();
const siteEnv = getSiteEnvironment( remoteSite );
const syncTexts = useSyncDialogTexts( type, siteEnv );
const defaultTree = useTopLevelSyncTree();
Expand Down Expand Up @@ -184,6 +191,14 @@ export function SyncDialog( {
phpVersion: localSite.phpVersion,
} );

const isMultisite = useIsMultisite( localSite );
const shouldShowMultisiteWarning = type === 'push' && isMultisite && ! remoteSite.isPressable;

const warningCount = [ shouldShowVersionMismatch, shouldShowMultisiteWarning ].filter(
Boolean
).length;
const [ warningsExpanded, setWarningsExpanded ] = useState( true );

const localSiteName = <SiteNameBox siteName={ localSite.name } envType="studio" />;
const remoteSiteName = <SiteNameBox siteName={ remoteSite.name } envType={ siteEnv } />;

Expand Down Expand Up @@ -267,20 +282,16 @@ export function SyncDialog( {

const getBottomPadding = () => {
if ( type === 'pull' ) {
return 'pb-[70px]'; // Original padding for pull
return 70;
}
// Calculate dynamic padding based on number of notices shown
const noticeCount = [ isPushSelectionOverLimit, shouldShowVersionMismatch ].filter(
Boolean
).length;

if ( noticeCount === 0 ) {
return 'pb-[140px]'; // Just progress bar
let padding = 140; // Base: progress bar + buttons
if ( isPushSelectionOverLimit ) {
padding += 80;
}
if ( noticeCount === 1 ) {
return 'pb-[220px]'; // Progress bar + one notice
if ( warningCount > 0 ) {
padding += warningsExpanded ? 70 * warningCount + 30 : 20;
}
return 'pb-[300px]'; // Progress bar + two notices
return padding;
};

return (
Expand All @@ -289,7 +300,7 @@ export function SyncDialog( {
onRequestClose={ onRequestClose }
title={ syncTexts.title }
>
<div className={ getBottomPadding() }>
<div style={ { paddingBottom: getBottomPadding() } }>
<div className="px-8 pb-6 pt-1">{ syncTexts.description }</div>
<div className="px-8">
<span className="sr-only">
Expand Down Expand Up @@ -417,7 +428,7 @@ export function SyncDialog( {
</div>
</Tooltip>

<div className="px-8 py-4 absolute left-0 right-0 bottom-0 bg-frame/[0.8] backdrop-blur-sm z-10 border-t border-frame-border">
<div className="px-8 py-4 absolute left-0 right-0 bottom-0 bg-frame z-10 border-t border-frame-border">
{ type === 'push' && (
<div className="mb-4">
<TwoColorProgressBar
Expand All @@ -444,14 +455,49 @@ export function SyncDialog( {
</p>
</Notice>
) }
{ shouldShowVersionMismatch && (
<Notice status="warning" isDismissible={ false } className="mb-4">
<p data-testid="push-version-mismatch-notice">
{ __(
'Your Studio site is using a different WordPress or PHP version than your remote site. The remote site will keep using the newest supported versions.'
{ warningCount > 0 && (
<div className="mb-4" data-testid="push-warnings-section">
<div
className="flex items-center gap-1 text-amber-500 text-[13px] leading-[16px]"
data-testid="push-warnings-toggle"
>
<Icon icon={ cautionFilled } size={ 16 } className="fill-amber-500" />
{ sprintf(
/* translators: %d: number of warnings */
_n( '%d warning', '%d warnings', warningCount ),
warningCount
) }
</p>
</Notice>
<Button
variant="link"
className="!p-0 !h-auto text-[12px] leading-[16px] [&.is-link]:text-frame-text-secondary [&.is-link]:hover:text-frame-theme"
onClick={ () => setWarningsExpanded( ! warningsExpanded ) }
>
{ warningsExpanded ? __( 'Hide' ) : __( 'Show' ) }

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.

I am not sure if it is me but this is looking slightly misaligned for me by a couple of pixels:

Image

e.g. Hide and Show seem slightly higher than the warning

</Button>
</div>
{ warningsExpanded && (
<div className="mt-2 flex flex-col gap-2">
{ shouldShowVersionMismatch && (
<Notice status="warning" isDismissible={ false }>
<p data-testid="push-version-mismatch-notice">
{ __(
'Your Studio site is using a different WordPress or PHP version than your remote site. The remote site will keep using the newest supported versions.'
) }
</p>
</Notice>
) }
{ shouldShowMultisiteWarning && (
<Notice status="warning" isDismissible={ false }>
<p data-testid="push-multisite-warning-notice">
{ __(
'Your Studio site is configured as a multisite network. WordPress.com does not support multisite, so pushing may cause unexpected issues on the remote site.'
) }
</p>
</Notice>
) }
</div>
) }
</div>
) }
<div className="flex justify-between items-center">
<div>
Expand Down
1 change: 1 addition & 0 deletions apps/studio/src/modules/sync/tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ describe( 'ContentTabSync', () => {
getDirectorySize: vi.fn().mockResolvedValue( 0 ),
connectWpcomSites: vi.fn(),
getWpVersion: vi.fn().mockResolvedValue( '6.4.3' ),
getIsMultisite: vi.fn().mockResolvedValue( false ),
listLocalFileTree: vi.fn().mockResolvedValue( [
{
name: 'plugins',
Expand Down
1 change: 1 addition & 0 deletions apps/studio/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const api: IpcApi = {
copyText: ( text ) => ipcRendererInvoke( 'copyText', text ),
getAppGlobals: () => ipcRendererInvoke( 'getAppGlobals' ),
getWpVersion: ( id ) => ipcRendererInvoke( 'getWpVersion', id ),
getIsMultisite: ( id ) => ipcRendererInvoke( 'getIsMultisite', id ),
generateProposedSitePath: ( siteName ) =>
ipcRendererInvoke( 'generateProposedSitePath', siteName ),
generateSiteNameFromList: ( usedSites ) =>
Expand Down
12 changes: 12 additions & 0 deletions tools/common/lib/is-multisite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import fs from 'fs';
import nodePath from 'path';

export function isMultisite( wordPressPath: string ): boolean {
let configContent = '';
try {
configContent = fs.readFileSync( nodePath.join( wordPressPath, 'wp-config.php' ), 'utf8' );
} catch ( err ) {
return false;
}
return /define\(\s*['"]MULTISITE['"]\s*,\s*true\s*\)/.test( configContent );
}
61 changes: 61 additions & 0 deletions tools/common/lib/tests/is-multisite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs from 'fs';
import nodePath from 'path';
import { vi } from 'vitest';
import { isMultisite } from '../is-multisite';

vi.mock( 'fs' );

const mockedFs = vi.mocked( fs );

describe( 'isMultisite', () => {
const wordPressPath = '/path/to/wordpress';
const wpConfigPath = nodePath.join( wordPressPath, 'wp-config.php' );

beforeEach( () => {
vi.resetAllMocks();
} );

it( 'returns true when MULTISITE is defined as true with single quotes', () => {
mockedFs.readFileSync.mockReturnValue( "define( 'MULTISITE', true );" );
expect( isMultisite( wordPressPath ) ).toBe( true );
expect( mockedFs.readFileSync ).toHaveBeenCalledWith( wpConfigPath, 'utf8' );
} );

it( 'returns true when MULTISITE is defined as true with double quotes', () => {
mockedFs.readFileSync.mockReturnValue( 'define( "MULTISITE", true );' );
expect( isMultisite( wordPressPath ) ).toBe( true );
} );

it( 'returns true when MULTISITE is defined without spaces', () => {
mockedFs.readFileSync.mockReturnValue( "define('MULTISITE',true);" );
expect( isMultisite( wordPressPath ) ).toBe( true );
} );

it( 'returns false when MULTISITE is not present', () => {
mockedFs.readFileSync.mockReturnValue( "define( 'DB_NAME', 'wordpress' );" );
expect( isMultisite( wordPressPath ) ).toBe( false );
} );

it( 'returns false when MULTISITE is set to false', () => {
mockedFs.readFileSync.mockReturnValue( "define( 'MULTISITE', false );" );
expect( isMultisite( wordPressPath ) ).toBe( false );
} );

it( 'returns false when wp-config.php does not exist', () => {
mockedFs.readFileSync.mockImplementation( () => {
throw new Error( 'ENOENT: no such file or directory' );
} );
expect( isMultisite( wordPressPath ) ).toBe( false );
} );

it( 'returns true within a full wp-config.php file', () => {
const fullConfig = `<?php
define( 'DB_NAME', 'database_name_here' );
define( 'WP_ALLOW_MULTISITE', true );
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
`;
mockedFs.readFileSync.mockReturnValue( fullConfig );
expect( isMultisite( wordPressPath ) ).toBe( true );
} );
} );
Loading