Migrate email editor content validation rules to the MP plugin

MAILPOET-6432
This commit is contained in:
Oluwaseun Olorunsola
2025-02-06 15:20:44 +01:00
committed by Rostislav Wolný
parent e2286167d8
commit 5190ac1ff2
4 changed files with 130 additions and 91 deletions

View File

@@ -6,11 +6,18 @@ import { addFilter, addAction } from '@wordpress/hooks';
import { MailPoet } from 'mailpoet';
import { withSatismeterSurvey } from './satismeter-survey';
import './index.scss';
import { ValidateEmailContent } from './validate-email-content';
addFilter('mailpoet_email_editor_wrap_editor_component', 'mailpoet', (editor) =>
withSatismeterSurvey(editor),
);
addFilter(
'mailpoet_email_editor_content_validation_rules',
'mailpoet',
(validationRules: []) => [...validationRules, ...ValidateEmailContent()],
);
const EVENTS_TO_TRACK = [
'email_editor_events_editor_layout_loaded', // email editor was opened
'email_editor_events_template_select_modal_template_selected', // a template was selected from the template-select modal

View File

@@ -0,0 +1,93 @@
import { useMemo } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreDataStore } from '@wordpress/core-data';
const emailEditorStore = 'email-editor/editor';
const contentLink = `<a data-link-href='[mailpoet/subscription-unsubscribe-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${__(
'Unsubscribe',
'mailpoet',
)}</a> | <a data-link-href='[mailpoet/subscription-manage-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${__(
'Manage subscription',
'mailpoet',
)}</a>`;
export function ValidateEmailContent() {
const { contentBlockId, hasFooter } = useSelect((select) => {
const allBlocks = select(blockEditorStore).getBlocks();
const noBodyBlocks = allBlocks.filter(
(block) =>
block.name !== 'mailpoet/powered-by-mailpoet' &&
block.name !== 'core/post-content',
);
// @ts-expect-error getBlocksByName is not defined in types
const blocks = select(blockEditorStore).getBlocksByName(
'core/post-content',
) as string[] | undefined;
return {
contentBlockId: blocks?.[0],
hasFooter: noBodyBlocks.length > 0,
};
});
/* eslint-disable @typescript-eslint/ban-ts-comment */
const { editedTemplateContent, postTemplateId } = useSelect((mapSelect) => ({
editedTemplateContent:
// @ts-ignore
mapSelect(emailEditorStore).getCurrentTemplateContent() as string,
postTemplateId:
// @ts-ignore
mapSelect(emailEditorStore).getCurrentTemplate()?.id as string,
}));
return useMemo(() => {
const linksParagraphBlock = createBlock('core/paragraph', {
align: 'center',
fontSize: 'small',
content: contentLink,
});
return [
{
id: 'missing-unsubscribe-link',
test: (emailContent: string) =>
!emailContent.includes('[mailpoet/subscription-unsubscribe-url]'),
message: __(
'All emails must include an "Unsubscribe" link.',
'mailpoet',
),
actions: [
{
label: __('Insert link', 'mailpoet'),
onClick: () => {
if (!hasFooter) {
void dispatch(blockEditorStore).insertBlock(
linksParagraphBlock,
undefined,
contentBlockId,
);
} else {
void dispatch(coreDataStore).editEntityRecord(
'postType',
'wp_template',
postTemplateId,
{
content: `
${editedTemplateContent}
<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
${contentLink}
<!-- /wp:paragraph -->
`,
},
);
}
},
},
],
},
];
}, [contentBlockId, postTemplateId, hasFooter, editedTemplateContent]);
}

View File

