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
28 changes: 25 additions & 3 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import {
__experimentalVStack as VStack,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { useState } from 'react';
import { useLocalizationSupport } from '../hooks/use-localization-support';
import { useOnboarding } from '../hooks/use-onboarding';
import { isWindows } from '../lib/app-globals';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import MainSidebar from './main-sidebar';
import Onboarding from './onboarding';
import { SiteContentTabs } from './site-content-tabs';
import TopBar from './top-bar';
import UserSettings from './user-settings';
import WindowsTitlebar from './windows-titlebar';

export default function App() {
useLocalizationSupport();
const { needsOnboarding } = useOnboarding();
const [ isSidebarVisible, setIsSidebarVisible ] = useState( true );
const toggleSidebar = () => {
getIpcApi().toggleMinWindowWidth( isSidebarVisible );
setIsSidebarVisible( ! isSidebarVisible );
};

return (
<>
Expand All @@ -35,12 +43,26 @@ export default function App() {
) }
spacing="0"
>
{ isWindows() && <WindowsTitlebar className="h-titlebar-win flex-shrink-0" /> }
{ isWindows() ? (
<WindowsTitlebar className="h-titlebar-win flex-shrink-0">
<TopBar onToggleSidebar={ toggleSidebar } />
</WindowsTitlebar>
) : (
<div className="pl-20 flex-shrink-0">
<TopBar onToggleSidebar={ toggleSidebar } />
</div>
) }

<HStack spacing="0" alignment="left" className="flex-grow">
<MainSidebar className="basis-52 flex-shrink-0 h-full" />
<MainSidebar
className={ cx(
'h-full transition-all duration-500',
isSidebarVisible ? 'basis-52 flex-shrink-0' : 'basis-0 !min-w-[10px]'
) }
/>
<main
data-testid="site-content"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden z-10"
>
<SiteContentTabs />
</main>
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function ButtonsSection( { buttonsArray, title, className = '' }: Buttons
return (
<div className="w-full">
<h2 className="a8c-subtitle-small mb-3">{ title }</h2>
<div className={ cx( 'gap-3', className || 'grid sd:grid-cols-2 lg:grid-cols-3' ) }>
<div className={ cx( 'gap-3', className || 'grid grid-cols-2 lg:grid-cols-3' ) }>
{ buttonsArray.map( ( button, index ) => (
<Button
className={ button.className }
Expand Down
2 changes: 1 addition & 1 deletion src/components/content-tab-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const ImportSite = ( props: { selectedSite: SiteDetails } ) => {
>
<div
className={ cx(
'h-48 w-full rounded-sm border border-zinc-300 flex-col justify-center items-center inline-flex',
'h-36 w-full rounded-sm border border-zinc-300 flex-col justify-center items-center inline-flex',
isDraggingOver && ! isImporting && 'border-a8c-blueberry bg-a8c-gray-0'
) }
>
Expand Down
12 changes: 5 additions & 7 deletions src/components/gravatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,26 @@ import profileIconDetailed from './profile-icon-detailed';
export function Gravatar( {
className,
isBlack = false,
isLarge = false,
size = 16,
detailedDefaultImage = false,
}: {
className?: string;
isBlack?: boolean;
isLarge?: boolean;
size?: number;
detailedDefaultImage?: boolean;
} ) {
const { __ } = useI18n();
const { user } = useAuth();
const gravatarUrl = useGravatarUrl( user?.email, isBlack, detailedDefaultImage );
const [ imageError, setImageError ] = useState( false );

const childClassName = cx(
isLarge ? 'w-[32px] h-[32px] rounded-full' : 'w-[16px] h-[16px] rounded-full',
className
);
const childClassName = cx( 'rounded-full', className );

const renderDefaultGravatarIcon = () => (
<Icon
icon={ detailedDefaultImage ? profileIconDetailed : commentAuthorAvatar }
viewBox={ detailedDefaultImage ? '0 0 32 33' : '4 4 16 16' }
size={ isLarge ? 32 : 16 }
size={ size }
className={ childClassName }
/>
);
Expand All @@ -47,6 +44,7 @@ export function Gravatar( {
alt={ __( 'User avatar' ) }
className={ childClassName }
onError={ () => setImageError( true ) }
style={ { width: `${ size }px`, height: `${ size }px` } }
/>
);
}
113 changes: 2 additions & 111 deletions src/components/main-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,131 +1,25 @@
import { __ } from '@wordpress/i18n';
import { Icon, help } from '@wordpress/icons';
import { STUDIO_DOCS_URL } from '../constants';
import { useAuth } from '../hooks/use-auth';
import { useOffline } from '../hooks/use-offline';
import { isMac } from '../lib/app-globals';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import AddSite from './add-site';
import Button from './button';
import { Gravatar } from './gravatar';
import offlineIcon from './offline-icon';
import { RunningSites } from './running-sites';
import SiteMenu from './site-menu';
import Tooltip from './tooltip';
import { WordPressLogo } from './wordpress-logo';

interface MainSidebarProps {
className?: string;
}

function SidebarAuthFooter() {
const { isAuthenticated, authenticate } = useAuth();
const isOffline = useOffline();
const offlineMessage = __( 'You’re currently offline.' );
const openDocs = async () => {
await getIpcApi().openURL( STUDIO_DOCS_URL );
};
if ( isAuthenticated ) {
return (
<nav aria-label={ __( 'Global' ) }>
<ul className="flex items-start self-stretch w-full">
<li>
<Button
onClick={ () => getIpcApi().showUserSettings() }
aria-label={ __( 'Account' ) }
variant="icon"
>
<Gravatar className="m-1" />
</Button>
</li>
<li className="ml-1.5">
<Button onClick={ openDocs } aria-label={ __( 'Help' ) } variant="icon">
<Icon size={ 22 } className="m-px text-white" icon={ help } />
</Button>
</li>
</ul>
</nav>
);
}

return (
<Tooltip
disabled={ ! isOffline }
icon={ offlineIcon }
text={ offlineMessage }
placement="right"
className="flex"
>
<Button
aria-description={ isOffline ? offlineMessage : '' }
aria-disabled={ isOffline }
className="flex gap-x-2 justify-between w-full text-white rounded !px-0 py-1 h-auto active:!text-white hover:!text-white hover:underline items-end"
onClick={ () => {
if ( isOffline ) {
return;
}
authenticate();
} }
>
<WordPressLogo />

<div className="text-xs text-right">{ __( 'Log in' ) }</div>
</Button>
</Tooltip>
);
}

function SidebarToolbar() {
const isOffline = useOffline();
const offlineMessage = [
__( 'You’re currently offline.' ),
__( 'Some features will be unavailable.' ),
];
return (
<div
className={ cx(
'absolute right-4 app-no-drag-region',
isMac() && 'top-1',
! isMac() && 'top-0'
) }
>
{ isOffline && (
<Tooltip
text={
<span>
{ offlineMessage[ 0 ] }
<br />
{ offlineMessage[ 1 ] }
</span>
}
>
<Button
aria-label={ __( 'Offline indicator' ) }
aria-description={ offlineMessage.join( ' ' ) }
className="cursor-default"
variant="icon"
>
<Icon className="m-1 text-white" size={ 16 } icon={ offlineIcon } />
</Button>
</Tooltip>
) }
</div>
);
}

export default function MainSidebar( { className }: MainSidebarProps ) {
return (
<div
data-testid="main-sidebar"
className={ cx(
'text-chrome-inverted relative',
isMac() && 'pt-[50px]',
isMac() && 'pt-[10px]',
! isMac() && 'pt-[38px]',
className
) }
>
<SidebarToolbar />
<div className="flex flex-col h-full">
<div
className={ cx(
Expand All @@ -138,10 +32,7 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
<div className="flex flex-col gap-4 pt-5 border-white border-t border-opacity-10 app-no-drag-region">
<RunningSites />
<div className={ cx( isMac() ? 'mx-5' : 'mx-4' ) }>
<AddSite className="w-full mb-4" />
<div className="mb-[6px]">
<SidebarAuthFooter />
</div>
<AddSite className="min-w-[168px] w-full mb-4" />
</div>
</div>
</div>
Expand Down
59 changes: 1 addition & 58 deletions src/components/tests/main-sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { fireEvent, render, act, waitFor, screen } from '@testing-library/react';
import { render, act, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { useAuth } from '../../hooks/use-auth';
import { useOffline } from '../../hooks/use-offline';
import MainSidebar from '../main-sidebar';

jest.mock( '../../hooks/use-auth' );
Expand Down Expand Up @@ -66,23 +65,6 @@ describe( 'MainSidebar Footer', () => {
expect( screen.getByRole( 'button', { name: 'Add site' } ) ).toBeVisible();
} );

it( 'Test unauthenticated footer has the Log in button', async () => {
const user = userEvent.setup();
const authenticate = jest.fn();
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false, authenticate } );
await act( async () => render( <MainSidebar /> ) );
expect( screen.getByRole( 'button', { name: 'Log in' } ) ).toBeVisible();
await user.click( screen.getByRole( 'button', { name: 'Log in' } ) );
expect( authenticate ).toHaveBeenCalledTimes( 1 );
} );

it( 'Test authenticated footer does not have the log in button and it has the settings and account buttons', async () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
await act( async () => render( <MainSidebar /> ) );
expect( screen.queryByRole( 'button', { name: 'Log in' } ) ).not.toBeInTheDocument();
expect( screen.getByRole( 'button', { name: 'Account' } ) ).toBeVisible();
} );

it( 'applies className prop', async () => {
const { container } = await act( async () =>
render( <MainSidebar className={ 'test-class' } /> )
Expand All @@ -94,30 +76,6 @@ describe( 'MainSidebar Footer', () => {
await act( async () => render( <MainSidebar /> ) );
expect( screen.getByRole( 'button', { name: 'Stop all' } ) ).toBeVisible();
} );

it( 'disables log in button when offline', async () => {
( useOffline as jest.Mock ).mockReturnValue( true );
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false } );
await act( async () => render( <MainSidebar /> ) );
const loginButton = screen.getByRole( 'button', { name: 'Log in' } );
expect( loginButton ).toHaveAttribute( 'aria-disabled', 'true' );
fireEvent.mouseOver( loginButton );
expect( screen.getByRole( 'tooltip', { name: 'You’re currently offline.' } ) ).toBeVisible();
} );

it( 'shows offline indicator', async () => {
( useOffline as jest.Mock ).mockReturnValue( true );
await act( async () => render( <MainSidebar /> ) );
const offlineIndicator = screen.getByRole( 'button', {
name: 'Offline indicator',
} );
fireEvent.mouseOver( offlineIndicator );
expect(
screen.getByRole( 'tooltip', {
name: 'You’re currently offline. Some features will be unavailable.',
} )
).toBeVisible();
} );
} );

describe( 'MainSidebar Site Menu', () => {
Expand Down Expand Up @@ -155,19 +113,4 @@ describe( 'MainSidebar Site Menu', () => {
'0e9e237b-335a-43fa-b439-9b078a613333'
);
} );

it( 'opens the support URL', async () => {
const user = userEvent.setup();
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );

render( <MainSidebar /> );

const helpIconButton = screen.getByRole( 'button', { name: 'Help' } );
await user.click( helpIconButton );
await waitFor( () =>
expect( mockOpenURL ).toHaveBeenCalledWith(
`https://developer.wordpress.com/docs/developer-tools/studio/`
)
);
} );
} );
Loading