Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Studio: Create new Previews tab and apply design changes #846

Merged
merged 54 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1e51b29
Create new tab
Jan 27, 2025
74d158a
Add no snapshots screen and copy
Jan 27, 2025
04df2a0
Add logic for creating a preview link and button
Jan 27, 2025
7c8b9ec
Add header on the top
Jan 27, 2025
b23b2e9
Add icon by Uploads
Jan 27, 2025
0c67b17
Refactor columns structure
Jan 27, 2025
23142fa
Add action items to the menu
Jan 27, 2025
3f068a2
Ensure site updates work
Jan 27, 2025
130f982
Implement loading state for updating
Jan 27, 2025
4f700a0
Center items horizontally
Jan 27, 2025
b93b29e
Extract Create preview link message
Jan 28, 2025
576cb15
Add create link button at the bottom of the connect sites
Jan 28, 2025
fd5a09b
Ensure that we display all snapshots per site
Jan 28, 2025
115dab9
Fix snapshots tab
Jan 28, 2025
d59af19
Add initial loading state for the first snapshot
Jan 28, 2025
bcb03ed
Add loading row
Jan 28, 2025
23f6665
Cleanup
Jan 28, 2025
4d2b2f7
Handle deleting progress
Jan 28, 2025
9ee8f1b
Cleanup unused imports
Jan 28, 2025
f00b94c
Minor design cleanup
Jan 28, 2025
8fbe6ff
Add small design fixes
Jan 28, 2025
6e0a151
Stick Create preview link button at the bottom
Jan 28, 2025
008d55e
Fix progress bar
Jan 28, 2025
25d2a91
Fix minor styling
Jan 28, 2025
3f2710e
Adjust padding
Jan 28, 2025
804fb3c
Create separate file for create button
Jan 28, 2025
08f5197
Fix create button positions
Jan 28, 2025
868cdb8
Move preview buttons
Jan 28, 2025
194acd8
Architecture changes
Jan 28, 2025
419e94a
Cleanup
Jan 28, 2025
eccfc82
Split files
Jan 28, 2025
32744e7
Remove unused variable
Jan 29, 2025
b1ecd28
Fix unit tests
Jan 29, 2025
240e6ca
Fix onClick handler
Jan 29, 2025
4b8919b
Ensure we do not prematurely expose the site
Jan 29, 2025
f696e4d
Improve progress bar and remove an extra prop
Jan 29, 2025
724588b
Remove icon
Jan 29, 2025
58d0296
Rename component
Jan 29, 2025
993fb0d
Fix filename
Jan 29, 2025
64f6aac
Update preview links to preview sites
Jan 29, 2025
1469e3f
Use confirmation dialog hook
Jan 29, 2025
4967852
Refactor confirmation dialog jook to use delete site
Jan 29, 2025
1f49927
Fix notifications
Jan 30, 2025
7cbf41c
Fix logic with loading snapshot
Jan 30, 2025
9d9f4d7
Remove unused prop
Jan 30, 2025
9e2c8b8
Change wording for generating a preview
Jan 30, 2025
949e9fb
Use constant for limit of sites per user
Jan 30, 2025
3c5a8d8
Merge branch 'trunk' of github.com:Automattic/studio into fix/impleme…
Jan 30, 2025
0acbd6b
Fix feature flag that was removed
Jan 30, 2025
11364ed
Rename header
Jan 30, 2025
e48a71b
Improve naming of preview site updating
Jan 30, 2025
a0ded47
Remove status text prop and default to -
Jan 30, 2025
fa0e3e5
Improve conditions
Jan 30, 2025
cc64885
Speed up the progress bar
Jan 30, 2025
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
230 changes: 230 additions & 0 deletions src/components/content-tab-previews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { check, external, Icon } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren } from 'react';
import { CLIENT_ID, PROTOCOL_PREFIX, SCOPES, WP_AUTHORIZE_ENDPOINT } from 'src/constants';
import { useArchiveSite } from 'src/hooks/use-archive-site';
import { useAuth } from 'src/hooks/use-auth';
import { useOffline } from 'src/hooks/use-offline';
import { useSnapshots } from 'src/hooks/use-snapshots';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { CreatePreviewButton } from 'src/modules/preview-site/components/create-preview-button';
import { PreviewLinksTableHeader } from 'src/modules/preview-site/components/preview-links-table-header';
import { PreviewSiteRow } from 'src/modules/preview-site/components/preview-site-row';
import { ProgressRow } from 'src/modules/preview-site/components/progress-row';
import Button from './button';
import offlineIcon from './offline-icon';
import { ScreenshotDemoSite } from './screenshot-demo-site';
import { Tooltip } from './tooltip';

