diff --git a/mailpoet/assets/js/src/mailpoet-email-editor-integration/index.ts b/mailpoet/assets/js/src/mailpoet-email-editor-integration/index.ts
index efc66e690a..146f45b8d6 100644
--- a/mailpoet/assets/js/src/mailpoet-email-editor-integration/index.ts
+++ b/mailpoet/assets/js/src/mailpoet-email-editor-integration/index.ts
@@ -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
diff --git a/mailpoet/assets/js/src/mailpoet-email-editor-integration/validate-email-content.ts b/mailpoet/assets/js/src/mailpoet-email-editor-integration/validate-email-content.ts
new file mode 100644
index 0000000000..0bd644d19e
--- /dev/null
+++ b/mailpoet/assets/js/src/mailpoet-email-editor-integration/validate-email-content.ts
@@ -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 = `${__(
+ 'Unsubscribe',
+ 'mailpoet',
+)} | ${__(
+ 'Manage subscription',
+ 'mailpoet',
+)}`;
+
+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}
+
+ ${contentLink}
+
+ `,
+ },
+ );
+ }
+ },
+ },
+ ],
+ },
+ ];
+ }, [contentBlockId, postTemplateId, hasFooter, editedTemplateContent]);
+}
diff --git a/packages/js/email-editor/src/hooks/use-content-validation.ts b/packages/js/email-editor/src/hooks/use-content-validation.ts
index d604776185..5d06341c31 100644
--- a/packages/js/email-editor/src/hooks/use-content-validation.ts
+++ b/packages/js/email-editor/src/hooks/use-content-validation.ts
@@ -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 = `${ __(
- 'Unsubscribe',
- 'mailpoet'
- ) } | ${ __(
- 'Manage subscription',
- 'mailpoet'
- ) }`;
-
- 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 }
-
- ${ contentLink }
-
- `,
- }
- );
- }
- },
- },
- ],
- },
- ];
- }, [
- contentBlockId,
- hasFooter,
- contentLink,
- postTemplateId,
- editedTemplateContent,
- ] );
-
const validateContent = useCallback( (): boolean => {
let isValid = true;
rules.forEach( ( { id, test, message, actions } ) => {
diff --git a/packages/js/email-editor/src/store/types.ts b/packages/js/email-editor/src/store/types.ts
index a2d48cb421..269eb2bdc0 100644
--- a/packages/js/email-editor/src/store/types.ts
+++ b/packages/js/email-editor/src/store/types.ts
@@ -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[];
+};