Add basic email preview to Emails settings (#52685)

* Create slotfill for React component in email settings for email preview

* Render email preview iframe

* Style email preview container

* Add email preview device type toggle

* Change email preview width when device type changes

* Show email preview subject and sender

* Add changelog

* Fix linter errors

* Add e2e tests for email preview
This commit is contained in:
Ján Mikláš 2024-11-12 15:40:21 +01:00 committed by GitHub
parent aa19636080
commit 2f0f30369b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 378 additions and 1 deletions

View File

@ -0,0 +1,5 @@
Significance: patch
Type: add
Comment: Add basic email preview in email settings. Behind a hidden experimental feature, not yet for public use

View File

@ -20,12 +20,14 @@ initRemoteLogging();
*/
import './stylesheets/_index.scss';
import { getAdminSetting } from '~/utils/admin-settings';
import { isFeatureEnabled } from '~/utils/features';
import { PageLayout, EmbedLayout, PrimaryLayout as NoticeArea } from './layout';
import { EmbeddedBodyLayout } from './embedded-body-layout';
import './xstate.js';
import { deriveWpAdminBackgroundColours } from './utils/derive-wp-admin-background-colours';
import { possiblyRenderSettingsSlots } from './settings/settings-slots';
import { registerTaxSettingsConflictErrorFill } from './settings/conflict-error-slotfill';
import { registerSettingsEmailPreviewFill } from './settings-email/settings-email-preview-slotfill';
import { registerPaymentsSettingsBannerFill } from './payments/payments-settings-banner-slotfill';
import { registerSiteVisibilitySlotFill } from './launch-your-store';
import {
@ -123,6 +125,10 @@ if ( appRoot ) {
possiblyRenderOrderAttributionSlot();
registerOrderAttributionSlotFill();
if ( isFeatureEnabled( 'email_improvements' ) ) {
registerSettingsEmailPreviewFill();
}
}
// Render the CustomerEffortScoreTracksContainer only if

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.25 16.4371C6.16445 15.2755 5.5 13.7153 5.5 12C5.5 8.41015 8.41015 5.5 12 5.5C15.5899 5.5 18.5 8.41015 18.5 12C18.5 13.7153 17.8356 15.2755 16.75 16.4371V16C16.75 14.4812 15.5188 13.25 14 13.25H10C8.48122 13.25 7.25 14.4812 7.25 16V16.4371ZM8.75 17.6304C9.70606 18.1835 10.8161 18.5 12 18.5C13.1839 18.5 14.2939 18.1835 15.25 17.6304V16C15.25 15.3096 14.6904 14.75 14 14.75H10C9.30964 14.75 8.75 15.3096 8.75 16V17.6304ZM4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM14 10C14 11.1046 13.1046 12 12 12C10.8954 12 10 11.1046 10 10C10 8.89543 10.8954 8 12 8C13.1046 8 14 8.89543 14 10Z" fill="#8526FF"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.97266 8C4.97266 7.30964 5.5323 6.75 6.22266 6.75H17.7782C18.4686 6.75 19.0282 7.30964 19.0282 8V16.3611H4.97266V8Z" stroke="#fff" stroke-width="1.5"/>
<path d="M2 17.5C2 16.6716 2.67157 16 3.5 16H20.5C21.3284 16 22 16.6716 22 17.5H2Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.97266 8C4.97266 7.30964 5.5323 6.75 6.22266 6.75H17.7782C18.4686 6.75 19.0282 7.30964 19.0282 8V16.3611H4.97266V8Z" stroke="#1e1e1e" stroke-width="1.5"/>
<path d="M2 17.5C2 16.6716 2.67157 16 3.5 16H20.5C21.3284 16 22 16.6716 22 17.5H2Z" fill="#1e1e1e"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7.75" y="4.75" width="8.5" height="14.5" rx="1.25" stroke="#fff" stroke-width="1.5"/>
<rect x="11" y="16" width="2" height="1.5" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7.75" y="4.75" width="8.5" height="14.5" rx="1.25" stroke="#1e1e1e" stroke-width="1.5"/>
<rect x="11" y="16" width="2" height="1.5" fill="#1e1e1e"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import desktopIcon from './icon-desktop.svg';
import desktopActiveIcon from './icon-desktop-active.svg';
import mobileIcon from './icon-mobile.svg';
import mobileActiveIcon from './icon-mobile-active.svg';
export const DEVICE_TYPE_DESKTOP = 'desktop';
export const DEVICE_TYPE_MOBILE = 'mobile';
type EmailPreviewDeviceTypeProps = {
deviceType: string;
setDeviceType: ( deviceType: string ) => void;
};
export const EmailPreviewDeviceType: React.FC<
EmailPreviewDeviceTypeProps
> = ( { deviceType, setDeviceType } ) => {
const isDesktop = deviceType === DEVICE_TYPE_DESKTOP;
const isMobile = deviceType === DEVICE_TYPE_MOBILE;
const setDesktop = () => setDeviceType( DEVICE_TYPE_DESKTOP );
const setMobile = () => setDeviceType( DEVICE_TYPE_MOBILE );
return (
<div className="wc-settings-email-preview-device-type">
<button
className={ isDesktop ? 'active' : '' }
onClick={ setDesktop }
title={ __( 'Email preview on desktop', 'woocommerce' ) }
type="button"
>
<img
src={ isDesktop ? desktopActiveIcon : desktopIcon }
alt={ __( 'Desktop icon', 'woocommerce' ) }
/>
</button>
<button
className={ isMobile ? 'active' : '' }
onClick={ setMobile }
title={ __( 'Mobile preview on desktop', 'woocommerce' ) }
type="button"
>
<img
src={ isMobile ? mobileActiveIcon : mobileIcon }
alt={ __( 'Mobile icon', 'woocommerce' ) }
/>
</button>
</div>
);
};

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { SETTINGS_STORE_NAME } from '@woocommerce/data';
import { __ } from '@wordpress/i18n';
import { resolveSelect } from '@wordpress/data';
import { useEffect, useState } from 'react';
/**
* Internal dependencies
*/
import avatarIcon from './icon-avatar.svg';
type FromSettings = {
woocommerce_email_from_name?: string;
woocommerce_email_from_address?: string;
};
export const EmailPreviewHeader: React.FC = () => {
const [ fromName, setFromName ] = useState( '' );
const [ fromAddress, setFromAddress ] = useState( '' );
useEffect( () => {
const fetchSettings = async () => {
const {
woocommerce_email_from_name = '',
woocommerce_email_from_address = '',
} = (
await resolveSelect( SETTINGS_STORE_NAME ).getSettings(
'email'
)
).email as FromSettings;
setFromName( woocommerce_email_from_name );
setFromAddress( woocommerce_email_from_address );
};
fetchSettings();
}, [] );
return (
<div className="wc-settings-email-preview-header">
<h3 className="wc-settings-email-preview-header-subject">
Your SampleStore order is now complete
</h3>
<div className="wc-settings-email-preview-header-data">
<div className="wc-settings-email-preview-header-icon">
<img
src={ avatarIcon }
alt={ __( 'Avatar icon', 'woocommerce' ) }
/>
</div>
<div className="wc-settings-email-preview-header-sender">
{ fromName }
<span>{ fromAddress }</span>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,66 @@
/**
* External dependencies
*/
import { createSlotFill } from '@wordpress/components';
import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { useState } from 'react';
/**
* Internal dependencies
*/
import './style.scss';
import { SETTINGS_SLOT_FILL_CONSTANT } from '~/settings/settings-slots';
import {
EmailPreviewDeviceType,
DEVICE_TYPE_DESKTOP,
} from './settings-email-preview-device-type';
import { EmailPreviewHeader } from './settings-email-preview-header';
const { Fill } = createSlotFill( SETTINGS_SLOT_FILL_CONSTANT );
type EmailPreviewFillProps = {
previewUrl: string;
};
const EmailPreviewFill: React.FC< EmailPreviewFillProps > = ( {
previewUrl,
} ) => {
const [ deviceType, setDeviceType ] =
useState< string >( DEVICE_TYPE_DESKTOP );
return (
<Fill>
<div className="wc-settings-email-preview-container">
<div className="wc-settings-email-preview-controls">
<EmailPreviewDeviceType
deviceType={ deviceType }
setDeviceType={ setDeviceType }
/>
</div>
<div
className={ `wc-settings-email-preview wc-settings-email-preview-${ deviceType }` }
>
<EmailPreviewHeader />
<iframe
src={ previewUrl }
title={ __( 'Email preview frame', 'woocommerce' ) }
/>
</div>
</div>
</Fill>
);
};
export const registerSettingsEmailPreviewFill = () => {
const slot_element_id = 'wc_settings_email_preview_slotfill';
const slot_element = document.getElementById( slot_element_id );
const preview_url = slot_element?.getAttribute( 'data-preview-url' );
if ( ! preview_url ) {
return null;
}
registerPlugin( 'woocommerce-admin-settings-email-preview', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-email-preview-settings',
render: () => <EmailPreviewFill previewUrl={ preview_url } />,
} );
};

View File

@ -0,0 +1,113 @@
$wc-setting-email-preview-gap: 16px;
.wc-settings-email-preview-container {
background: $gray-100;
border: 1px solid $gray-200;
border-radius: 4px;
display: grid;
grid-gap: $wc-setting-email-preview-gap;
grid-template-rows: 36px 1fr;
height: 960px;
padding: $wc-setting-email-preview-gap;
width: 652px;
}
.wc-settings-email-preview-device-type {
display: grid;
grid-gap: 8px;
grid-template-columns: 32px 32px;
button {
align-items: center;
background: none;
border: none;
border-radius: 2px;
cursor: pointer;
display: flex;
justify-content: center;
height: 32px;
width: 32px;
&:hover,
&:focus {
background: $gray-200;
}
&.active {
background: #1e1e1e;
&:hover,
&:focus {
background: #000;
}
}
}
}
.wc-settings-email-preview {
background: #fff;
border: 1px solid $gray-200;
border-radius: 4px;
display: grid;
grid-template-rows: auto 1fr;
margin: 0 auto;
transition: max-width 0.3s ease-in-out;
width: 100%;
iframe {
border-radius: 0 0 3px 3px;
display: block;
height: 100%;
width: 100%;
}
}
.wc-settings-email-preview-desktop {
max-width: 650px;
}
.wc-settings-email-preview-mobile {
max-width: 360px;
}
.wc-settings-email-preview-header {
border-bottom: 1px solid $gray-200;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: $wc-setting-email-preview-gap;
}
.wc-settings-email-preview-header-subject {
font-size: 21px;
font-weight: normal;
line-height: 21px;
margin: 0 0 $wc-setting-email-preview-gap;
}
.wc-settings-email-preview-header-data {
align-items: center;
display: flex;
}
.wc-settings-email-preview-header-icon {
align-items: center;
background: rgba(#cbbeff, 0.3);
border-radius: 16px;
display: flex;
height: 32px;
justify-content: center;
margin-right: 8px;
width: 32px;
}
.wc-settings-email-preview-header-sender {
font-size: 13px;
font-weight: 600;
span {
font-size: 11px;
font-weight: 400;
margin-left: 4px;
}
}

View File

@ -26,6 +26,10 @@ export const possiblyRenderSettingsSlots = () => {
id: 'wc_settings_blueprint_slotfill',
scope: 'woocommerce-blueprint-settings',
},
{
id: 'wc_settings_email_preview_slotfill',
scope: 'woocommerce-email-preview-settings',
},
];
slots.forEach( ( slot ) => {

View File

@ -25,6 +25,7 @@ class WC_Settings_Emails extends WC_Settings_Page {
$this->label = __( 'Emails', 'woocommerce' );
add_action( 'woocommerce_admin_field_email_notification', array( $this, 'email_notification_setting' ) );
add_action( 'woocommerce_admin_field_email_preview', array( $this, 'email_preview' ) );
parent::__construct();
}
@ -208,6 +209,8 @@ class WC_Settings_Emails extends WC_Settings_Page {
'id' => 'email_template_options',
),
array( 'type' => 'email_preview' ),
array(
'title' => __( 'Store management insights', 'woocommerce' ),
'type' => 'title',
@ -378,6 +381,18 @@ class WC_Settings_Emails extends WC_Settings_Page {
</tr>
<?php
}
/**
* Creates the React mount point for the email preview.
*/
public function email_preview() {
?>
<div
id="wc_settings_email_preview_slotfill"
data-preview-url="<?php echo esc_url( wp_nonce_url( admin_url( '?preview_woocommerce_mail=true' ), 'preview-mail' ) ); ?>"
></div>
<?php
}
}
return new WC_Settings_Emails();

View File

@ -0,0 +1,35 @@
const { test, expect, request } = require( '@playwright/test' );
const { setOption } = require( '../../utils/options' );
const setFeatureFlag = async ( baseURL, value ) =>
await setOption(
request,
baseURL,
'woocommerce_feature_email_improvements_enabled',
value
);
test.describe( 'WooCommerce Email Settings', () => {
test.use( { storageState: process.env.ADMINSTATE } );
test( 'See email preview with a feature flag', async ( {
page,
baseURL,
} ) => {
const emailPreviewElement =
'#wc_settings_email_preview_slotfill iframe';
const hasIframe = async () => {
return ( await page.locator( emailPreviewElement ).count() ) > 0;
};
// Disable the email_improvements feature flag
await setFeatureFlag( baseURL, 'no' );
await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=email' );
expect( await hasIframe() ).toBeFalsy();
// Enable the email_improvements feature flag
await setFeatureFlag( baseURL, 'yes' );
await page.reload();
expect( await hasIframe() ).toBeTruthy();
} );
} );

View File

@ -70,7 +70,7 @@ class WC_Settings_Emails_Test extends WC_Settings_Unit_Test_Case {
$expected = array(
'email_notification_settings' => array( 'title', 'sectionend' ),
'' => 'email_notification',
'' => array( 'email_notification', 'email_preview' ),
'email_recipient_options' => 'sectionend',
'email_options' => array( 'title', 'sectionend' ),
'woocommerce_email_from_name' => 'text',