interface ContentTabPreviewsProps {
selectedSite: SiteDetails;
}

function EmptyGeneric( {
children,
selectedSite,
}: PropsWithChildren< { selectedSite: SiteDetails } > ) {
const { __ } = useI18n();
return (
<div className="p-8 flex justify-between max-w-3xl gap-4">
<div className="flex flex-col">
<div className="a8c-subtitle mb-1">{ __( 'Share a preview of your Studio site' ) }</div>
<div className="w-[40ch] text-a8c-gray-70 a8c-body">
{ __(
'Get feedback from anyone, anywhere with a free hosted preview of your Studio site.'
) }
</div>
<div className="mt-6">
{ [
__( 'Create up to 10 preview sites for free.' ),
katinthehatsite marked this conversation as resolved.
Show resolved Hide resolved
__( 'Preview sites expire 7 days after the last update.' ),
createInterpolateElement( __( 'Powered by <a> WordPress.com</a>.' ), {
a: (
<Button
variant="link"
className="whitespace-pre"
onClick={ () =>
getIpcApi().openURL(
'https://wordpress.com/?utm_source=studio&utm_medium=referral&utm_campaign=demo_sites_onboarding'
)
}
/>
),
} ),
].map( ( text ) => (
<div
key={ typeof text === 'string' ? text : 'wordpress-com' }
className="text-a8c-gray-70 a8c-body flex items-center"
>
<Icon className="fill-a8c-blueberry ltr:mr-2 rtl:ml-2 shrink-0" icon={ check } />
{ text }
</div>
) ) }
</div>
{ children }
</div>
<div className="flex flex-col shrink-0 items-end">
<ScreenshotDemoSite site={ selectedSite } />
</div>
</div>
);
}

function NoAuth( { selectedSite }: React.ComponentProps< typeof EmptyGeneric > ) {
const isOffline = useOffline();
const { __ } = useI18n();
const { authenticate } = useAuth();
const offlineMessage = __( "You're currently offline." );

return (
<EmptyGeneric selectedSite={ selectedSite }>
<div className="mt-8">
<Tooltip disabled={ ! isOffline } icon={ offlineIcon } text={ offlineMessage }>
<Button
aria-description={ isOffline ? offlineMessage : '' }
aria-disabled={ isOffline }
variant="primary"
onClick={ () => {
if ( isOffline ) {
return;
}
authenticate();
} }
>
{ __( 'Log in to WordPress.com' ) }
<Icon className="ltr:ml-1 rtl:mr-1 rtl:scale-x-[-1]" icon={ external } size={ 21 } />
</Button>
</Tooltip>
</div>
<div className="mt-3 w-[40ch] text-a8c-gray-70 a8c-body">
<Tooltip
disabled={ ! isOffline }
icon={ offlineIcon }
text={ offlineMessage }
placement="bottom-start"
>
{ createInterpolateElement(
__(
'A WordPress.com account is required to create preview sites. <a>Create a free account</a>'
),
{
a: (
<Button
aria-description={ isOffline ? offlineMessage : '' }
aria-disabled={ isOffline }
className="!p-0 text-a8c-blueberry hover:opacity-80 h-auto"
onClick={ () => {
if ( isOffline ) {
return;
}
const baseURL = 'https://wordpress.com/log-in/link';
const authURL = encodeURIComponent(
`${ WP_AUTHORIZE_ENDPOINT }?response_type=token&client_id=${ CLIENT_ID }&redirect_uri=${ PROTOCOL_PREFIX }%3A%2F%2Fauth&scope=${ SCOPES }&from-calypso=1`
);
const finalURL = `${ baseURL }?redirect_to=${ authURL }&client_id=${ CLIENT_ID }`;
getIpcApi().openURL( finalURL );
} }
/>
),
}
) }
</Tooltip>
</div>
</EmptyGeneric>
);
}