@@ -1,123 +1,50 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useCallback, useMemo } from '@wordpress/element';
import { dispatch, useSelect, subscribe } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { createBlock } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';
import { useCallback } from '@wordpress/element';
import { useSelect, subscribe } from '@wordpress/data';
import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { storeName as emailEditorStore } from '../store';
import {
EmailContentValidationRule,
storeName as emailEditorStore,
} from '../store';
import { useShallowEqual } from './use-shallow-equal';
import { useValidationNotices } from './use-validation-notices';
// Shared reference to an empty array for cases where it is important to avoid
// returning a new array reference on every invocation
const EMPTY_ARRAY = [];
export type ContentValidationData = {
isInvalid: boolean;
validateContent: () => boolean;
};
export const useContentValidation = (): ContentValidationData => {
const { contentBlockId, hasFooter } = useSelect( ( select ) => {
const allBlocks = select( blockEditorStore ).getBlocks();
const noBodyBlocks = allBlocks.filter(
( block ) =>
block.name !== 'mailpoet/powered-by-mailpoet' &&
block.name !== 'core/post-content'
);
// @ts-expect-error getBlocksByName is not defined in types
const blocks = select( blockEditorStore ).getBlocksByName(
'core/post-content'
) as string[] | undefined;
return {
contentBlockId: blocks?.[ 0 ],
hasFooter: noBodyBlocks.length > 0,
};
} );
const { addValidationNotice, hasValidationNotice, removeValidationNotice } =
useValidationNotices();
const { editedContent, editedTemplateContent, postTemplateId } = useSelect(
const { editedContent, editedTemplateContent } = useSelect(
( mapSelect ) => ( {
editedContent:
mapSelect( emailEditorStore ).getEditedEmailContent(),
editedTemplateContent:
mapSelect( emailEditorStore ).getCurrentTemplateContent(),
postTemplateId:
mapSelect( emailEditorStore ).getCurrentTemplate()?.id,
} )
);
const rules: EmailContentValidationRule[] = applyFilters(
'mailpoet_email_editor_content_validation_rules',
EMPTY_ARRAY
) as EmailContentValidationRule[];
const content = useShallowEqual( editedContent );
const templateContent = useShallowEqual( editedTemplateContent );
const contentLink = `<a data-link-href='[mailpoet/subscription-unsubscribe-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${ __(
'Unsubscribe',
'mailpoet'
) }</a> | <a data-link-href='[mailpoet/subscription-manage-url]' contenteditable='false' style='text-decoration: underline;' class='mailpoet-email-editor__personalization-tags-link'>${ __(
'Manage subscription',
'mailpoet'
) }</a>`;
const rules = useMemo( () => {
const linksParagraphBlock = createBlock( 'core/paragraph', {
align: 'center',
fontSize: 'small',
content: contentLink,
} );
return [
{
id: 'missing-unsubscribe-link',
test: ( emailContent ) =>
! emailContent.includes(
'[mailpoet/subscription-unsubscribe-url]'
),
message: __(
'All emails must include an "Unsubscribe" link.',
'mailpoet'
),
actions: [
{
label: __( 'Insert link', 'mailpoet' ),
onClick: () => {
if ( ! hasFooter ) {
void dispatch( blockEditorStore ).insertBlock(
linksParagraphBlock,
undefined,
contentBlockId
);
} else {
void dispatch( coreDataStore ).editEntityRecord(
'postType',
'wp_template',
postTemplateId,
{
content: `
${ editedTemplateContent }
<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
${ contentLink }
<!-- /wp:paragraph -->
`,
}
);
}
},
},
],
},
];
}, [
contentBlockId,
hasFooter,
contentLink,
postTemplateId,
editedTemplateContent,
] );
const validateContent = useCallback( (): boolean => {
let isValid = true;
rules.forEach( ( { id, test, message, actions } ) => {

View File

@@ -269,3 +269,15 @@ export type EmailEditorPostType = Omit< Post, 'type' > & {
type: string;
mailpoet_data?: MailPoetEmailPostContentExtended;
};
export type EmailContentValidationAction = {
label: string;
onClick: () => void;
};
export type EmailContentValidationRule = {
id: string;
test: ( emailContent: string ) => boolean;
message: string;
actions: EmailContentValidationAction[];
};