Migrate email editor content validation rules to the MP plugin
MAILPOET-6432
This commit is contained in:
committed by
Rostislav Wolný
parent
e2286167d8
commit
5190ac1ff2
@@ -6,11 +6,18 @@ import { addFilter, addAction } from '@wordpress/hooks';
|
|||||||
import { MailPoet } from 'mailpoet';
|
import { MailPoet } from 'mailpoet';
|
||||||
import { withSatismeterSurvey } from './satismeter-survey';
|
import { withSatismeterSurvey } from './satismeter-survey';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import { ValidateEmailContent } from './validate-email-content';
|
||||||
|
|
||||||
addFilter('mailpoet_email_editor_wrap_editor_component', 'mailpoet', (editor) =>
|
addFilter('mailpoet_email_editor_wrap_editor_component', 'mailpoet', (editor) =>
|
||||||
withSatismeterSurvey(editor),
|
withSatismeterSurvey(editor),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
addFilter(
|
||||||
|
'mailpoet_email_editor_content_validation_rules',
|
||||||
|
'mailpoet',
|
||||||
|
(validationRules: []) => [...validationRules, ...ValidateEmailContent()],
|
||||||
|
);
|
||||||
|
|
||||||
const EVENTS_TO_TRACK = [
|
const EVENTS_TO_TRACK = [
|
||||||
'email_editor_events_editor_layout_loaded', // email editor was opened
|
'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
|
'email_editor_events_template_select_modal_template_selected', // a template was selected from the template-select modal
|
||||||
|
@@ -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]);
|
||||||
|
}
|
@@ -1,123 +1,50 @@
|
|||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { useCallback } from '@wordpress/element';
|
||||||
import { useCallback, useMemo } from '@wordpress/element';
|
import { useSelect, subscribe } from '@wordpress/data';
|
||||||
import { dispatch, useSelect, subscribe } from '@wordpress/data';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
|
||||||
import { createBlock } from '@wordpress/blocks';
|
|
||||||
import { store as coreDataStore } from '@wordpress/core-data';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { storeName as emailEditorStore } from '../store';
|
import {
|
||||||
|
EmailContentValidationRule,
|
||||||
|
storeName as emailEditorStore,
|
||||||
|
} from '../store';
|
||||||
import { useShallowEqual } from './use-shallow-equal';
|
import { useShallowEqual } from './use-shallow-equal';
|
||||||
import { useValidationNotices } from './use-validation-notices';
|
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 = {
|
export type ContentValidationData = {
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
validateContent: () => boolean;
|
validateContent: () => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useContentValidation = (): ContentValidationData => {
|
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 } =
|
const { addValidationNotice, hasValidationNotice, removeValidationNotice } =
|
||||||
useValidationNotices();
|
useValidationNotices();
|
||||||
const { editedContent, editedTemplateContent, postTemplateId } = useSelect(
|
|
||||||
|
const { editedContent, editedTemplateContent } = useSelect(
|
||||||
( mapSelect ) => ( {
|
( mapSelect ) => ( {
|
||||||
editedContent:
|
editedContent:
|
||||||
mapSelect( emailEditorStore ).getEditedEmailContent(),
|
mapSelect( emailEditorStore ).getEditedEmailContent(),
|
||||||
editedTemplateContent:
|
editedTemplateContent:
|
||||||
mapSelect( emailEditorStore ).getCurrentTemplateContent(),
|
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 content = useShallowEqual( editedContent );
|
||||||
const templateContent = useShallowEqual( editedTemplateContent );
|
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 => {
|
const validateContent = useCallback( (): boolean => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
rules.forEach( ( { id, test, message, actions } ) => {
|
rules.forEach( ( { id, test, message, actions } ) => {
|
||||||
|
@@ -269,3 +269,15 @@ export type EmailEditorPostType = Omit< Post, 'type' > & {
|
|||||||
type: string;
|
type: string;
|
||||||
mailpoet_data?: MailPoetEmailPostContentExtended;
|
mailpoet_data?: MailPoetEmailPostContentExtended;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmailContentValidationAction = {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmailContentValidationRule = {
|
||||||
|
id: string;
|
||||||
|
test: ( emailContent: string ) => boolean;
|
||||||
|
message: string;
|
||||||
|
actions: EmailContentValidationAction[];
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user