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:
parent
aa19636080
commit
2f0f30369b
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 } />,
|
||||
} );
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 ) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
} );
|
||||
} );
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue