Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
0077818bf5 | |||
bbdbf6a52d | |||
56ed9f4ece | |||
e6188f5cef | |||
a37e5bbe74 | |||
9df99b1a00 | |||
5ff543e62d | |||
8f14c5ac53 | |||
4e0aa3c56d | |||
545c6bfa5e | |||
cd3652eaa6 | |||
655641737b | |||
152794720a | |||
cb294fb303 | |||
98744a53c1 | |||
430fcc20a8 | |||
1df78f22d3 | |||
2f823f5606 | |||
086d6dce7e | |||
879cca9fb3 | |||
3434fbe3b5 | |||
d4f9ccca5e | |||
4a74c3e6fd | |||
60d9a3f1ac | |||
b64fbbdb7f | |||
12e8d44a43 | |||
667658ae2f | |||
9d2624163c | |||
8f6688eba7 | |||
b360d9a2cf | |||
11384bbf6a | |||
6235944442 | |||
83573b0d43 | |||
bd87c09e1a | |||
1010b64c05 | |||
dd1fcd5100 | |||
75706c9e8b | |||
61e1dd6a83 | |||
8d5af952f6 | |||
2751a4bf3a | |||
ed297dd68d | |||
17c7d42ede | |||
1741b39375 | |||
800432ab54 | |||
f169c8f8ac | |||
f92a12db30 | |||
3ea730a0b6 | |||
e3c19fa306 | |||
f2c4890def | |||
5bc2f62d98 | |||
c954603bfc | |||
bf552801ec | |||
6bb1af0a18 | |||
48dca5e298 | |||
c08813be1d | |||
1cc6d93717 | |||
18a071fd7b | |||
32d310d999 | |||
c76d18e647 | |||
865706c112 | |||
3238049828 | |||
a41de82030 | |||
ccec1faeb1 | |||
b19887add0 | |||
ba2cb75877 | |||
b89905aa80 | |||
a25e879cac | |||
0106c5123d | |||
2cb20a8f63 | |||
fe1a994442 | |||
70889ab06d | |||
aab6865f50 | |||
efd32043e3 | |||
3eaa13a421 | |||
80d2ab44a3 |
@ -410,7 +410,9 @@ jobs:
|
||||
name: Group acceptance tests
|
||||
command: |
|
||||
# Convert test result filename values to be relative paths because the circleci CLI's split command requires exact matches
|
||||
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
|
||||
if [ -e $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json ]; then
|
||||
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
|
||||
fi
|
||||
# `circleci tests split` returns different values based on the container it's run on
|
||||
# in case group is defined find only tests containing the group
|
||||
if [[ -n '<< parameters.group >>' ]]; then
|
||||
|
@ -0,0 +1,44 @@
|
||||
.mailpoet-automatoin-deactivate-modal {
|
||||
color: #1d2327;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
max-width: 480px;
|
||||
|
||||
.mailpoet-automation-options {
|
||||
li {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-option {
|
||||
border: 2px solid #dcdcde;
|
||||
border-radius: 4px;
|
||||
color: #646970;
|
||||
display: grid;
|
||||
font-size: 12px;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: 20px auto;
|
||||
line-height: 16px;
|
||||
padding: 8px;
|
||||
|
||||
&.active {
|
||||
border-color: #2271b1;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #1d2327;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
.components-button {
|
||||
float: right;
|
||||
|
||||
&.is-tertiary {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,10 @@
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
padding: 16px 48px 16px 16px;
|
||||
|
||||
label & {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-action {
|
||||
@ -57,3 +61,78 @@
|
||||
color: #757575;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel {
|
||||
animation: mailpoet-automation-activate-panel-animation .1s forwards;
|
||||
background: #fff;
|
||||
border-left: 1px solid #ddd;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: auto;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translateX(100%);
|
||||
width: 281px;
|
||||
z-index: 999999;
|
||||
|
||||
button {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header {
|
||||
align-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 61px;
|
||||
|
||||
.has-icon {
|
||||
margin-left: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header,
|
||||
.mailpoet-automation-activate-panel__section {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header,
|
||||
.mailpoet-automation-activate-panel__body {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
|
||||
.components-spinner {
|
||||
display: block;
|
||||
margin: 100px auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__section {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header-activate-button,
|
||||
.mailpoet-automation-activate-panel__header-cancel-button {
|
||||
flex-grow: 1;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header-activate-button {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-activate-panel__header-cancel-button {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
@keyframes mailpoet-automation-activate-panel-animation {
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
@ -17,3 +17,21 @@
|
||||
padding: 3px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-editor-stats {
|
||||
margin: 0 auto 32px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
|
||||
.mailpoet-automation-stats-item {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-label {
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,43 @@
|
||||
.mailpoet-add-new-button {
|
||||
padding-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-listing {
|
||||
/* Prevent border radius beneath tabs */
|
||||
border-radius: 0 0 1px 1px;
|
||||
}
|
||||
.mailpoet-automation-listing-heading {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-listing {
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mailpoet-filter-tab-panel {
|
||||
background-color: #fff;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
|
||||
outline: none;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 2px;
|
||||
|
||||
.components-tab-panel__tabs {
|
||||
box-shadow: inset 0 -1px 0 0 #dcdcde;
|
||||
}
|
||||
|
||||
.components-tab-panel__tabs-item:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.components-tab-panel__tabs-item.is-active {
|
||||
box-shadow: inset 0 -4px 0 0 var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
.components-tab-panel__tabs-item:focus-visible {
|
||||
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
.components-tab-panel__tabs-item.is-active:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),
|
||||
inset 0 -4px 0 0 var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
.count {
|
||||
background-color: #f0f0f1;
|
||||
@ -27,6 +52,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-listing-heading {
|
||||
margin-bottom: 16px;
|
||||
.mailpoet-automation-listing-more-button button.components-button {
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
|
||||
svg {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
.mailpoet-automation-listing-cell-actions {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-auto-flow: column;
|
||||
white-space: nowrap;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
.mailpoet-automation-listing-cell-status {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
white-space: nowrap;
|
||||
|
||||
> div.components-base-control > div.components-base-control__field {
|
||||
margin-bottom: 0;
|
||||
|
@ -0,0 +1,32 @@
|
||||
.mailpoet-automation-stats {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-item {
|
||||
color: $color-wordpress-heading;
|
||||
display: grid;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-label {
|
||||
color: #646970;
|
||||
display: block;
|
||||
|
||||
&.display-after {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-item-separator {
|
||||
color: #a7aaad;
|
||||
font-size: 20px;
|
||||
margin: 0 16px;
|
||||
}
|
@ -232,3 +232,7 @@ progress::-moz-progress-bar {
|
||||
.mailpoet-form-field-tags label.components-form-token-field__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mailpoet-form-field-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
.mailpoet-automation-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
margin: auto;
|
||||
|
||||
@at-root #mailpoet_automation_editor #{&} {
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-item {
|
||||
color: $color-wordpress-heading;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
@at-root #mailpoet_automation_editor #{&} {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
@at-root #mailpoet_automation #{&} {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@at-root #mailpoet_automation_editor #{&} {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
align-items: center;
|
||||
color: #a7aaad;
|
||||
content: '›';
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:last-of-type:after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.mailpoet-automation-stats-label {
|
||||
color: #646970;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,13 @@
|
||||
@import '../../../node_modules/@wordpress/edit-site/build-style/style';
|
||||
@import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles
|
||||
@import 'settings/colors';
|
||||
|
||||
// automation components
|
||||
|
||||
@import './components-automation/statistics';
|
||||
|
||||
// automation editor
|
||||
|
||||
@import './components-automation-editor/add-step-button';
|
||||
@import './components-automation-editor/add-trigger';
|
||||
@import './components-automation-editor/block-icon';
|
||||
@ -16,8 +23,8 @@
|
||||
@import './components-automation-editor/step-card';
|
||||
@import './components-automation-editor/workflow';
|
||||
@import './components-automation-editor/notices';
|
||||
@import './components-automation-editor/deactivate-modal';
|
||||
|
||||
// integrations
|
||||
|
||||
@import './components-automation-integrations/mailpoet';
|
||||
@import './components/automation_statistics';
|
||||
|
@ -1,7 +1,14 @@
|
||||
@import '../../../node_modules/@woocommerce/components/build-style/style';
|
||||
@import 'settings/colors';
|
||||
|
||||
// automation components
|
||||
|
||||
@import './components-automation/statistics';
|
||||
|
||||
// automation listing
|
||||
|
||||
@import './components-automation-listing/listing';
|
||||
@import './components-automation-listing/header';
|
||||
@import './components-automation-listing/search';
|
||||
@import './components-automation-listing/cells/actions';
|
||||
@import './components-automation-listing/cells/status';
|
||||
@import './components/automation_statistics';
|
||||
|
@ -2,35 +2,24 @@ import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { TopBarWithBeamer } from 'common/top_bar/top_bar';
|
||||
import { plusIcon } from 'common/button/icon/plus';
|
||||
import { Button, Flex } from '@wordpress/components';
|
||||
import { Workflow } from './listing/workflow';
|
||||
import { Button, Flex, Popover, SlotFillProvider } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { initializeApi, useMutation } from './api';
|
||||
import { createStore, storeName } from './listing/store';
|
||||
import { AutomationListing } from './listing';
|
||||
import { registerApiErrorHandler } from './listing/api-error-handler';
|
||||
import { Notices } from './listing/components/notices';
|
||||
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
|
||||
import { Onboarding } from './onboarding';
|
||||
import {
|
||||
CreateEmptyWorkflowButton,
|
||||
CreateWorkflowFromTemplateButton,
|
||||
} from './testing';
|
||||
import { useMutation, useQuery } from './api';
|
||||
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
|
||||
import { MailPoet } from '../mailpoet';
|
||||
|
||||
function Content(): JSX.Element {
|
||||
const { data, loading, error } = useQuery<{ data: Workflow[] }>('workflows');
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading workflows...</div>;
|
||||
}
|
||||
|
||||
const workflows = data?.data ?? [];
|
||||
return workflows.length > 0 ? (
|
||||
<AutomationListing workflows={workflows} loading={loading} />
|
||||
) : (
|
||||
<Onboarding />
|
||||
);
|
||||
const count = useSelect((select) => select(storeName).getWorkflowCount());
|
||||
return count > 0 ? <AutomationListing /> : <Onboarding />;
|
||||
}
|
||||
|
||||
function Workflows(): JSX.Element {
|
||||
@ -48,6 +37,7 @@ function Workflows(): JSX.Element {
|
||||
New automation
|
||||
</Button>
|
||||
</Flex>
|
||||
<Notices />
|
||||
<Content />
|
||||
</>
|
||||
);
|
||||
@ -104,33 +94,40 @@ function DeleteSchemaButton(): JSX.Element {
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Workflows />
|
||||
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
|
||||
<CreateEmptyWorkflowButton />
|
||||
<CreateWorkflowFromTemplateButton slug="simple-welcome-email">
|
||||
Create testing workflow from template (welcome email)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
|
||||
Create testing workflow from template (welcome sequence, only
|
||||
premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
|
||||
Create testing workflow from template (advanced welcome sequence,
|
||||
only premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<RecreateSchemaButton />
|
||||
<DeleteSchemaButton />
|
||||
<SlotFillProvider>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Workflows />
|
||||
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
|
||||
<CreateEmptyWorkflowButton />
|
||||
<CreateWorkflowFromTemplateButton slug="simple-welcome-email">
|
||||
Create testing workflow from template (welcome email)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
|
||||
Create testing workflow from template (welcome sequence, only
|
||||
premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
|
||||
Create testing workflow from template (advanced welcome sequence,
|
||||
only premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<RecreateSchemaButton />
|
||||
<DeleteSchemaButton />
|
||||
</div>
|
||||
<Popover.Slot />
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</BrowserRouter>
|
||||
</SlotFillProvider>
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
createStore();
|
||||
|
||||
const root = document.getElementById('mailpoet_automation');
|
||||
if (root) {
|
||||
registerApiErrorHandler();
|
||||
initializeApi();
|
||||
ReactDOM.render(<App />, root);
|
||||
}
|
||||
});
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { Fragment } from '@wordpress/element';
|
||||
|
||||
type Item = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: Item[];
|
||||
labelPosition?: 'before' | 'after';
|
||||
};
|
||||
|
||||
export function Statistics({
|
||||
items,
|
||||
labelPosition = 'before',
|
||||
}: Props): JSX.Element {
|
||||
const intl = new Intl.NumberFormat();
|
||||
return (
|
||||
<div className="mailpoet-automation-stats">
|
||||
{items.map((item, i) => (
|
||||
<Fragment key={item.key}>
|
||||
<div key={item.key} className="mailpoet-automation-stats-item">
|
||||
<span
|
||||
className={`mailpoet-automation-stats-label display-${labelPosition}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="mailpoet-automation-stats-value">
|
||||
{intl.format(item.value)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{i < items.length - 1 && (
|
||||
<div className="mailpoet-automation-stats-item-separator">›</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,7 +4,9 @@ declare global {
|
||||
root: string;
|
||||
nonce: string;
|
||||
};
|
||||
mailpoet_workflow_count: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = window.mailpoet_automation_api;
|
||||
export const workflowCount = window.mailpoet_workflow_count;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, NavigableMenu, TextControl } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
|
||||
import { PinnedItems } from '@wordpress/interface';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { DocumentActions } from './document_actions';
|
||||
@ -8,12 +9,13 @@ import { InserterToggle } from './inserter_toggle';
|
||||
import { MoreMenu } from './more_menu';
|
||||
import { storeName } from '../../store';
|
||||
import { WorkflowStatus } from '../../../listing/workflow';
|
||||
import { DeactivateModal } from '../modals/deactivate-modal';
|
||||
|
||||
// See:
|
||||
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js
|
||||
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
|
||||
|
||||
function ActivateButton(): JSX.Element {
|
||||
function ActivateButton({ onClick }): JSX.Element {
|
||||
const { errors } = useSelect(
|
||||
(select) => ({
|
||||
errors: select(storeName).getErrors(),
|
||||
@ -21,30 +23,28 @@ function ActivateButton(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const { activate } = useDispatch(storeName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="editor-post-publish-button"
|
||||
onClick={activate}
|
||||
onClick={onClick}
|
||||
disabled={!!errors}
|
||||
>
|
||||
Activate
|
||||
{__('Activate', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateButton(): JSX.Element {
|
||||
const { activate } = useDispatch(storeName);
|
||||
const { save } = useDispatch(storeName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="editor-post-publish-button"
|
||||
onClick={activate}
|
||||
onClick={save}
|
||||
>
|
||||
Update
|
||||
{__('Update', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -54,16 +54,60 @@ function SaveDraftButton(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Button variant="tertiary" onClick={save}>
|
||||
{__('Save draft')}
|
||||
{__('Save draft', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function DeactivateButton(): JSX.Element {
|
||||
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const { hasUsersInProgress } = useSelect(
|
||||
(select) => ({
|
||||
hasUsersInProgress:
|
||||
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const deactivateOrShowModal = () => {
|
||||
if (hasUsersInProgress) {
|
||||
setShowDeactivateModal(true);
|
||||
return;
|
||||
}
|
||||
setIsBusy(true);
|
||||
void dispatch(storeName).deactivate();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDeactivateModal && (
|
||||
<DeactivateModal
|
||||
onClose={() => {
|
||||
setShowDeactivateModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
isBusy={isBusy}
|
||||
variant="tertiary"
|
||||
onClick={deactivateOrShowModal}
|
||||
>
|
||||
{__('Deactivate', 'mailpoet')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
showInserterToggle: boolean;
|
||||
toggleActivatePanel: () => void;
|
||||
};
|
||||
|
||||
export function Header({ showInserterToggle }: Props): JSX.Element {
|
||||
export function Header({
|
||||
showInserterToggle,
|
||||
toggleActivatePanel,
|
||||
}: Props): JSX.Element {
|
||||
const { setWorkflowName } = useDispatch(storeName);
|
||||
const { workflowName, workflowStatus } = useSelect(
|
||||
(select) => ({
|
||||
@ -90,13 +134,14 @@ export function Header({ showInserterToggle }: Props): JSX.Element {
|
||||
{() => (
|
||||
<div className="mailpoet-automation-editor-dropdown-name-edit">
|
||||
<div className="mailpoet-automation-editor-dropdown-name-edit-title">
|
||||
{__('Automation name')}
|
||||
{__('Automation name', 'mailpoet')}
|
||||
</div>
|
||||
<TextControl
|
||||
value={workflowName}
|
||||
onChange={(newName) => setWorkflowName(newName)}
|
||||
help={__(
|
||||
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
|
||||
'mailpoet',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -107,9 +152,18 @@ export function Header({ showInserterToggle }: Props): JSX.Element {
|
||||
<div className="edit-site-header_end">
|
||||
<div className="edit-site-header__actions">
|
||||
<Errors />
|
||||
<SaveDraftButton />
|
||||
{workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />}
|
||||
{workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />}
|
||||
{workflowStatus !== WorkflowStatus.ACTIVE && (
|
||||
<>
|
||||
<SaveDraftButton />
|
||||
<ActivateButton onClick={toggleActivatePanel} />
|
||||
</>
|
||||
)}
|
||||
{workflowStatus === WorkflowStatus.ACTIVE && (
|
||||
<>
|
||||
<DeactivateButton />
|
||||
<UpdateButton />
|
||||
</>
|
||||
)}
|
||||
<PinnedItems.Slot scope={storeName} />
|
||||
<MoreMenu />
|
||||
</div>
|
||||
|
@ -0,0 +1,115 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { storeName } from '../../store';
|
||||
import { WorkflowStatus } from '../../../listing/workflow';
|
||||
|
||||
export function DeactivateModal({ onClose }): JSX.Element {
|
||||
const { workflowName } = useSelect(
|
||||
(select) => ({
|
||||
workflowName: select(storeName).getWorkflowData().name,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const [selected, setSelected] = useState<
|
||||
WorkflowStatus.INACTIVE | WorkflowStatus.DEACTIVATING
|
||||
>(WorkflowStatus.DEACTIVATING);
|
||||
const [isBusy, setIsBusy] = useState<boolean>(false);
|
||||
// translators: %s is the name of the automation.
|
||||
const title = sprintf(
|
||||
__('Deactivate the "%s" automation?', 'mailpoet'),
|
||||
workflowName,
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="mailpoet-automatoin-deactivate-modal"
|
||||
title={title}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
{__(
|
||||
"Some subscribers entered but have not finished the flow. Let's decide what to do in this case.",
|
||||
'mailpoet',
|
||||
)}
|
||||
<ul className="mailpoet-automation-options">
|
||||
<li>
|
||||
<label
|
||||
className={
|
||||
selected === WorkflowStatus.DEACTIVATING
|
||||
? 'mailpoet-automation-option active'
|
||||
: 'mailpoet-automation-option'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<input
|
||||
type="radio"
|
||||
disabled={isBusy}
|
||||
name="deactivation-method"
|
||||
checked={selected === WorkflowStatus.DEACTIVATING}
|
||||
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
{__('Let entered subscribers finish the flow', 'mailpoet')}
|
||||
</strong>
|
||||
{__(
|
||||
"New subscribers won't enter, but recently entered could proceed.",
|
||||
'mailpoet',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className={
|
||||
selected === WorkflowStatus.INACTIVE
|
||||
? 'mailpoet-automation-option active'
|
||||
: 'mailpoet-automation-option'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<input
|
||||
type="radio"
|
||||
disabled={isBusy}
|
||||
name="deactivation-method"
|
||||
checked={selected === WorkflowStatus.INACTIVE}
|
||||
onChange={() => setSelected(WorkflowStatus.INACTIVE)}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
{__('Stop automation for all subscribers', 'mailpoet')}
|
||||
</strong>
|
||||
{__(
|
||||
'Automation will be deactivated for all the subscribers immediately.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
isBusy={isBusy}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setIsBusy(true);
|
||||
if (selected === WorkflowStatus.DEACTIVATING) {
|
||||
// @ToDo Use the correct method provided in MAILPOET-4731
|
||||
dispatch(storeName).deactivate();
|
||||
return;
|
||||
}
|
||||
dispatch(storeName).deactivate();
|
||||
}}
|
||||
>
|
||||
{__('Deactivate automation', 'mailpoet')}
|
||||
</Button>
|
||||
|
||||
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
|
||||
{__('Cancel', 'mailpoet')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { Button, Spinner } from '@wordpress/components';
|
||||
import { closeSmall } from '@wordpress/icons';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { storeName } from '../../store';
|
||||
import { WorkflowStatus } from '../../../listing/workflow';
|
||||
import { MailPoet } from '../../../../mailpoet';
|
||||
|
||||
function PreStep({ onClose }): JSX.Element {
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
const { activate } = useDispatch(storeName);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet-automation-activate-panel__header">
|
||||
<div className="mailpoet-automation-activate-panel__header-activate-button">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isActivating}
|
||||
isBusy={isActivating}
|
||||
autoFocus={!isActivating}
|
||||
onClick={() => {
|
||||
setIsActivating(true);
|
||||
activate();
|
||||
}}
|
||||
>
|
||||
{isActivating && __('Activating…', 'mailpoet')}
|
||||
{!isActivating && __('Activate', 'mailpoet')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mailpoet-automation-activate-panel__header-cancel-button">
|
||||
<Button variant="secondary" onClick={onClose} disabled={isActivating}>
|
||||
{__('Cancel', 'mailpoet')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isActivating && (
|
||||
<div className="mailpoet-automation-activate-panel__body">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isActivating && (
|
||||
<div className="mailpoet-automation-activate-panel__body">
|
||||
<p>
|
||||
<strong>{__('Are you ready to activate?', 'mailpoet')}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{__('Double-check your settings before activating.', 'mailpoet')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PostStep({ onClose }): JSX.Element {
|
||||
const { workflow } = useSelect(
|
||||
(select) => ({
|
||||
workflow: select(storeName).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const goToListings = () => {
|
||||
window.location.href = MailPoet.urls.automationListing;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet-automation-activate-panel__header">
|
||||
<Button
|
||||
icon={closeSmall}
|
||||
onClick={onClose}
|
||||
label={__('Close', 'mailpoet')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mailpoet-automation-activate-panel__body">
|
||||
<div className="mailpoet-automation-activate-panel__section">
|
||||
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)}
|
||||
</div>
|
||||
<p>
|
||||
<strong>{__("What's next?", 'mailpoet')}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
'View all your automations to track statistics and create new ones.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</p>
|
||||
<Button variant="secondary" onClick={goToListings}>
|
||||
{__('View all automations', 'mailpoet')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivatePanel({ onClose }): JSX.Element {
|
||||
const { workflow, errors } = useSelect(
|
||||
(select) => ({
|
||||
errors: select(storeName).getErrors(),
|
||||
workflow: select(storeName).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (errors) {
|
||||
onClose();
|
||||
}
|
||||
}, [errors, onClose]);
|
||||
|
||||
if (errors) {
|
||||
return null;
|
||||
}
|
||||
const isActive = workflow.status === WorkflowStatus.ACTIVE;
|
||||
return (
|
||||
<div className="mailpoet-automation-activate-panel">
|
||||
{isActive && <PostStep onClose={onClose} />}
|
||||
{!isActive && <PreStep onClose={onClose} />}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { storeName } from '../../store';
|
||||
import { Statistics as BaseStatistics } from '../../../components/statistics';
|
||||
|
||||
export function Statistics(): JSX.Element {
|
||||
const { workflow } = useSelect(
|
||||
@ -11,27 +12,26 @@ export function Statistics(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul className="mailpoet-automation-stats">
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Total Entered', 'mailpoet')}
|
||||
</span>
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.entered)}
|
||||
</li>
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Total Processing', 'mailpoet')}
|
||||
</span>
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)}
|
||||
</li>
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Total Exited', 'mailpoet')}
|
||||
</span>
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.exited)}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mailpoet-automation-editor-stats">
|
||||
<BaseStatistics
|
||||
items={[
|
||||
{
|
||||
key: 'entered',
|
||||
label: __('Total Entered', 'mailpoet'),
|
||||
value: workflow.stats.totals.entered,
|
||||
},
|
||||
{
|
||||
key: 'processing',
|
||||
label: __('Total Processing', 'mailpoet'),
|
||||
value: workflow.stats.totals.in_progress,
|
||||
},
|
||||
{
|
||||
key: 'exited',
|
||||
label: __('Total Exited', 'mailpoet'),
|
||||
value: workflow.stats.totals.exited,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
|
||||
const compositeState = useContext(WorkflowCompositeContext);
|
||||
const { batch } = useRegistry();
|
||||
|
||||
const compositeItemId = `step-${step.id}`;
|
||||
const stepTypeData = stepType ?? getUnknownStepType(step);
|
||||
return (
|
||||
<div className="mailpoet-automation-editor-step-wrapper">
|
||||
@ -63,6 +64,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
|
||||
'is-selected-step': isSelected,
|
||||
'is-unknown-step': !stepType,
|
||||
})}
|
||||
id={compositeItemId}
|
||||
key={step.id}
|
||||
focusable
|
||||
onClick={() =>
|
||||
@ -82,11 +84,14 @@ export function Step({ step, isSelected }: Props): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mailpoet-automation-editor-step-title">
|
||||
<label
|
||||
htmlFor={compositeItemId}
|
||||
className="mailpoet-automation-editor-step-title"
|
||||
>
|
||||
{step.type !== 'trigger'
|
||||
? stepTypeData.title
|
||||
: __('Trigger', 'mailpoet')}
|
||||
</div>
|
||||
</label>
|
||||
<div className="mailpoet-automation-editor-step-subtitle">
|
||||
{step.type !== 'trigger'
|
||||
? stepTypeData.subtitle(step)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classnames from 'classnames';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useState } from 'react';
|
||||
import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { wordpress } from '@wordpress/icons';
|
||||
@ -23,6 +24,7 @@ import { initialize as initializeMailPoetIntegration } from '../integrations/mai
|
||||
import { MailPoet } from '../../mailpoet';
|
||||
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
|
||||
import { registerApiErrorHandler } from './api-error-handler';
|
||||
import { ActivatePanel } from './components/panel/activate-panel';
|
||||
|
||||
// See:
|
||||
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
|
||||
@ -48,6 +50,7 @@ function Editor(): JSX.Element {
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const [showActivatePanel, setShowActivatePanel] = useState(false);
|
||||
|
||||
const className = classnames('interface-interface-skeleton', {
|
||||
'is-sidebar-opened': isSidebarOpened,
|
||||
@ -60,6 +63,11 @@ function Editor(): JSX.Element {
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const toggleActivatePanel = () => {
|
||||
setShowActivatePanel(!showActivatePanel);
|
||||
};
|
||||
|
||||
return (
|
||||
<ShortcutProvider>
|
||||
<SlotFillProvider>
|
||||
@ -80,7 +88,12 @@ function Editor(): JSX.Element {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
header={<Header showInserterToggle={showInserterSidebar} />}
|
||||
header={
|
||||
<Header
|
||||
showInserterToggle={showInserterSidebar}
|
||||
toggleActivatePanel={toggleActivatePanel}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<>
|
||||
<EditorNotices />
|
||||
@ -92,6 +105,7 @@ function Editor(): JSX.Element {
|
||||
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
|
||||
}
|
||||
/>
|
||||
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
|
||||
<Popover.Slot />
|
||||
</SlotFillProvider>
|
||||
</ShortcutProvider>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { select } from '@wordpress/data';
|
||||
import { dispatch, select, StoreDescriptor } from '@wordpress/data';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { store as interfaceStore } from '@wordpress/interface';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
@ -7,6 +9,7 @@ import { storeName } from './constants';
|
||||
import { Feature, State } from './types';
|
||||
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices';
|
||||
import { MailPoet } from '../../../mailpoet';
|
||||
import { WorkflowStatus } from '../../listing/workflow';
|
||||
|
||||
export const openSidebar =
|
||||
(key) =>
|
||||
@ -79,12 +82,52 @@ export function* activate() {
|
||||
},
|
||||
});
|
||||
|
||||
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
|
||||
if (data?.data.status === WorkflowStatus.ACTIVE) {
|
||||
void createNotice(
|
||||
'success',
|
||||
__('Well done! Automation is now activated!', 'mailpoet'),
|
||||
{
|
||||
type: 'snackbar',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'ACTIVATE',
|
||||
workflow: data?.data ?? workflow,
|
||||
} as const;
|
||||
}
|
||||
|
||||
// @ToDo: Decide on best naming once MAILPOET-4731 decides about the "deactivating" status name
|
||||
export function* deactivate() {
|
||||
const workflow = select(storeName).getWorkflowData();
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
...workflow,
|
||||
status: 'inactive',
|
||||
},
|
||||
});
|
||||
|
||||
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
|
||||
if (data?.data.status === WorkflowStatus.INACTIVE) {
|
||||
void createNotice(
|
||||
'success',
|
||||
__('Automation is now deactivated!', 'mailpoet'),
|
||||
{
|
||||
type: 'snackbar',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'DEACTIVATE',
|
||||
workflow: data?.data ?? workflow,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* trash(onTrashed: () => void = undefined) {
|
||||
const workflow = select(storeName).getWorkflowData();
|
||||
const data = yield apiFetch({
|
||||
|
@ -39,6 +39,12 @@ export function reducer(state: State, action: Action): State {
|
||||
workflowData: action.workflow,
|
||||
workflowSaved: true,
|
||||
};
|
||||
case 'DEACTIVATE':
|
||||
return {
|
||||
...state,
|
||||
workflowData: action.workflow,
|
||||
workflowSaved: true,
|
||||
};
|
||||
case 'TRASH':
|
||||
return {
|
||||
...state,
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
FlexItem,
|
||||
} from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { PlainBodyTitle } from '../../../../editor/components/panel';
|
||||
import { storeName } from '../../../../editor/store';
|
||||
import { DelayTypeOptions } from './types/delayTypes';
|
||||
@ -18,13 +19,16 @@ export function Edit(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const delayValueInputId = `delay-number-${selectedStep.id}`;
|
||||
return (
|
||||
<PanelBody opened>
|
||||
<PlainBodyTitle title="Wait for" />
|
||||
<label htmlFor={delayValueInputId}>
|
||||
<PlainBodyTitle title={__('Wait for', 'mailpoet')} />
|
||||
</label>
|
||||
<Flex align="top">
|
||||
<FlexItem style={{ flex: '1 1 0' }}>
|
||||
<TextControl
|
||||
label=""
|
||||
id={delayValueInputId}
|
||||
type="number"
|
||||
placeholder="Number"
|
||||
value={(selectedStep.args.delay as string) ?? ''}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import apiFetch, { APIFetchOptions } from '@wordpress/api-fetch';
|
||||
import { dispatch, StoreDescriptor } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { ApiError } from '../api';
|
||||
|
||||
export const registerApiErrorHandler = (): void =>
|
||||
apiFetch.use(
|
||||
async (
|
||||
options: APIFetchOptions,
|
||||
next: (nextOptions: APIFetchOptions) => Promise<unknown>,
|
||||
) => {
|
||||
try {
|
||||
const result = await next(options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorObject = error as ApiError;
|
||||
const status = errorObject.data?.status;
|
||||
|
||||
if (status && status >= 400 && status < 500) {
|
||||
const message = errorObject.message;
|
||||
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
|
||||
message ?? __('An unknown error occurred.', 'mailpoet'),
|
||||
{ explicitDismiss: true },
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
|
||||
__('An unknown error occurred.', 'mailpoet'),
|
||||
{ explicitDismiss: true },
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { Workflow } from '../../workflow';
|
||||
import { MailPoet } from '../../../../mailpoet';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function EditWorkflow({ workflow, label }: Props): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
href={addQueryArgs(MailPoet.urls.automationEditor, { id: workflow.id })}
|
||||
>
|
||||
{label ?? __('Edit', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './edit-workflow';
|
||||
export * from './undo-trash';
|
@ -0,0 +1,26 @@
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { storeName } from '../../store/constants';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
previousStatus: WorkflowStatus;
|
||||
};
|
||||
|
||||
export function UndoTrashButton({
|
||||
workflow,
|
||||
previousStatus,
|
||||
}: Props): JSX.Element {
|
||||
const { restoreWorkflow } = useDispatch(storeName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => restoreWorkflow(workflow, previousStatus)}
|
||||
>
|
||||
{__('Undo', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { Fragment } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { DropdownMenu } from '@wordpress/components';
|
||||
import { moreVertical } from '@wordpress/icons';
|
||||
import {
|
||||
useDeleteButton,
|
||||
useDuplicateButton,
|
||||
useRestoreButton,
|
||||
useTrashButton,
|
||||
} from '../menu';
|
||||
import { Workflow } from '../../workflow';
|
||||
import { EditWorkflow } from '../actions';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
};
|
||||
|
||||
export function Actions({ workflow }: Props): JSX.Element {
|
||||
// Menu items are using custom hooks because the "DropdownMenu" component uses the "controls"
|
||||
// attribute rather than child components, but we need to render modal confirmation dialogs.
|
||||
const duplicate = useDuplicateButton(workflow);
|
||||
const trash = useTrashButton(workflow);
|
||||
const restore = useRestoreButton(workflow);
|
||||
const del = useDeleteButton(workflow);
|
||||
|
||||
const menuItems = [duplicate, trash, restore, del].filter((item) => item);
|
||||
|
||||
return (
|
||||
<div className="mailpoet-automation-listing-cell-actions">
|
||||
<EditWorkflow workflow={workflow} />
|
||||
{menuItems.map(({ control, slot }) => (
|
||||
<Fragment key={control.title}>{slot}</Fragment>
|
||||
))}
|
||||
<DropdownMenu
|
||||
className="mailpoet-automation-listing-more-button"
|
||||
label={__('More', 'mailpoet')}
|
||||
icon={moreVertical}
|
||||
controls={menuItems.map(({ control }) => control)}
|
||||
popoverProps={{ position: 'bottom left' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Workflow } from '../../workflow';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function Edit({ workflow, label }: Props): JSX.Element {
|
||||
return (
|
||||
<a href={`admin.php?page=mailpoet-automation-editor&id=${workflow.id}`}>
|
||||
{label ?? __('Edit', 'mailpoet')}
|
||||
</a>
|
||||
);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export * from './edit';
|
||||
export * from './more';
|
||||
export * from './actions';
|
||||
export * from './name';
|
||||
export * from './status';
|
||||
export * from './subscribers';
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { EllipsisMenu, MenuItem } from '@woocommerce/components/build';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Workflow } from '../../workflow';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
};
|
||||
|
||||
export function More({ workflow }: Props): JSX.Element {
|
||||
return (
|
||||
<EllipsisMenu
|
||||
label={`Actions for ${workflow.name}`}
|
||||
renderContent={() => (
|
||||
<div>
|
||||
<MenuItem onInvoke={() => {}}>
|
||||
<p>{__('Duplicate', 'mailpoet')}</p>
|
||||
</MenuItem>
|
||||
<MenuItem onInvoke={() => {}}>
|
||||
<p>{__('Move to trash', 'mailpoet')}</p>
|
||||
</MenuItem>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Edit } from './edit';
|
||||
import { EditWorkflow } from '../actions';
|
||||
import { Workflow } from '../../workflow';
|
||||
|
||||
type Props = {
|
||||
@ -6,5 +6,5 @@ type Props = {
|
||||
};
|
||||
|
||||
export function Name({ workflow }: Props): JSX.Element {
|
||||
return <Edit workflow={workflow} label={workflow.name} />;
|
||||
return <EditWorkflow workflow={workflow} label={workflow.name} />;
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ToggleControl } from '@wordpress/components';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
type Props = {
|
||||
@ -8,14 +6,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export function Status({ workflow }: Props): JSX.Element {
|
||||
const [isActive, setIsActive] = useState(workflow.status === 'active');
|
||||
|
||||
return (
|
||||
<div className="mailpoet-automation-listing-cell-status">
|
||||
<ToggleControl
|
||||
checked={isActive}
|
||||
onChange={(active) => setIsActive(active)}
|
||||
/>
|
||||
{workflow.status === WorkflowStatus.ACTIVE
|
||||
? __('Active', 'mailpoet')
|
||||
: __('Not active', 'mailpoet')}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Workflow } from '../../workflow';
|
||||
import { Statistics } from '../../../components/statistics';
|
||||
|
||||
type Props = {
|
||||
workflow: Workflow;
|
||||
@ -7,25 +8,25 @@ type Props = {
|
||||
|
||||
export function Subscribers({ workflow }: Props): JSX.Element {
|
||||
return (
|
||||
<ul className="mailpoet-automation-stats">
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.entered)}
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Entered', 'mailpoet')}
|
||||
</span>
|
||||
</li>
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)}
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Processing', 'mailpoet')}
|
||||
</span>
|
||||
</li>
|
||||
<li className="mailpoet-automation-stats-item">
|
||||
{new Intl.NumberFormat().format(workflow.stats.totals.exited)}
|
||||
<span className="mailpoet-automation-stats-label">
|
||||
{__('Exited', 'mailpoet')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Statistics
|
||||
labelPosition="after"
|
||||
items={[
|
||||
{
|
||||
key: 'entered',
|
||||
label: __('Entered', 'mailpoet'),
|
||||
value: workflow.stats.totals.entered,
|
||||
},
|
||||
{
|
||||
key: 'processing',
|
||||
label: __('Processing', 'mailpoet'),
|
||||
value: workflow.stats.totals.in_progress,
|
||||
},
|
||||
{
|
||||
key: 'exited',
|
||||
label: __('Exited', 'mailpoet'),
|
||||
value: workflow.stats.totals.exited,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Item } from './item';
|
||||
import { storeName } from '../../store';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
export const useDeleteButton = (workflow: Workflow): Item | undefined => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { deleteWorkflow } = useDispatch(storeName);
|
||||
|
||||
if (workflow.status !== WorkflowStatus.TRASH) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'delete',
|
||||
control: {
|
||||
title: __('Delete permanently', 'mailpoet'),
|
||||
icon: null,
|
||||
onClick: () => setShowDialog(true),
|
||||
},
|
||||
slot: (
|
||||
<ConfirmDialog
|
||||
isOpen={showDialog}
|
||||
title={__('Permanently delete automation', 'mailpoet')}
|
||||
confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
|
||||
__experimentalHideHeader={false}
|
||||
onConfirm={() => deleteWorkflow(workflow)}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
>
|
||||
{sprintf(
|
||||
// translators: %s is the workflow name
|
||||
__(
|
||||
'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!',
|
||||
'mailpoet',
|
||||
),
|
||||
workflow.name,
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
),
|
||||
};
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Item } from './item';
|
||||
import { storeName } from '../../store';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
|
||||
const { duplicateWorkflow } = useDispatch(storeName);
|
||||
|
||||
if (workflow.status === WorkflowStatus.TRASH) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'duplicate',
|
||||
control: {
|
||||
title: __('Duplicate', 'mailpoet'),
|
||||
icon: null,
|
||||
onClick: () => duplicateWorkflow(workflow),
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from './delete';
|
||||
export * from './duplicate';
|
||||
export * from './restore';
|
||||
export * from './trash';
|
@ -0,0 +1,9 @@
|
||||
import { DropdownMenu } from '@wordpress/components';
|
||||
|
||||
import Control = DropdownMenu.Control;
|
||||
|
||||
export type Item = {
|
||||
key: string;
|
||||
control: Control;
|
||||
slot?: JSX.Element;
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Item } from './item';
|
||||
import { storeName } from '../../store';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
export const useRestoreButton = (workflow: Workflow): Item | undefined => {
|
||||
const { restoreWorkflow } = useDispatch(storeName);
|
||||
|
||||
if (workflow.status !== WorkflowStatus.TRASH) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'restore',
|
||||
control: {
|
||||
title: __('Restore', 'mailpoet'),
|
||||
icon: null,
|
||||
onClick: () => restoreWorkflow(workflow, WorkflowStatus.DRAFT),
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Item } from './item';
|
||||
import { storeName } from '../../store';
|
||||
import { Workflow, WorkflowStatus } from '../../workflow';
|
||||
|
||||
export const useTrashButton = (workflow: Workflow): Item | undefined => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { trashWorkflow } = useDispatch(storeName);
|
||||
|
||||
if (workflow.status === WorkflowStatus.TRASH) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'trash',
|
||||
control: {
|
||||
title: __('Trash', 'mailpoet'),
|
||||
icon: null,
|
||||
onClick: () => setShowDialog(true),
|
||||
},
|
||||
slot: (
|
||||
<ConfirmDialog
|
||||
isOpen={showDialog}
|
||||
title={__('Trash workflow', 'mailpoet')}
|
||||
confirmButtonText={__('Yes, move to trash', 'mailpoet')}
|
||||
__experimentalHideHeader={false}
|
||||
onConfirm={() => trashWorkflow(workflow)}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
>
|
||||
{sprintf(
|
||||
// translators: %s is the workflow name
|
||||
__(
|
||||
'Are you sure you want to move the workflow "%s" to the Trash?',
|
||||
'mailpoet',
|
||||
),
|
||||
workflow.name,
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
),
|
||||
};
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { StoreDescriptor, useSelect, useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { Notice } from '../../../../notices/notice';
|
||||
|
||||
export function Notices(): JSX.Element {
|
||||
const { notices } = useSelect(
|
||||
(select) => ({
|
||||
notices: select(noticesStore as StoreDescriptor).getNotices(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { removeNotice } = useDispatch(noticesStore as StoreDescriptor);
|
||||
|
||||
const dismissibleNotices = notices.filter(
|
||||
({ isDismissible, type }) => isDismissible && type === 'default',
|
||||
);
|
||||
|
||||
const nonDismissibleNotices = notices.filter(
|
||||
({ isDismissible, type }) => !isDismissible && type === 'default',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nonDismissibleNotices
|
||||
.reverse()
|
||||
.map(({ id, status, content, __unstableHTML }) => (
|
||||
<Notice key={id} renderInPlace type={status} timeout={false}>
|
||||
{__unstableHTML ?? <p>{content}</p>}
|
||||
</Notice>
|
||||
))}
|
||||
|
||||
{dismissibleNotices
|
||||
.reverse()
|
||||
.map(({ id, status, content, __unstableHTML }) => (
|
||||
<Notice
|
||||
key={id}
|
||||
type={status}
|
||||
renderInPlace
|
||||
timeout={false}
|
||||
closable
|
||||
onClose={removeNotice}
|
||||
>
|
||||
{__unstableHTML ?? <p>{content}</p>}
|
||||
</Notice>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Workflow } from './workflow';
|
||||
import { Edit, More, Name, Status, Subscribers } from './components/cells';
|
||||
import { Actions, Name, Status, Subscribers } from './components/cells';
|
||||
|
||||
export function getRow(workflow: Workflow): object[] {
|
||||
return [
|
||||
@ -21,12 +21,7 @@ export function getRow(workflow: Workflow): object[] {
|
||||
{
|
||||
id: workflow.id,
|
||||
value: null,
|
||||
display: <Edit workflow={workflow} />,
|
||||
},
|
||||
{
|
||||
id: workflow.id,
|
||||
value: null,
|
||||
display: <More workflow={workflow} />,
|
||||
display: <Actions workflow={workflow} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { Search, TableCard } from '@woocommerce/components/build';
|
||||
import { TabPanel } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { getRow } from './get-row';
|
||||
import { storeName } from './store/constants';
|
||||
import { Workflow, WorkflowStatus } from './workflow';
|
||||
|
||||
type Props = {
|
||||
workflows: Workflow[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
const filterTabs = [
|
||||
const tabConfig = [
|
||||
{
|
||||
name: 'all',
|
||||
title: 'All',
|
||||
@ -43,11 +40,10 @@ const tableHeaders = [
|
||||
{ key: 'name', label: __('Name', 'mailpoet') },
|
||||
{ key: 'subscribers', label: __('Subscribers', 'mailpoet') },
|
||||
{ key: 'status', label: __('Status', 'mailpoet') },
|
||||
{ key: 'edit' },
|
||||
{ key: 'more' },
|
||||
{ key: 'actions' },
|
||||
] as const;
|
||||
|
||||
export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
export function AutomationListing(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const pageSearch = useMemo(
|
||||
@ -55,6 +51,22 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
[location],
|
||||
);
|
||||
|
||||
const workflows = useSelect((select) => select(storeName).getWorkflows());
|
||||
const { loadWorkflows } = useDispatch(storeName);
|
||||
|
||||
const status = pageSearch.get('status');
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkflows();
|
||||
}, [loadWorkflows]);
|
||||
|
||||
// focus tab button on status change (needed due to the force re-mount below)
|
||||
useLayoutEffect(() => {
|
||||
if (status) {
|
||||
document.querySelector<HTMLElement>(`.mailpoet-tab-${status}`)?.focus();
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const updateUrlSearchString = useCallback(
|
||||
(search: Record<string, string>) => {
|
||||
const newSearch = new URLSearchParams({
|
||||
@ -75,10 +87,8 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
);
|
||||
|
||||
const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => {
|
||||
const grouped = {
|
||||
all: [],
|
||||
};
|
||||
workflows.forEach((workflow) => {
|
||||
const grouped = { all: [] };
|
||||
(workflows ?? []).forEach((workflow) => {
|
||||
if (!grouped[workflow.status]) {
|
||||
grouped[workflow.status] = [];
|
||||
}
|
||||
@ -92,30 +102,27 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
filterTabs.map((filterTab) => {
|
||||
const count = (groupedWorkflows[filterTab.name] || []).length;
|
||||
tabConfig.map((tab) => {
|
||||
const count = (groupedWorkflows[tab.name] ?? []).length;
|
||||
return {
|
||||
name: filterTab.name,
|
||||
title:
|
||||
count > 0 ? (
|
||||
<>
|
||||
<span>{filterTab.title}</span>
|
||||
<span className="count">{count}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{filterTab.title}</span>
|
||||
),
|
||||
className: filterTab.className,
|
||||
name: tab.name,
|
||||
title: (
|
||||
<>
|
||||
<span>{tab.title}</span>
|
||||
{count > 0 && <span className="count">{count}</span>}
|
||||
</>
|
||||
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- typed as string but supports JSX
|
||||
className: tab.className,
|
||||
};
|
||||
}),
|
||||
[groupedWorkflows],
|
||||
);
|
||||
|
||||
const tabRenderer = useCallback(
|
||||
const renderTabs = useCallback(
|
||||
(tab) => {
|
||||
const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? [];
|
||||
const rowsPerPage = parseInt(pageSearch.get('per_page') || '25', 10);
|
||||
const currentPage = parseInt(pageSearch.get('paged') || '1', 10);
|
||||
const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
|
||||
const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10);
|
||||
const start = (currentPage - 1) * rowsPerPage;
|
||||
const rows = filteredWorkflows
|
||||
.map((workflow) => getRow(workflow))
|
||||
@ -125,7 +132,7 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
<TableCard
|
||||
className="mailpoet-automation-listing"
|
||||
title=""
|
||||
isLoading={loading}
|
||||
isLoading={!workflows}
|
||||
headers={tableHeaders}
|
||||
rows={rows}
|
||||
rowKey={(_, i) => filteredWorkflows[i].id}
|
||||
@ -143,40 +150,30 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
allowFreeTextSearch
|
||||
inlineTags
|
||||
key="search"
|
||||
// onChange={ onSearchChange }
|
||||
// placeholder={
|
||||
// labels.placeholder ||
|
||||
// __( 'Search by item name', 'woocommerce' )
|
||||
// }
|
||||
// selected={ searchedLabels }
|
||||
type="custom"
|
||||
disabled={loading || workflows.length === 0}
|
||||
disabled={!workflows}
|
||||
autocompleter={{}}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[workflows, groupedWorkflows, pageSearch, loading, updateUrlSearchString],
|
||||
[workflows, groupedWorkflows, pageSearch, updateUrlSearchString],
|
||||
);
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="mailpoet-filter-tab-panel"
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - the Tab type actually expects a string for titles but won't render HTML,
|
||||
// making it very difficult to style the count badges. It seems to be compatible with JSX
|
||||
// elements, however.
|
||||
tabs={tabs}
|
||||
onSelect={(tabName) => {
|
||||
if (pageSearch.get('status') !== tabName) {
|
||||
if (status !== tabName) {
|
||||
updateUrlSearchString({ status: tabName });
|
||||
}
|
||||
}}
|
||||
initialTabName={pageSearch.get('status') || 'all'}
|
||||
key={pageSearch.get('status')} // Force re-render on browser forward/back
|
||||
initialTabName={status ?? 'all'}
|
||||
key={status} // force re-mount on history change to switch tab (via "initialTabName")
|
||||
>
|
||||
{tabRenderer}
|
||||
{renderTabs}
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
111
mailpoet/assets/js/src/automation/listing/store/actions.tsx
Normal file
111
mailpoet/assets/js/src/automation/listing/store/actions.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { dispatch, StoreDescriptor } from '@wordpress/data';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { Workflow, WorkflowStatus } from '../workflow';
|
||||
import { EditWorkflow, UndoTrashButton } from '../components/actions';
|
||||
|
||||
const createSuccessNotice = (content: string, options?: unknown) =>
|
||||
dispatch(noticesStore as StoreDescriptor).createSuccessNotice(
|
||||
content,
|
||||
options,
|
||||
);
|
||||
|
||||
const removeNotice = (id: string) =>
|
||||
dispatch(noticesStore as StoreDescriptor).removeNotice(id);
|
||||
|
||||
export function* loadWorkflows() {
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows`,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'SET_WORKFLOWS',
|
||||
workflows: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* duplicateWorkflow(workflow: Workflow) {
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}/duplicate`,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
void createSuccessNotice(
|
||||
// translators: %s is the workflow name
|
||||
sprintf(__('Automation "%s" was duplicated.', 'mailpoet'), workflow.name),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'ADD_WORKFLOW',
|
||||
workflow: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* trashWorkflow(workflow: Workflow) {
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
status: WorkflowStatus.TRASH,
|
||||
},
|
||||
});
|
||||
|
||||
const message = __('1 automation moved to the Trash.', 'mailpoet');
|
||||
void createSuccessNotice(message, {
|
||||
id: `workflow-trashed-${workflow.id}`,
|
||||
__unstableHTML: (
|
||||
<p>
|
||||
{message}{' '}
|
||||
<UndoTrashButton workflow={workflow} previousStatus={workflow.status} />
|
||||
</p>
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'UPDATE_WORKFLOW',
|
||||
workflow: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* restoreWorkflow(workflow: Workflow, status: WorkflowStatus) {
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
void removeNotice(`workflow-trashed-${workflow.id}`);
|
||||
|
||||
const message = __('1 automation restored from the Trash.', 'mailpoet');
|
||||
void createSuccessNotice(message, {
|
||||
__unstableHTML: (
|
||||
<p>
|
||||
{message} <EditWorkflow workflow={workflow} label="Edit Workflow" />
|
||||
</p>
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'UPDATE_WORKFLOW',
|
||||
workflow: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* deleteWorkflow(workflow: Workflow) {
|
||||
yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
void createSuccessNotice(
|
||||
__('1 automation and all associated data permanently deleted.', 'mailpoet'),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'DELETE_WORKFLOW',
|
||||
workflow,
|
||||
} as const;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export const storeName = 'mailpoet/automation-listing';
|
3
mailpoet/assets/js/src/automation/listing/store/index.ts
Normal file
3
mailpoet/assets/js/src/automation/listing/store/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './constants';
|
||||
export * from './store';
|
||||
export * from './types';
|
@ -0,0 +1,5 @@
|
||||
import { State } from './types';
|
||||
|
||||
export const getInitialState = (): State => ({
|
||||
workflows: undefined,
|
||||
});
|
34
mailpoet/assets/js/src/automation/listing/store/reducer.ts
Normal file
34
mailpoet/assets/js/src/automation/listing/store/reducer.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Action } from '@wordpress/data';
|
||||
import { State } from './types';
|
||||
import { Workflow } from '../workflow';
|
||||
|
||||
export function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'SET_WORKFLOWS':
|
||||
return {
|
||||
...state,
|
||||
workflows: action.workflows,
|
||||
};
|
||||
case 'ADD_WORKFLOW':
|
||||
return {
|
||||
...state,
|
||||
workflows: [action.workflow, ...state.workflows],
|
||||
};
|
||||
case 'UPDATE_WORKFLOW':
|
||||
return {
|
||||
...state,
|
||||
workflows: state.workflows.map((workflow: Workflow) =>
|
||||
workflow.id === action.workflow.id ? action.workflow : workflow,
|
||||
),
|
||||
};
|
||||
case 'DELETE_WORKFLOW':
|
||||
return {
|
||||
...state,
|
||||
workflows: state.workflows.filter(
|
||||
(workflow: Workflow) => workflow.id !== action.workflow.id,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
11
mailpoet/assets/js/src/automation/listing/store/selectors.ts
Normal file
11
mailpoet/assets/js/src/automation/listing/store/selectors.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { State } from './types';
|
||||
import { Workflow } from '../workflow';
|
||||
import { workflowCount } from '../../config';
|
||||
|
||||
export function getWorkflows(state: State): Workflow[] {
|
||||
return state.workflows;
|
||||
}
|
||||
|
||||
export function getWorkflowCount(state: State): number {
|
||||
return state.workflows ? state.workflows.length : workflowCount;
|
||||
}
|
41
mailpoet/assets/js/src/automation/listing/store/store.ts
Normal file
41
mailpoet/assets/js/src/automation/listing/store/store.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
createReduxStore,
|
||||
register,
|
||||
StoreConfig,
|
||||
StoreDescriptor,
|
||||
} from '@wordpress/data';
|
||||
import { controls } from '@wordpress/data-controls';
|
||||
import { storeName } from './constants';
|
||||
import { getInitialState } from './initial_state';
|
||||
import { reducer } from './reducer';
|
||||
import * as actions from './actions';
|
||||
import * as selectors from './selectors';
|
||||
import { State } from './types';
|
||||
import { OmitFirstArgs } from '../../../types';
|
||||
|
||||
type StoreType = Omit<StoreDescriptor, 'name'> & {
|
||||
name: typeof storeName;
|
||||
};
|
||||
|
||||
export const createStore = (): StoreType => {
|
||||
const storeConfig = {
|
||||
actions,
|
||||
controls,
|
||||
selectors,
|
||||
reducer,
|
||||
initialState: getInitialState(),
|
||||
} as StoreConfig<State>;
|
||||
|
||||
const store = createReduxStore<State>(storeName, storeConfig) as StoreType;
|
||||
register(store);
|
||||
return store;
|
||||
};
|
||||
|
||||
export type StoreKey = typeof storeName | StoreType;
|
||||
|
||||
declare module '@wordpress/data' {
|
||||
function select(key: StoreKey): OmitFirstArgs<typeof selectors>;
|
||||
function dispatch(key: StoreKey): typeof actions;
|
||||
}
|
||||
|
||||
export { actions, selectors };
|
5
mailpoet/assets/js/src/automation/listing/store/types.ts
Normal file
5
mailpoet/assets/js/src/automation/listing/store/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Workflow } from '../workflow';
|
||||
|
||||
export type State = {
|
||||
workflows?: Workflow[];
|
||||
};
|
@ -3,6 +3,8 @@ export enum WorkflowStatus {
|
||||
INACTIVE = 'inactive',
|
||||
DRAFT = 'draft',
|
||||
TRASH = 'trash',
|
||||
// @ToDo: Needs to be aligned with MAILPOET-4731
|
||||
DEACTIVATING = 'deactivating',
|
||||
}
|
||||
|
||||
export type Workflow = {
|
||||
|
@ -2,6 +2,7 @@ import { Component } from 'react';
|
||||
import { FormFieldText } from 'form/fields/text.jsx';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FormFieldTextarea } from 'form/fields/textarea.jsx';
|
||||
import { FormFieldSelect } from 'form/fields/select.jsx';
|
||||
@ -151,8 +152,26 @@ class FormField extends Component {
|
||||
field = 'invalid';
|
||||
break;
|
||||
}
|
||||
|
||||
const isDisabled =
|
||||
typeof this.props.field.disabled === 'function'
|
||||
? this.props.field.disabled(this.props.field)
|
||||
: this.props.field.disabled;
|
||||
|
||||
const eventListeners = {
|
||||
...(this.props.field.onWrapperClick
|
||||
? { onClick: this.props.field.onWrapperClick }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mailpoet-form-field" key={`field-${data.index || 0}`}>
|
||||
<div
|
||||
className={classNames('mailpoet-form-field', {
|
||||
'mailpoet-form-field-disabled': isDisabled,
|
||||
})}
|
||||
key={`field-${data.index || 0}`}
|
||||
{...eventListeners}
|
||||
>
|
||||
{field}
|
||||
{description}
|
||||
</div>
|
||||
@ -212,6 +231,8 @@ FormField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.object),
|
||||
description: PropTypes.string,
|
||||
onWrapperClick: PropTypes.func,
|
||||
disabled: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
@ -26,4 +26,5 @@ export type Field = {
|
||||
getLabel?: (segment: Segment) => string;
|
||||
getCount?: (segment: Segment) => string;
|
||||
transformChangedValue?: (arg: unknown) => Segment;
|
||||
onWrapperClick?: () => void;
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ import { GlobalContext } from 'context/index.jsx';
|
||||
|
||||
import { extractEmailDomain } from 'common/functions';
|
||||
import { mapFilterType } from '../analytics';
|
||||
import { PremiumModal } from '../common/premium_modal';
|
||||
|
||||
const automaticEmails = window.mailpoet_woocommerce_automatic_emails || [];
|
||||
|
||||
@ -113,7 +114,7 @@ class NewsletterSendComponent extends Component {
|
||||
item: {},
|
||||
loading: true,
|
||||
thumbnailPromise: null,
|
||||
isSavingDraft: false,
|
||||
showPremiumModal: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -182,6 +183,8 @@ class NewsletterSendComponent extends Component {
|
||||
return addresses.indexOf(fromAddress) !== -1;
|
||||
};
|
||||
|
||||
isGaFieldDisabled = () => !window.mailpoet_premium_active;
|
||||
|
||||
loadItem = (id) => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
@ -215,7 +218,7 @@ class NewsletterSendComponent extends Component {
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!item.ga_campaign) {
|
||||
if (!item.ga_campaign && !this.isGaFieldDisabled()) {
|
||||
item.ga_campaign = generateGaTrackingCampaignName(
|
||||
item.id,
|
||||
item.subject,
|
||||
@ -615,27 +618,9 @@ class NewsletterSendComponent extends Component {
|
||||
return true;
|
||||
};
|
||||
|
||||
handleSaveDraft = () =>
|
||||
this.setState({
|
||||
isSavingDraft: true,
|
||||
});
|
||||
|
||||
disableSegmentsValidation = (field) => {
|
||||
if (
|
||||
this.state.isSavingDraft &&
|
||||
field.name === 'segments' &&
|
||||
field.validation &&
|
||||
field.validation['data-parsley-required']
|
||||
) {
|
||||
return {
|
||||
...field,
|
||||
validation: {
|
||||
'data-parsley-required': false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return field;
|
||||
handleSaveDraft = () => {
|
||||
// Disabling all validations when saving a draft
|
||||
jQuery('#mailpoet_newsletter').parsley().destroy();
|
||||
};
|
||||
|
||||
disableSegmentsSelectorWhenPaused = (isPaused) => (field) => {
|
||||
@ -645,19 +630,37 @@ class NewsletterSendComponent extends Component {
|
||||
return field;
|
||||
};
|
||||
|
||||
getPreparedFields = (isPaused) =>
|
||||
disableGAIfPremiumInactive = (disabled) => (field) => {
|
||||
if (field.name === 'ga_campaign') {
|
||||
let onWrapperClick = () => {};
|
||||
if (disabled()) {
|
||||
onWrapperClick = () => this.setState({ showPremiumModal: true });
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
disabled,
|
||||
onWrapperClick,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
};
|
||||
|
||||
getPreparedFields = (isPaused, gaFieldDisabled) =>
|
||||
this.state.fields
|
||||
.map(this.disableSegmentsSelectorWhenPaused(isPaused))
|
||||
.map(this.disableSegmentsValidation);
|
||||
.map(this.disableGAIfPremiumInactive(gaFieldDisabled));
|
||||
|
||||
closePremiumModal = () => this.setState({ showPremiumModal: false });
|
||||
|
||||
render() {
|
||||
const isPaused =
|
||||
this.state.item.status === 'sending' &&
|
||||
this.state.item.queue &&
|
||||
this.state.item.queue.status === 'paused';
|
||||
|
||||
const {
|
||||
showPremiumModal,
|
||||
item: { status, queue, type, options },
|
||||
} = this.state;
|
||||
const isPaused = status === 'sending' && queue && queue.status === 'paused';
|
||||
const sendButtonOptions = this.getSendButtonOptions();
|
||||
const fields = this.getPreparedFields(isPaused);
|
||||
const fields = this.getPreparedFields(isPaused, this.isGaFieldDisabled);
|
||||
|
||||
const sendingDisabled = !!(
|
||||
window.mailpoet_subscribers_limit_reached ||
|
||||
@ -665,9 +668,9 @@ class NewsletterSendComponent extends Component {
|
||||
this.state.validationError !== undefined
|
||||
);
|
||||
|
||||
let emailType = this.state.item.type;
|
||||
let emailType = type;
|
||||
if (emailType === 'automatic') {
|
||||
emailType = this.state.item.options.group || emailType;
|
||||
emailType = options.group || emailType;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -754,6 +757,12 @@ class NewsletterSendComponent extends Component {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPremiumModal && (
|
||||
<PremiumModal onRequestClose={this.closePremiumModal}>
|
||||
{MailPoet.I18n.t('gaOnlyAvailableForPremium')}
|
||||
</PremiumModal>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import { Label, Inputs } from 'settings/components';
|
||||
|
||||
export function Libs3rdParty() {
|
||||
const [enabled, setEnabled] = useSetting('3rd_party_libs', 'enabled');
|
||||
const [, setAnalyticsEnabled] = useSetting('analytics', 'enabled');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -40,10 +39,7 @@ export function Libs3rdParty() {
|
||||
id="libs-3rd-party-disabled"
|
||||
value=""
|
||||
checked={enabled === ''}
|
||||
onCheck={() => {
|
||||
setEnabled('');
|
||||
setAnalyticsEnabled('');
|
||||
}}
|
||||
onCheck={setEnabled}
|
||||
/>
|
||||
<label htmlFor="libs-3rd-party-disabled">{t('no')}</label>
|
||||
</Inputs>
|
||||
|
@ -5,7 +5,6 @@ import { Label, Inputs } from 'settings/components';
|
||||
|
||||
export function ShareData() {
|
||||
const [enabled, setEnabled] = useSetting('analytics', 'enabled');
|
||||
const [, set3rdPartyLibsEnabled] = useSetting('3rd_party_libs', 'enabled');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -32,10 +31,7 @@ export function ShareData() {
|
||||
id="share-data-enabled"
|
||||
value="1"
|
||||
checked={enabled === '1'}
|
||||
onCheck={() => {
|
||||
setEnabled('1');
|
||||
set3rdPartyLibsEnabled('1');
|
||||
}}
|
||||
onCheck={setEnabled}
|
||||
automationId="analytics-yes"
|
||||
/>
|
||||
<label htmlFor="share-data-enabled">{t('yes')}</label>
|
||||
|
@ -185,7 +185,7 @@ export function normalizeSettings(data: Record<string, unknown>): Settings {
|
||||
}),
|
||||
}),
|
||||
mailpoet_subscribe_old_woocommerce_customers: asObject({
|
||||
enabled: enabledRadio,
|
||||
enabled: disabledCheckbox,
|
||||
}),
|
||||
premium: asObject({
|
||||
premium_key: text,
|
||||
|
@ -1,10 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import { Button } from 'common/button/button';
|
||||
import { Grid } from 'common/grid';
|
||||
import { Heading } from 'common/typography/heading/heading';
|
||||
import { List } from 'common/typography/list/list';
|
||||
import { YesNo } from 'common/form/yesno/yesno';
|
||||
|
||||
function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
|
||||
@ -25,23 +24,6 @@ function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
|
||||
{MailPoet.I18n.t('welcomeWizardUsageTrackingStepTitle')}
|
||||
</Heading>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
<p>{MailPoet.I18n.t('welcomeWizardTrackingText')}</p>
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<Heading level={5}>
|
||||
{MailPoet.I18n.t('welcomeWizardUsageTrackingStepSubTitle')}
|
||||
</Heading>
|
||||
<Grid.TwoColumnsList>
|
||||
<List>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardTrackingList1')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardTrackingList2')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardTrackingList3')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardTrackingList4')}</li>
|
||||
<li>{MailPoet.I18n.t('welcomeWizardTrackingList5')}</li>
|
||||
</List>
|
||||
</Grid.TwoColumnsList>
|
||||
|
||||
<div className="mailpoet-gap" />
|
||||
|
||||
<form onSubmit={submit}>
|
||||
@ -50,38 +32,43 @@ function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
|
||||
<YesNo
|
||||
onCheck={(value) => {
|
||||
const newState = {
|
||||
tracking: value,
|
||||
libs3rdParty: state.libs3rdParty,
|
||||
libs3rdParty: value,
|
||||
};
|
||||
if (value) {
|
||||
newState.libs3rdParty = value;
|
||||
}
|
||||
setState(newState);
|
||||
setState((prevState) => ({ ...prevState, ...newState }));
|
||||
}}
|
||||
checked={state.tracking}
|
||||
name="mailpoet_tracking"
|
||||
checked={state.libs3rdParty}
|
||||
name="mailpoet_libs_3rdParty"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
{MailPoet.I18n.t('welcomeWizardUsageTrackingStepTrackingLabel')}{' '}
|
||||
<a
|
||||
href="https://kb.mailpoet.com/article/130-sharing-your-data-with-us"
|
||||
data-beacon-article="57ce0aaac6979108399a0454"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{MailPoet.I18n.t('welcomeWizardTrackingLink')}
|
||||
</a>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabel',
|
||||
)}{' '}
|
||||
</p>
|
||||
<div className="mailpoet-wizard-note">
|
||||
<span>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNoteNote',
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNoteNote',
|
||||
)}
|
||||
</span>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNote',
|
||||
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNote',
|
||||
),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
(match, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href="https://kb.mailpoet.com/article/338-what-3rd-party-libraries-we-use"
|
||||
data-beacon-article="5f7c7dd94cedfd0017dcece8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -94,42 +81,41 @@ function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
|
||||
<YesNo
|
||||
onCheck={(value) => {
|
||||
const newState = {
|
||||
libs3rdParty: value,
|
||||
tracking: state.tracking,
|
||||
tracking: value,
|
||||
};
|
||||
if (!value) {
|
||||
newState.tracking = value;
|
||||
}
|
||||
setState(newState);
|
||||
setState((prevState) => ({ ...prevState, ...newState }));
|
||||
}}
|
||||
checked={state.libs3rdParty}
|
||||
name="mailpoet_libs_3rdParty"
|
||||
checked={state.tracking}
|
||||
name="mailpoet_tracking"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabel',
|
||||
)}{' '}
|
||||
<a
|
||||
href="https://kb.mailpoet.com/article/338-what-3rd-party-libraries-we-use"
|
||||
data-beacon-article="5f7c7dd94cedfd0017dcece8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLink',
|
||||
)}
|
||||
</a>
|
||||
{MailPoet.I18n.t('welcomeWizardUsageTrackingStepTrackingLabel')}{' '}
|
||||
</p>
|
||||
<div className="mailpoet-wizard-note">
|
||||
<span>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNoteNote',
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNoteNote',
|
||||
)}
|
||||
</span>
|
||||
{MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNote',
|
||||
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t(
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNote',
|
||||
),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
(match, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href="https://kb.mailpoet.com/article/130-sharing-your-data-with-us"
|
||||
data-beacon-article="57ce0aaac6979108399a0454"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,9 @@ import { YesNo } from '../../common/form/yesno/yesno';
|
||||
|
||||
function WizardWooCommerceStep(props) {
|
||||
const [allowed, setAllowed] = useState(null);
|
||||
const [importType, setImportType] = useState(null);
|
||||
const [importType, setImportType] = useState(
|
||||
props.showCustomersImportSetting === false ? 'unsubscribed' : null,
|
||||
);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const submit = (event) => {
|
||||
@ -39,43 +41,44 @@ function WizardWooCommerceStep(props) {
|
||||
<p>{MailPoet.I18n.t('wooCommerceSetupInfo')}</p>
|
||||
<div className="mailpoet-gap" />
|
||||
<form onSubmit={submit}>
|
||||
<div className="mailpoet-wizard-woocommerce-option">
|
||||
<div className="mailpoet-wizard-woocommerce-toggle">
|
||||
<YesNo
|
||||
showError={submitted && importType === null}
|
||||
checked={importTypeChecked}
|
||||
onCheck={(value) =>
|
||||
setImportType(value ? 'subscribed' : 'unsubscribed')
|
||||
}
|
||||
name="mailpoet_woocommerce_import_type"
|
||||
automationId="woocommerce_import_type"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t('wooCommerceSetupImportInfo'),
|
||||
/\[link\](.*?)\[\/link\]/,
|
||||
(match) => (
|
||||
<a
|
||||
key={match}
|
||||
href="https://kb.mailpoet.com/article/284-import-old-customers-to-the-woocommerce-customers-list"
|
||||
data-beacon-article="5d722c7104286364bc8ecf19"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
<div className="mailpoet-wizard-note">
|
||||
<span>GDPR</span>
|
||||
{MailPoet.I18n.t('wooCommerceSetupImportGDPRInfo')}
|
||||
{props.showCustomersImportSetting ? (
|
||||
<div className="mailpoet-wizard-woocommerce-option">
|
||||
<div className="mailpoet-wizard-woocommerce-toggle">
|
||||
<YesNo
|
||||
showError={submitted && importType === null}
|
||||
checked={importTypeChecked}
|
||||
onCheck={(value) =>
|
||||
setImportType(value ? 'subscribed' : 'unsubscribed')
|
||||
}
|
||||
name="mailpoet_woocommerce_import_type"
|
||||
automationId="woocommerce_import_type"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
{ReactStringReplace(
|
||||
MailPoet.I18n.t('wooCommerceSetupImportInfo'),
|
||||
/\[link\](.*?)\[\/link\]/,
|
||||
(match) => (
|
||||
<a
|
||||
key={match}
|
||||
href="https://kb.mailpoet.com/article/284-import-old-customers-to-the-woocommerce-customers-list"
|
||||
data-beacon-article="5d722c7104286364bc8ecf19"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
<div className="mailpoet-wizard-note">
|
||||
<span>GDPR</span>
|
||||
{MailPoet.I18n.t('wooCommerceSetupImportGDPRInfo')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
) : null}
|
||||
<div className="mailpoet-wizard-woocommerce-option">
|
||||
<div className="mailpoet-wizard-woocommerce-toggle">
|
||||
<YesNo
|
||||
@ -129,6 +132,7 @@ function WizardWooCommerceStep(props) {
|
||||
WizardWooCommerceStep.propTypes = {
|
||||
submitForm: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
showCustomersImportSetting: PropTypes.bool.isRequired,
|
||||
isWizardStep: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -57,6 +57,7 @@ function WooCommerceController({ isWizardStep = false }) {
|
||||
loading={loading}
|
||||
submitForm={submit}
|
||||
isWizardStep={isWizardStep}
|
||||
showCustomersImportSetting={window.mailpoet_show_customers_import}
|
||||
/>
|
||||
</WelcomeWizardStepLayout>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ namespace MailPoet\AdminPages\Pages;
|
||||
|
||||
use MailPoet\AdminPages\PageRenderer;
|
||||
use MailPoet\Automation\Engine\Migrations\Migrator;
|
||||
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class Automation {
|
||||
@ -16,14 +17,19 @@ class Automation {
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WorkflowStorage */
|
||||
private $workflowStorage;
|
||||
|
||||
public function __construct(
|
||||
Migrator $migrator,
|
||||
PageRenderer $pageRenderer,
|
||||
WPFunctions $wp
|
||||
WPFunctions $wp,
|
||||
WorkflowStorage $workflowStorage
|
||||
) {
|
||||
$this->migrator = $migrator;
|
||||
$this->pageRenderer = $pageRenderer;
|
||||
$this->wp = $wp;
|
||||
$this->workflowStorage = $workflowStorage;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
@ -36,6 +42,7 @@ class Automation {
|
||||
'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'),
|
||||
'nonce' => $this->wp->wpCreateNonce('wp_rest'),
|
||||
],
|
||||
'workflowCount' => $this->workflowStorage->getWorkflowCount(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace MailPoet\AdminPages\Pages;
|
||||
use MailPoet\AdminPages\PageRenderer;
|
||||
use MailPoet\Config\Menu;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class WelcomeWizard {
|
||||
@ -17,13 +18,18 @@ class WelcomeWizard {
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $wooCommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
PageRenderer $pageRenderer,
|
||||
SettingsController $settings,
|
||||
WooCommerceHelper $wooCommerceHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->pageRenderer = $pageRenderer;
|
||||
$this->settings = $settings;
|
||||
$this->wooCommerceHelper = $wooCommerceHelper;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
@ -34,6 +40,7 @@ class WelcomeWizard {
|
||||
'sender' => $this->settings->get('sender'),
|
||||
'admin_email' => $this->wp->getOption('admin_email'),
|
||||
'current_wp_user' => $this->wp->wpGetCurrentUser()->to_array(),
|
||||
'show_customers_import' => $this->wooCommerceHelper->getCustomersCount() > 0,
|
||||
];
|
||||
$this->pageRenderer->displayPage('welcome_wizard.html', $data);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace MailPoet\AdminPages\Pages;
|
||||
|
||||
use MailPoet\AdminPages\PageRenderer;
|
||||
use MailPoet\Config\Menu;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class WooCommerceSetup {
|
||||
@ -13,11 +14,16 @@ class WooCommerceSetup {
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooCommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
PageRenderer $pageRenderer,
|
||||
Helper $wooCommerceHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->pageRenderer = $pageRenderer;
|
||||
$this->wooCommerceHelper = $wooCommerceHelper;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
@ -25,6 +31,7 @@ class WooCommerceSetup {
|
||||
if ((bool)(defined('DOING_AJAX') && DOING_AJAX)) return;
|
||||
$data = [
|
||||
'finish_wizard_url' => $this->wp->adminUrl('admin.php?page=' . Menu::MAIN_PAGE_SLUG),
|
||||
'show_customers_import' => $this->wooCommerceHelper->getCustomersCount() > 0,
|
||||
];
|
||||
$this->pageRenderer->displayPage('woocommerce_setup.html', $data);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class AbandonedCart {
|
||||
'slug' => self::SLUG,
|
||||
'title' => _x('Abandoned Shopping Cart', 'This is the name of a type of automatic email for ecommerce. Those emails are sent automatically when a customer adds product to his shopping cart but never complete the checkout process.', 'mailpoet'),
|
||||
'description' => __('Send an email to logged-in visitors who have items in their shopping carts but left your website without checking out. Can convert up to 5% of abandoned carts.', 'mailpoet'),
|
||||
'listingScheduleDisplayText' => _x('Email sent when a customer abandons his cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'),
|
||||
'listingScheduleDisplayText' => _x('Send the email when a customer abandons their cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'),
|
||||
'badge' => [
|
||||
'text' => __('Must-have', 'mailpoet'),
|
||||
'style' => 'red',
|
||||
|
@ -16,6 +16,7 @@ use MailPoet\Automation\Engine\Hooks;
|
||||
use MailPoet\Automation\Engine\Integration\Action;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Storage\WorkflowRunLogStorage;
|
||||
use MailPoet\Automation\Engine\Storage\WorkflowRunStorage;
|
||||
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
|
||||
@ -50,6 +51,9 @@ class StepHandler {
|
||||
/** @var Hooks */
|
||||
private $hooks;
|
||||
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
ActionScheduler $actionScheduler,
|
||||
ActionStepRunner $actionStepRunner,
|
||||
@ -58,7 +62,8 @@ class StepHandler {
|
||||
WordPress $wordPress,
|
||||
WorkflowRunStorage $workflowRunStorage,
|
||||
WorkflowRunLogStorage $workflowRunLogStorage,
|
||||
WorkflowStorage $workflowStorage
|
||||
WorkflowStorage $workflowStorage,
|
||||
Registry $registry
|
||||
) {
|
||||
$this->actionScheduler = $actionScheduler;
|
||||
$this->actionStepRunner = $actionStepRunner;
|
||||
@ -68,6 +73,7 @@ class StepHandler {
|
||||
$this->workflowRunStorage = $workflowRunStorage;
|
||||
$this->workflowRunLogStorage = $workflowRunLogStorage;
|
||||
$this->workflowStorage = $workflowStorage;
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function initialize(): void {
|
||||
@ -124,19 +130,19 @@ class StepHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
$step = $workflow->getStep($stepId);
|
||||
if (!$step) {
|
||||
$stepData = $workflow->getStep($stepId);
|
||||
if (!$stepData) {
|
||||
throw Exceptions::workflowStepNotFound($stepId);
|
||||
}
|
||||
|
||||
$stepType = $step->getType();
|
||||
$step = $this->registry->getStep($stepData->getKey());
|
||||
$stepType = $stepData->getType();
|
||||
if (isset($this->stepRunners[$stepType])) {
|
||||
$log = new WorkflowRunLog($workflowRun->getId(), $step->getId());
|
||||
$log = new WorkflowRunLog($workflowRun->getId(), $stepData->getId());
|
||||
try {
|
||||
$requiredSubjects = $step instanceof Action ? $step->getSubjectKeys() : [];
|
||||
$subjectEntries = $this->getSubjectEntries($workflowRun, $requiredSubjects);
|
||||
$args = new StepRunArgs($workflow, $workflowRun, $step, $subjectEntries);
|
||||
$validationArgs = new StepValidationArgs($workflow, $step, array_map(function (SubjectEntry $entry) {
|
||||
$args = new StepRunArgs($workflow, $workflowRun, $stepData, $subjectEntries);
|
||||
$validationArgs = new StepValidationArgs($workflow, $stepData, array_map(function (SubjectEntry $entry) {
|
||||
return $entry->getSubject();
|
||||
}, $subjectEntries));
|
||||
$this->stepRunners[$stepType]->run($args, $validationArgs);
|
||||
@ -157,7 +163,7 @@ class StepHandler {
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
|
||||
$nextStep = $step->getNextSteps()[0] ?? null;
|
||||
$nextStep = $stepData->getNextSteps()[0] ?? null;
|
||||
$nextStepArgs = [
|
||||
[
|
||||
'workflow_run_id' => $workflowRunId,
|
||||
|
@ -26,6 +26,9 @@ class WorkflowStatisticsStorage {
|
||||
* @throws \MailPoet\Automation\Engine\Exceptions\InvalidStateException
|
||||
*/
|
||||
public function getWorkflowStatisticsForWorkflows(Workflow ...$workflows): array {
|
||||
if (empty($workflows)) {
|
||||
return [];
|
||||
}
|
||||
$workflowIds = array_map(
|
||||
function(Workflow $workflow): int {
|
||||
return $workflow->getId();
|
||||
|
@ -93,6 +93,11 @@ class WorkflowStorage {
|
||||
}, (array)$data);
|
||||
}
|
||||
|
||||
public function getWorkflowCount(): int {
|
||||
$workflowTable = esc_sql($this->workflowTable);
|
||||
return (int)$this->wpdb->get_var("SELECT COUNT(*) FROM $workflowTable");
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getActiveTriggerKeys(): array {
|
||||
$workflowTable = esc_sql($this->workflowTable);
|
||||
|
@ -122,8 +122,15 @@ class AutomaticEmailScheduler {
|
||||
// try to find existing scheduled task for given subscriber
|
||||
$task = $this->scheduledTasksRepository->findOneScheduledByNewsletterAndSubscriber($newsletter, $subscriber);
|
||||
if ($task) {
|
||||
$this->sendingQueuesRepository->deleteByTask($task);
|
||||
$queue = $task->getSendingQueue();
|
||||
if ($queue instanceof SendingQueueEntity) {
|
||||
$this->sendingQueuesRepository->remove($queue);
|
||||
}
|
||||
$this->scheduledTaskSubscribersRepository->deleteByTask($task);
|
||||
// In case any of task associated SchedulesTaskSubscriberEntity was loaded we need to detach them
|
||||
foreach ($task->getSubscribers() as $taskSubscriber) {
|
||||
$this->scheduledTaskSubscribersRepository->detach($taskSubscriber);
|
||||
}
|
||||
$this->scheduledTasksRepository->remove($task);
|
||||
$this->scheduledTasksRepository->flush();
|
||||
}
|
||||
|
@ -338,18 +338,43 @@ class WooCommerce {
|
||||
}
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
$metaKeys = [
|
||||
'_billing_first_name',
|
||||
'_billing_last_name',
|
||||
];
|
||||
$metaData = $this->connection->executeQuery("
|
||||
SELECT post_id, meta_key, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key IN (:metaKeys) AND post_id IN (:postIds)
|
||||
",
|
||||
['metaKeys' => $metaKeys, 'postIds' => array_values($orders)],
|
||||
['metaKeys' => Connection::PARAM_STR_ARRAY, 'postIds' => Connection::PARAM_INT_ARRAY]
|
||||
)->fetchAllAssociative();
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$addressesTableName = $this->woocommerceHelper->getAddressesTableName();
|
||||
$metaData = [];
|
||||
$results = $this->connection->executeQuery("
|
||||
SELECT order_id, first_name, last_name
|
||||
FROM {$addressesTableName}
|
||||
WHERE order_id IN (:orderIds) and address_type = 'billing'",
|
||||
['orderIds' => array_values($orders)],
|
||||
['orderIds' => Connection::PARAM_INT_ARRAY]
|
||||
)->fetchAllAssociative();
|
||||
|
||||
// format data in the same format that is used when querying wp_postmeta (see below).
|
||||
foreach ($results as $result) {
|
||||
$firstNameData['post_id'] = $result['order_id'];
|
||||
$firstNameData['meta_key'] = '_billing_first_name';
|
||||
$firstNameData['meta_value'] = $result['first_name'];
|
||||
$metaData[] = $firstNameData;
|
||||
|
||||
$lastNameData['post_id'] = $result['order_id'];
|
||||
$lastNameData['meta_key'] = '_billing_last_name';
|
||||
$lastNameData['meta_value'] = $result['last_name'];
|
||||
$metaData[] = $lastNameData;
|
||||
}
|
||||
} else {
|
||||
$metaKeys = [
|
||||
'_billing_first_name',
|
||||
'_billing_last_name',
|
||||
];
|
||||
$metaData = $this->connection->executeQuery("
|
||||
SELECT post_id, meta_key, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key IN ('_billing_first_name', '_billing_last_name') AND post_id IN (:postIds)
|
||||
",
|
||||
['metaKeys' => $metaKeys, 'postIds' => array_values($orders)],
|
||||
['metaKeys' => Connection::PARAM_STR_ARRAY, 'postIds' => Connection::PARAM_INT_ARRAY]
|
||||
)->fetchAllAssociative();
|
||||
}
|
||||
|
||||
$subscribersData = [];
|
||||
foreach ($orders as $email => $postId) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace MailPoet\WooCommerce;
|
||||
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Query;
|
||||
use MailPoet\DI\ContainerWrapper;
|
||||
use MailPoet\RuntimeException;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
@ -102,6 +103,18 @@ class Helper {
|
||||
return (new \WC_Countries)->get_allowed_countries() ?? [];
|
||||
}
|
||||
|
||||
public function getCustomersCount(): int {
|
||||
if (!class_exists(Query::class)) {
|
||||
return 0;
|
||||
}
|
||||
$query = new Query([
|
||||
'fields' => ['customers_count'],
|
||||
]);
|
||||
// Query::get_data declares it returns array but the underlying DataStore returns stdClass
|
||||
$result = (array)$query->get_data();
|
||||
return isset($result['customers_count']) ? intval($result['customers_count']) : 0;
|
||||
}
|
||||
|
||||
public function wasMailPoetInstalledViaWooCommerceOnboardingWizard(): bool {
|
||||
$wp = ContainerWrapper::getInstance()->get(WPFunctions::class);
|
||||
$installedViaWooCommerce = false;
|
||||
@ -126,4 +139,12 @@ class Helper {
|
||||
|
||||
return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_orders_table_name();
|
||||
}
|
||||
|
||||
public function getAddressesTableName() {
|
||||
if (!method_exists('\Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore', 'get_addresses_table_name')) {
|
||||
throw new RuntimeException('Cannot get addresses table name when running a WooCommerce version that doesn\'t support custom order tables.');
|
||||
}
|
||||
|
||||
return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_addresses_table_name();
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
/*
|
||||
* Plugin Name: MailPoet
|
||||
* Version: 3.101.0
|
||||
* Version: 3.101.1
|
||||
* Plugin URI: http://www.mailpoet.com
|
||||
* Description: Create and send newsletters, post notifications and welcome emails from your WordPress.
|
||||
* Author: MailPoet
|
||||
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
$mailpoetPlugin = [
|
||||
'version' => '3.101.0',
|
||||
'version' => '3.101.1',
|
||||
'filename' => __FILE__,
|
||||
'path' => dirname(__FILE__),
|
||||
'autoloader' => dirname(__FILE__) . '/vendor/autoload.php',
|
||||
|
@ -3,7 +3,7 @@ Contributors: mailpoet
|
||||
Tags: email, email marketing, post notification, woocommerce emails, email automation, newsletter, newsletter builder, newsletter subscribers
|
||||
Requires at least: 5.6
|
||||
Tested up to: 6.0
|
||||
Stable tag: 3.101.0
|
||||
Stable tag: 3.101.1
|
||||
Requires PHP: 7.2
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
@ -219,6 +219,11 @@ Check our [Knowledge Base](https://kb.mailpoet.com) or contact us through our [s
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 3.101.1 - 2022-10-24 =
|
||||
* Improved: simplified privacy and data sharing section in onboarding;
|
||||
* Improved: don't require any newsletter settings when saving a draft;
|
||||
* Fixed: an error in the checkout, when the shop has multiple automatic emails set up.
|
||||
|
||||
= 3.101.0 - 2022-10-17 =
|
||||
* Added: new API method getSubscribersCount;
|
||||
* Added: new API method getSubscribers;
|
||||
|
@ -60,9 +60,13 @@ parameters:
|
||||
message: '/^Call to static method get_orders_table_name\(\) on an unknown class Automattic\\WooCommerce\\Internal\\DataStores\\Orders\\OrdersTableDataStore\.$/'
|
||||
count: 1
|
||||
path: ../../lib/WooCommerce/Helper.php
|
||||
-
|
||||
message: '/^Call to static method get_addresses_table_name\(\) on an unknown class Automattic\\WooCommerce\\Internal\\DataStores\\Orders\\OrdersTableDataStore\.$/'
|
||||
count: 1
|
||||
path: ../../lib/WooCommerce/Helper.php
|
||||
-
|
||||
message: '/^Call to function method_exists\(\) with/'
|
||||
count: 2
|
||||
count: 3
|
||||
path: ../../lib/WooCommerce/Helper.php
|
||||
- # WooCommerce stubs contains stubs for older ActionScheduler version
|
||||
message: '/^Function as_schedule_recurring_action invoked with 6 parameters, 3-5 required.$/'
|
||||
|
@ -706,6 +706,15 @@ class AcceptanceTester extends \Codeception\Actor {
|
||||
$i->cli(['action-scheduler', 'run', '--force']);
|
||||
}
|
||||
|
||||
public function triggerAutomationActionScheduler(): void {
|
||||
$i = $this;
|
||||
// Reschedule automation trigger action to run immediately
|
||||
$i->importSql([
|
||||
"UPDATE mp_actionscheduler_actions SET scheduled_date_gmt = SUBTIME(now(), '01:00:00'), scheduled_date_local = SUBTIME(now(), '01:00:00') WHERE hook = 'mailpoet/automation/workflow/step' AND status = 'pending';",
|
||||
]);
|
||||
$i->cli(['action-scheduler', 'run', '--force']);
|
||||
}
|
||||
|
||||
public function isWooCustomOrdersTableEnabled(): bool {
|
||||
return (bool)getenv('ENABLE_COT');
|
||||
}
|
||||
|
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace MailPoet\Test\Acceptance;
|
||||
|
||||
use Facebook\WebDriver\WebDriverKeys;
|
||||
use MailPoet\Automation\Engine\Migrations\Migrator;
|
||||
use MailPoet\DI\ContainerWrapper;
|
||||
use MailPoet\Features\FeaturesController;
|
||||
use MailPoet\Test\DataFactories\Features;
|
||||
use MailPoet\Test\DataFactories\Settings;
|
||||
|
||||
class CreateEmailAutomationAndWalkThroughCest
|
||||
{
|
||||
public function _before() {
|
||||
// @ToDo Remove once MVP is released.
|
||||
$features = new Features();
|
||||
$features->withFeatureEnabled(FeaturesController::AUTOMATION);
|
||||
$container = ContainerWrapper::getInstance();
|
||||
$migrator = $container->get(Migrator::class);
|
||||
$migrator->createSchema();
|
||||
|
||||
$settings = new Settings();
|
||||
$settings->withCronTriggerMethod('Action Scheduler');
|
||||
}
|
||||
|
||||
public function createEmailWorkflowAndReceiveAnAutomatedEmail(\AcceptanceTester $i) {
|
||||
$i->wantTo('Create a workflow to send an email after a user subscribed');
|
||||
$i->login();
|
||||
|
||||
$i->amOnMailpoetPage('Automation');
|
||||
$i->see('Automations');
|
||||
$i->waitForText('Scale your business with advanced automations');
|
||||
$i->dontSee('Simple welcome email');
|
||||
$i->dontSee('Active');
|
||||
$i->dontSee('Entered');
|
||||
|
||||
$i->click('New automation');
|
||||
$i->see('Choose your automation template');
|
||||
$i->click('Simple welcome email');
|
||||
|
||||
$i->waitForText('Draft');
|
||||
$i->click('Trigger');
|
||||
$i->fillField('When someone subscribers to the following list(s):', 'Newsletter mailing list');
|
||||
$i->click('Delay');
|
||||
$i->fillField('Wait for', '5');
|
||||
|
||||
$i->click('Send email');
|
||||
$i->fillField('“From” name','From');
|
||||
$i->fillField('“From” email address','test@mailpoet.com');
|
||||
$i->fillField('Subject','Automation-Test-Subject');
|
||||
|
||||
$i->click('Design email');
|
||||
$i->waitForText('Newsletters');
|
||||
$i->click('Newsletters');
|
||||
$i->click('button[data-automation-id="select_template_0"]');
|
||||
$i->waitForText('Design');
|
||||
$i->click('Save and continue');
|
||||
|
||||
$i->waitForText('Draft');
|
||||
|
||||
$i->click('Send email');
|
||||
$i->click('Reply to');
|
||||
$i->fillField('“Reply to” name', 'Reply');
|
||||
$i->fillField('“Reply to” email address', 'reply@mailpoet.com');
|
||||
|
||||
$i->click('Activate');
|
||||
$i->waitForText('Are you ready to activate?');
|
||||
|
||||
// We use a selector to be specific about which Activate button we want to click.
|
||||
$panelActivateButton = '.mailpoet-automation-activate-panel__header-activate-button button';
|
||||
$i->click($panelActivateButton);
|
||||
|
||||
// Check workflow is activated
|
||||
$i->waitForText('"Simple welcome email" is now live.');
|
||||
$i->click('View all automations');
|
||||
$i->waitForText('Name');
|
||||
$i->see('Simple welcome email');
|
||||
$i->see('Active');
|
||||
$i->see('Entered 0'); //Actually I see "0 Entered", but this CSS switch is not caught by the test
|
||||
$i->dontSeeInDatabase('mp_actionscheduler_actions', ['hook' => 'mailpoet/automation/workflow/step']);
|
||||
|
||||
$i->wantTo('Check a new subscriber gets the automation email.');
|
||||
$i->amOnPage('/wp-admin/admin.php?page=mailpoet-subscribers#/new');
|
||||
$i->fillField('#field_email', 'test@mailpoet.com');
|
||||
$i->fillField('#field_first_name', 'automation-tester-firstname');
|
||||
$i->selectOptionInSelect2('Newsletter mailing list');
|
||||
$i->click('Save');
|
||||
|
||||
$i->amOnMailpoetPage('Automation');
|
||||
$i->seeInDatabase('mp_actionscheduler_actions', ['hook' => 'mailpoet/automation/workflow/step', 'status' => 'pending']);
|
||||
$i->waitForText('Simple welcome email');
|
||||
$i->see('Entered 1'); //Actually I see "0 Entered", but this CSS switch is not caught by the test
|
||||
$i->see('Processing 1');
|
||||
$i->see('Exited 0');
|
||||
$i->amOnMailboxAppPage();
|
||||
$i->see('Inbox (0)');
|
||||
|
||||
// Jump the waiting time by scheduling the delay action to now.
|
||||
$i->triggerAutomationActionScheduler(); // Initialize the run, creates the delay step
|
||||
$i->triggerAutomationActionScheduler(); // Set delay scheduled at to now, runs delay and send email
|
||||
$i->triggerMailPoetActionScheduler(); // Runs the email queue
|
||||
|
||||
$i->amOnUrl('http://test.local/wp-admin/');
|
||||
$i->amOnMailpoetPage('Automation');
|
||||
$i->waitForText('Simple welcome email');
|
||||
$i->see('Entered 1'); //Actually I see "0 Entered", but this CSS switch is not caught by the test
|
||||
$i->see('Processing 0');
|
||||
$i->see('Exited 1');
|
||||
$i->amOnMailboxAppPage();
|
||||
$i->see('Inbox (1)');
|
||||
$i->see('Automation-Test-Subject');
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ class CreateWooCommerceNewsletterCest {
|
||||
$i->click($template);
|
||||
|
||||
$this->fillNewsletterTitle($i, 'Abandoned Cart Email Creation');
|
||||
$this->activateNewsletterAndVerify($i, 'Abandoned Cart Email Creation', 'Email sent when a customer abandons his cart');
|
||||
$this->activateNewsletterAndVerify($i, 'Abandoned Cart Email Creation', 'Send the email when a customer abandons their cart');
|
||||
}
|
||||
|
||||
private function fillNewsletterTitle(\AcceptanceTester $i, $newsletterTitle) {
|
||||
|
@ -35,6 +35,10 @@ class WooCommerceSetupPageCest {
|
||||
$order = $this->orderFactory->create();
|
||||
$guestUserData = $order['billing'];
|
||||
$registeredCustomer = $this->customerFactory->withEmail('customer1@email.com')->create();
|
||||
// run action scheduler to sync customer and order data to lookup tables
|
||||
$i->wait(2);
|
||||
$i->cli(['action-scheduler', 'run', '--force']);
|
||||
|
||||
$i->login();
|
||||
$i->amOnPage('wp-admin/admin.php?page=mailpoet-woocommerce-setup');
|
||||
$importTypeToggle = '[data-automation-id="woocommerce_import_type"]';
|
||||
@ -72,9 +76,29 @@ class WooCommerceSetupPageCest {
|
||||
$i->see($guestUserData['email']);
|
||||
}
|
||||
|
||||
|
||||
public function noCustomersBehaviourTest(\AcceptanceTester $i) {
|
||||
$i->wantTo('Make sure we don‘t show import setting when there are no customers');
|
||||
$i->login();
|
||||
$i->amOnPage('wp-admin/admin.php?page=mailpoet-woocommerce-setup');
|
||||
$i->see('Get ready to use MailPoet for WooCommerce');
|
||||
$importTypeToggle = '[data-automation-id="woocommerce_import_type"]';
|
||||
$trackingToggle = '[data-automation-id="woocommerce_tracking"]';
|
||||
$submitButton = '[data-automation-id="submit_woocommerce_setup"]';
|
||||
$errorClass = '.mailpoet-form-yesno-error';
|
||||
$i->dontSeeElement($importTypeToggle);
|
||||
$i->seeElement($trackingToggle);
|
||||
}
|
||||
|
||||
public function setupPageFormBehaviourTest(\AcceptanceTester $i) {
|
||||
$order = $this->orderFactory->create();
|
||||
|
||||
$i->wantTo('Make sure the form shows errors when it is submitted without making choices');
|
||||
$i->login();
|
||||
// run action scheduler to sync customer and order data to lookup tables
|
||||
$i->wait(2);
|
||||
$i->cli(['action-scheduler', 'run', '--force']);
|
||||
|
||||
$i->amOnPage('wp-admin/admin.php?page=mailpoet-woocommerce-setup');
|
||||
$i->see('Get ready to use MailPoet for WooCommerce');
|
||||
$importTypeToggle = '[data-automation-id="woocommerce_import_type"]';
|
||||
|
@ -347,6 +347,7 @@ class AbandonedCartTest extends \MailPoetTest {
|
||||
$this->entityManager->flush();
|
||||
|
||||
$scheduledTask->setScheduledAt($scheduleAt);
|
||||
$scheduledTask->setSendingQueue($sendingQueue);
|
||||
$scheduledTask->setStatus(($this->currentTime < $scheduleAt) ? ScheduledTaskEntity::STATUS_SCHEDULED : ScheduledTaskEntity::STATUS_COMPLETED);
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
@ -50,16 +50,7 @@ class AutomaticEmailTest extends \MailPoetTest {
|
||||
$this->scheduledTasksRepository = $this->diContainer->get(ScheduledTasksRepository::class);
|
||||
|
||||
$this->newsletterFactory = new NewsletterFactory();
|
||||
$this->newsletter = $this->newsletterFactory->withActiveStatus()->withAutomaticType()->create();
|
||||
$this->newsletterOptionFactory = new NewsletterOptionFactory();
|
||||
$this->newsletterOptionFactory->createMultipleOptions(
|
||||
$this->newsletter,
|
||||
[
|
||||
'sendTo' => 'user',
|
||||
'afterTimeType' => 'hours',
|
||||
'afterTimeNumber' => 2,
|
||||
]
|
||||
);
|
||||
$this->newsletter = $this->createAutomaticNewsletter();
|
||||
}
|
||||
|
||||
public function testItCreatesScheduledAutomaticEmailSendingTaskForUser() {
|
||||
@ -149,6 +140,30 @@ class AutomaticEmailTest extends \MailPoetTest {
|
||||
$this->assertCount(0, $this->sendingQueuesRepository->findAll());
|
||||
}
|
||||
|
||||
public function testItCanCancelMultipleAutomaticEmails() {
|
||||
$newsletter = $this->newslettersRepository->findOneById($this->newsletter->getId());
|
||||
$this->newsletterOptionFactory->createMultipleOptions(
|
||||
$this->newsletter,
|
||||
[
|
||||
'group' => 'some_group',
|
||||
'event' => 'some_event',
|
||||
]
|
||||
);
|
||||
$newsletter2 = $this->createAutomaticNewsletter();
|
||||
$this->newsletterOptionFactory->createMultipleOptions(
|
||||
$newsletter2,
|
||||
[
|
||||
'group' => 'some_group',
|
||||
'event' => 'some_event',
|
||||
]
|
||||
);
|
||||
$this->assertInstanceOf(NewsletterEntity::class, $newsletter);
|
||||
$subscriber = (new SubscriberFactory())->create();
|
||||
$this->automaticEmailScheduler->createAutomaticEmailScheduledTask($newsletter, $subscriber);
|
||||
$this->automaticEmailScheduler->createAutomaticEmailScheduledTask($newsletter2, $subscriber);
|
||||
$this->automaticEmailScheduler->cancelAutomaticEmail('some_group', 'some_event', $subscriber);
|
||||
}
|
||||
|
||||
public function testItSchedulesAutomaticEmailWhenConditionMatches() {
|
||||
$currentTime = Carbon::createFromTimestamp(WPFunctions::get()->currentTime('timestamp'));
|
||||
$this->newsletterOptionFactory->createMultipleOptions(
|
||||
@ -200,6 +215,20 @@ class AutomaticEmailTest extends \MailPoetTest {
|
||||
->equals($currentTime->addHours(2)->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
private function createAutomaticNewsletter(): NewsletterEntity {
|
||||
$newsletter = $this->newsletterFactory->withActiveStatus()->withAutomaticType()->create();
|
||||
$this->newsletterOptionFactory = new NewsletterOptionFactory();
|
||||
$this->newsletterOptionFactory->createMultipleOptions(
|
||||
$newsletter,
|
||||
[
|
||||
'sendTo' => 'user',
|
||||
'afterTimeType' => 'hours',
|
||||
'afterTimeNumber' => 2,
|
||||
]
|
||||
);
|
||||
return $newsletter;
|
||||
}
|
||||
|
||||
public function _after() {
|
||||
Carbon::setTestNow();
|
||||
$this->truncateEntity(NewsletterEntity::class);
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
<% block content %>
|
||||
<div class="wrap">
|
||||
<div id="mailpoet_notices"></div>
|
||||
<div id="mailpoet_automation"></div>
|
||||
</div>
|
||||
<% endblock %>
|
||||
@ -10,6 +9,7 @@
|
||||
<% block after_javascript %>
|
||||
<script type="text/javascript">
|
||||
var mailpoet_automation_api = <%= json_encode(api) %>;
|
||||
var mailpoet_workflow_count = <%= json_encode(workflowCount) %>;
|
||||
</script>
|
||||
<%= javascript('automation.js')%>
|
||||
<% endblock %>
|
||||
|
@ -410,6 +410,7 @@
|
||||
|
||||
'gaCampaignLine': __('Google Analytics Campaign'),
|
||||
'gaCampaignTip': __('For example, “Spring email”. [link]Read the guide.[/link]'),
|
||||
'gaOnlyAvailableForPremium': __('Google Analytics tracking is not available in the free version of the MailPoet plugin.'),
|
||||
|
||||
'automaticEmail': __('Automatic Email'),
|
||||
'tip': __('Tip:'),
|
||||
|
@ -11,6 +11,7 @@
|
||||
var sender_data = <%= json_encode(sender) %>;
|
||||
var admin_email = <%= json_encode(admin_email) %>;
|
||||
var hide_mailpoet_beacon = true;
|
||||
var mailpoet_show_customers_import = <%= json_encode(show_customers_import) %>;
|
||||
var mailpoet_account_url = '<%= add_referral_id("https://account.mailpoet.com/?s=" ~ subscriber_count ~ "&email=" ~ current_wp_user.user_email|escape('js')) %>';
|
||||
</script>
|
||||
|
||||
@ -26,22 +27,13 @@
|
||||
<%= localize({
|
||||
'welcomeWizardLetsStartTitle': __('Welcome! Let’s get you started on the right foot.'),
|
||||
'welcomeWizardSenderText': __('Who is the sender of the emails you’ll be creating with MailPoet?'),
|
||||
'welcomeWizardUsageTrackingStepTitle': __('Help MailPoet improve with anonymous usage tracking.'),
|
||||
'welcomeWizardUsageTrackingStepSubTitle': __('Data we don’t gather:'),
|
||||
'welcomeWizardUsageTrackingStepTrackingLabel': __('Do you want to share anonymous data and help us improve the plugin? We appreciate your help!'),
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNote': __('This requires loading 3rd-party libraries enabled.'),
|
||||
'welcomeWizardUsageTrackingStepTitle': __('Privacy and data sharing'),
|
||||
'welcomeWizardUsageTrackingStepTrackingLabel': __('Help improve MailPoet'),
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNote': __('Get improved features and fixes faster by sharing with us [link]non-sensitive data about how you use MailPoet[/link]. No personal data is tracked or stored.'),
|
||||
'welcomeWizardUsageTrackingStepTrackingLabelNoteNote': __('Note'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNote': __('This needs to be enabled if you want to be able to contact support.'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNoteNote': __('Important'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabel': __('Do you want to enable loading 3rd-party libraries, like Google Fonts (used in Form Editor) and HelpScout (used for support)?'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLink': __('Read more about libraries we use.'),
|
||||
'welcomeWizardTrackingText': __('Gathering usage data allows us to make MailPoet better — the way you use MailPoet will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense.'),
|
||||
'welcomeWizardTrackingList1': __('Any personal data'),
|
||||
'welcomeWizardTrackingList2': __('Email addresses'),
|
||||
'welcomeWizardTrackingList3': __('Login and passwords'),
|
||||
'welcomeWizardTrackingList4': __('Content of your emails'),
|
||||
'welcomeWizardTrackingList5': __('Open and click rates'),
|
||||
'welcomeWizardTrackingLink': __('Read more about what we collect.'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNote': __('If enabled, we may load the Google Fonts library and [link]other 3rd-party libraries we use[/link].'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabelNoteNote': __('Note'),
|
||||
'welcomeWizardUsageTrackingStepLibs3rdPartyLabel': __('Enable better-looking Google fonts in emails and show contextual help articles in MailPoet?'),
|
||||
'seeVideoGuide': _x('See video guide', 'A label on a button'),
|
||||
'skip': _x('Skip', 'A label on a skip button'),
|
||||
'finishLater': _x('Finish later', 'A label on a skip button'),
|
||||
|
@ -4,6 +4,7 @@
|
||||
<script>
|
||||
var mailpoet_logo_url = '<%= cdn_url('welcome-wizard/mailpoet-logo.20200623.png') %>';
|
||||
var wizard_woocommerce_illustration_url = '<%= cdn_url('welcome-wizard/woocommerce.20200623.png') %>';
|
||||
var mailpoet_show_customers_import = <%= json_encode(show_customers_import) %>;
|
||||
var finish_wizard_url = '<%= finish_wizard_url %>';
|
||||
</script>
|
||||
|
||||
|
@ -4,9 +4,9 @@
|
||||
'wooCommerceSetupGDPRTag': _x('GDPR', 'WooCommerce setup GDPR tag'),
|
||||
'wooCommerceSetupImportInfo': __('MailPoet will import all your WooCommerce customers. Do you want to import your WooCommerce customers as subscribed? [link]Learn more[/link].'),
|
||||
'wooCommerceSetupImportGDPRInfo': _x('To be compliant, your customers must have accepted to receive your emails.', 'GDPR compliance information'),
|
||||
'wooCommerceSetupTrackingInfo': __('Do you want to enable cookie tracking on your website? MailPoet will use cookies to provide you with more precise statistics. [link]Learn more[/link].'),
|
||||
'wooCommerceSetupTrackingInfo': __('Collect more precise email and site engagement, and e-commerce metrics by enabling cookie tracking. [link]Learn more[/link].'),
|
||||
'wooCommerceSetupTrackingGDPRInfo': _x('To be compliant, you should display a cookie tracking banner on your website.', 'GDPR compliance information'),
|
||||
'wooCommerceSetupFinishButtonTextWizard': _x('Start using MailPoet', 'Submit button caption in the WooCommerce step in the wizard'),
|
||||
'wooCommerceSetupFinishButtonTextStandalone': _x('Start using WooCommerce features', 'Submit button caption on the standalone WooCommerce setup page'),
|
||||
'unknownError': __('Unknown error'),
|
||||
}) %>
|
||||
}) %>
|
||||
|
Reference in New Issue
Block a user