function NoPreviews( { selectedSite }: React.ComponentProps< typeof EmptyGeneric > ) {
const { archiveSite } = useArchiveSite();

return (
<EmptyGeneric selectedSite={ selectedSite }>
<div className="mt-8">
<CreatePreviewButton
onClick={ () => archiveSite( selectedSite.id ) }
selectedSite={ selectedSite }
/>
</div>
</EmptyGeneric>
);
}

export function ContentTabPreviews( { selectedSite }: ContentTabPreviewsProps ) {
katinthehatsite marked this conversation as resolved.
Show resolved Hide resolved
const { __ } = useI18n();
const { snapshots } = useSnapshots();
const { isAuthenticated } = useAuth();
const { archiveSite, isUploadingSiteId } = useArchiveSite();
const isUploading = isUploadingSiteId( selectedSite.id );
const isSnapshotLoading = snapshots.some( ( snapshot ) => snapshot.isLoading );

if ( ! isAuthenticated ) {
return <NoAuth selectedSite={ selectedSite } />;
}

const snapshotsOnSite = snapshots.filter(
( snapshot ) => snapshot.localSiteId === selectedSite.id
);

if ( ! snapshotsOnSite.length ) {
if ( isUploading || isSnapshotLoading ) {
return (
<div className="relative min-h-full flex flex-col">
<div className="w-full flex flex-col flex-1">
<div className="flex-1">
<PreviewLinksTableHeader />
<div className="[&>*:not(:last-child)]:border-b [&>*]:border-a8c-gray-5">
{ ( isUploading || isSnapshotLoading ) && (
<ProgressRow
text={ __( 'Generating preview site' ) }
katinthehatsite marked this conversation as resolved.
Show resolved Hide resolved
statusText={ __( 'Just now' ) }
katinthehatsite marked this conversation as resolved.
Show resolved Hide resolved
/>
) }
</div>
</div>
<div className="sticky bottom-0 bg-white/[0.8] backdrop-blur-sm w-full px-8 py-6 mt-auto">
<CreatePreviewButton
onClick={ () => archiveSite( selectedSite.id ) }
selectedSite={ selectedSite }
/>
</div>
</div>
</div>
);
}
return <NoPreviews selectedSite={ selectedSite } />;
}

return (
<div className="relative min-h-full flex flex-col">
<div className="w-full flex flex-col flex-1">
<div className="flex-1">
<PreviewLinksTableHeader />
<div className="[&>*:not(:last-child)]:border-b [&>*]:border-a8c-gray-5">
{ ( isUploading || isSnapshotLoading ) && (
<ProgressRow
text={ __( 'Generating preview site' ) }
statusText={ __( 'Just now' ) }
/>
) }
{ snapshotsOnSite.map( ( snapshot ) => (
<PreviewSiteRow
snapshot={ snapshot }
previousSnapshot={ null }
selectedSite={ selectedSite }
key={ snapshot.atomicSiteId }
/>
) ) }
</div>
</div>
<div className="sticky bottom-0 bg-white/[0.8] backdrop-blur-sm w-full px-8 py-6 mt-auto">
<CreatePreviewButton
onClick={ () => archiveSite( selectedSite.id ) }
selectedSite={ selectedSite }
/>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/content-tab-snapshots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function SnapshotRow( {
const isUploading = isUploadingSiteId( selectedSite.id );
const { updateDemoSite, isDemoSiteUpdating } = useUpdateDemoSite();
const errorMessages = useArchiveErrorMessages();
const isSiteDemoUpdating = isDemoSiteUpdating( snapshot.localSiteId );
const isSiteDemoUpdating = isDemoSiteUpdating( snapshot.atomicSiteId );
const { formatRelativeTime } = useFormatLocalizedTimestamps();

const { isOverLimit } = useSiteSize( selectedSite.id );
Expand Down
2 changes: 2 additions & 0 deletions src/components/site-content-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WelcomeMessagesProvider } from '../hooks/use-welcome-messages';
import { ContentTabAssistant } from './content-tab-assistant';
import { ContentTabImportExport } from './content-tab-import-export';
import { ContentTabOverview } from './content-tab-overview';
import { ContentTabPreviews } from './content-tab-previews';
import { ContentTabSettings } from './content-tab-settings';
import { ContentTabSnapshots } from './content-tab-snapshots';
import { ContentTabSync } from './content-tab-sync';
Expand Down Expand Up @@ -47,6 +48,7 @@ export function SiteContentTabs() {
<div className="h-full">
{ name === 'overview' && <ContentTabOverview selectedSite={ selectedSite } /> }
{ name === 'share' && <ContentTabSnapshots selectedSite={ selectedSite } /> }
{ name === 'previews' && <ContentTabPreviews selectedSite={ selectedSite } /> }
{ name === 'sync' && <ContentTabSync selectedSite={ selectedSite } /> }
{ name === 'settings' && <ContentTabSettings selectedSite={ selectedSite } /> }
{ name === 'assistant' && (
Expand Down
16 changes: 8 additions & 8 deletions src/hooks/tests/use-update-demo-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe( 'useUpdateDemoSite', () => {
} );

// Assert that 'isDemoSiteUpdating' is set back to false
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false );

// Assert that demo site is updated with a new expiration date
expect( updateSnapshotMock ).toHaveBeenCalledWith(
Expand Down Expand Up @@ -144,7 +144,7 @@ describe( 'useUpdateDemoSite', () => {
} );

// Assert that 'isDemoSiteUpdating' is set back to false
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false );
} );

it( 'should allow updating two sites independently with different completion times', async () => {
Expand Down Expand Up @@ -184,8 +184,8 @@ describe( 'useUpdateDemoSite', () => {
} );

// Initially, both sites should be marked as updating
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( true );
expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( true );
expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( true );
expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( true );

// Wait for the first site to complete
await act( async () => {
Expand All @@ -194,8 +194,8 @@ describe( 'useUpdateDemoSite', () => {
} );

// After 1000ms, the first site should be done, but the second should still be updating
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( true );
expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( true );

// Wait for the second site to complete
await act( async () => {
Expand All @@ -204,8 +204,8 @@ describe( 'useUpdateDemoSite', () => {
} );

// After another 1000ms, both sites should be done updating
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false );
expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( false );

// Assert that the update function was called for both sites
expect( clientReqPost ).toHaveBeenCalledTimes( 2 );
Expand Down
13 changes: 9 additions & 4 deletions src/hooks/use-confirmation-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ interface ConfirmationDialogOptions {
checkboxLabel?: string;
confirmButtonLabel: string;
cancelButtonLabel?: string;
localStorageKey: string;
type?: 'none' | 'info' | 'error' | 'question' | 'warning';
localStorageKey?: string;
}

export function useConfirmationDialog( options: ConfirmationDialogOptions ) {
Expand All @@ -19,27 +20,31 @@ export function useConfirmationDialog( options: ConfirmationDialogOptions ) {
confirmButtonLabel,
cancelButtonLabel = __( 'Cancel' ),
localStorageKey,
type,
} = options;

return async ( onConfirm: () => void, { detail: detailOverride }: { detail?: string } = {} ) => {
if ( localStorage.getItem( localStorageKey ) === 'true' ) {
if ( localStorageKey && localStorage.getItem( localStorageKey ) === 'true' ) {
onConfirm();
return;
}

const CONFIRM_BUTTON_INDEX = 0;
const CANCEL_BUTTON_INDEX = 1;
const { response, checkboxChecked } = await getIpcApi().showMessageBox( {
type,
message,
detail: detailOverride ?? detail,
checkboxLabel,
...( localStorageKey && {
checkboxLabel: checkboxLabel ?? __( "Don't show this warning again" ),
} ),
buttons: [ confirmButtonLabel, cancelButtonLabel ],
cancelId: CANCEL_BUTTON_INDEX,
} );

if ( response === CONFIRM_BUTTON_INDEX ) {
// Confirm button is always the first button
if ( checkboxChecked ) {
if ( localStorageKey && checkboxChecked ) {
localStorage.setItem( localStorageKey, 'true' );
}
onConfirm();
Expand Down
Loading
Loading