Compare commits

..

75 Commits

Author SHA1 Message Date
0077818bf5 Release 3.101.1 2022-10-24 18:13:12 +02:00
bbdbf6a52d Fix WooCommerce setup in the welcome wizard
The component that requires the global mailpoet_show_customers_import
variable is used also on the wizard page.
This commit ensures that it is set on the page.
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
56ed9f4ece Update acceptance tests for Woo setup page
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
e6188f5cef Update WooCommerce cookie tracking info in Wizard
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
a37e5bbe74 Hide WooCommerce initial import from wizard when there are no customers
[MAILPOET-4672]
2022-10-24 15:16:04 +02:00
9df99b1a00 Add method for getting customers count to WooCommerce helper
[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
5ff543e62d Unify default values of mailpoet_subscribe_old_woocommerce_customers
The value is used in case the setting is not defined. Normally this shouldn't
happen because we set the value in the wizard.
The default value used in PHP is false, but in settings in admin was true.
See b0aacdd4ef/mailpoet/lib/Segments/WooCommerce.php (L528)
This commit unifies it.

[MAILPOET-4652]
2022-10-24 15:16:04 +02:00
8f14c5ac53 Fix issue caused by merge conflict
[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
4e0aa3c56d Remove redundant code which was selectively disabling validation
We no longer need selective validation disabling since we're
disabling all validations when saving a draft

[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
545c6bfa5e Disable all validations when saving a draft
[MAILPOET-4591]
2022-10-24 14:05:41 +02:00
cd3652eaa6 Fix canceling multiple automatic emails
When we deleted sending queue using SQL it remained in the entity manager
and subsequent flush (not the first one) triggered the error, because it didn't know the ScheduledTask entity attached
to the orphaned SendingQueue entity.

This commit fixes this by refactoring deletion of sending queue using standard repository method.
After fixing the issue for sending queue there was another issue with SchedulesTaskSubscriberEntity that remained in memory.
I fixed that by detaching those. Theoretically there might be many SchedulesTaskSubscriberEntities for an Automatic email so
I consider still safer to delete using SQL and if there are some loaded (in this case there is one) detach them.
[MAILPOET-4741]
2022-10-24 14:03:54 +02:00
655641737b Do not use toggleModal()
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
152794720a Add DeactivateButton and logic
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
cb294fb303 Style DeactivateModal component
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
98744a53c1 Add Deactivate Modal
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
430fcc20a8 Add new store action to deactivate a workflow
[MAILPOET-4730]
2022-10-24 13:49:15 +03:00
1df78f22d3 Make the message better
[MAILPOET-4740]
2022-10-24 10:18:28 +02:00
2f823f5606 On settings page, Remove the dependency between loading 3rd-party libraries and tracking (data sharing). Allow user choose toggle values independently
MAILPOET-4657
2022-10-20 10:49:48 +02:00
086d6dce7e On WelcomeWizardUsageTrackingStep, Remove the dependency between loading 3rd-party libraries and tracking (data sharing). Allow user choose toggle values independently
MAILPOET-4657
2022-10-20 10:49:48 +02:00
879cca9fb3 On WelcomeWizardUsageTrackingStep, update section wordings and re-order items
MAILPOET-4657
2022-10-20 10:49:48 +02:00
3434fbe3b5 Add not-allowed cursor to disabled form fields
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
d4f9ccca5e Add disabled class to the wrapper of a disabled field
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
4a74c3e6fd Open premium modal if the premium plugin is not active
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
60d9a3f1ac Add posibility to add wrapper click handler to form fields
In some cases we need to disable the form field but add a click
handler to the field, but to make it work accross browsers the
click handler should get added to the wrapper and not the disabled
form field itself

[PREMIUM-200]
2022-10-20 10:30:30 +02:00
b64fbbdb7f Remove ga field default value when disabled
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
12e8d44a43 Disable ga field when premium plugin is not active
[PREMIUM-200]
2022-10-20 10:30:30 +02:00
667658ae2f Show 'is now activated' snackbar only when status of workflow is active
[MAILPOET-4737]
2022-10-20 10:24:06 +02:00
9d2624163c Close ActivatePanel when there are errors
[MAILPOET-4737]
2022-10-20 10:24:06 +02:00
8f6688eba7 Check for CircleCI results file existence 2022-10-20 09:31:58 +02:00
b360d9a2cf Set focus on new activate button
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
11384bbf6a Fix bug where the type of $step was Data\Step instead of Integration\Step
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
6235944442 Fix bug where an empty array would generate a malformed SQL query
[MAILPOET-4732]
2022-10-19 13:34:45 +02:00
83573b0d43 Use labels to make editor more accessible
[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
bd87c09e1a Add acceptance test for automation workflow
* Creates an simple welcome email workflow
* Activates it
* Subscribes and checks whether the email arrives in the inbox

[MAILPOET-4530]
2022-10-19 13:34:45 +02:00
1010b64c05 Update updateNames() to work with Woo Custom Order Tables
[MAILPOET-4711]
2022-10-19 11:32:41 +02:00
dd1fcd5100 Add helper method to get the name of the WooCommerce order addresses table
[MAILPOET-4711]
2022-10-19 11:32:41 +02:00
75706c9e8b Store success message of activation in activate action
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
61e1dd6a83 Add an activating state
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
8d5af952f6 Use sprintf instead of replace
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
2751a4bf3a End the sentence with a period
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
ed297dd68d Show snackbar notice
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
17c7d42ede Remove activate toggle
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
1741b39375 Style ActivatePanel
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
800432ab54 Introduce ActivatePanel
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
f169c8f8ac Use save method in Update button
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
f92a12db30 Make strings translateable
[MAILPOET-4462]
2022-10-19 09:07:33 +02:00
3ea730a0b6 Fix tab button focus styles
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
e3c19fa306 Improve listing styles
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
f2c4890def Avoid loosing tab focus on navigation
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
5bc2f62d98 Improve namin & add small simplifications to listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c954603bfc Improve popover rendering
1) Rendering it in a slot (under slot fill provider) avoids inheriting deeply nested styles.
2) Bottom-left is a nicer default as it's the rightmost button in the table rows.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
bf552801ec Fix editor stats label color
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
6bb1af0a18 Simplify and fix stats rendering using CSS grid
Fixed separator rendering, simplified CSS (grid can do this), removed
had extra paddings that were causing problems on the listing page.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
48dca5e298 Reuse stats component in workflow listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c08813be1d Extract workflow stats to atomation root component level
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
1cc6d93717 Do not wrap or shrink status cells
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
18a071fd7b Render "edit" and "more" actions in the same column
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
32d310d999 Add listing api error handler
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
c76d18e647 Add edit workflow link
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
865706c112 Extract edit workflow from cell to a generic action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
3238049828 Add undo trash button
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
a41de82030 Add workflow duplicate/trash/restore/delete success notices
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
ccec1faeb1 Add notices component for automation listing
This component combines data store from @wordpress/notices together with
MailPoet's notice rendering that is suitable for listing.

[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
b19887add0 Remove duplicate notices div
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
ba2cb75877 Add delete confirmation dialog
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
b89905aa80 Add trash confirmation dialog
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
a25e879cac Add delete workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
0106c5123d Add restore workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
2cb20a8f63 Add trash workflow action
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
fe1a994442 Add duplicate workflow action, use dropdown from @wordpress/components
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
70889ab06d Load workflows list from store
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
aab6865f50 Initialize automation listing store
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
efd32043e3 Add workflow count to automation listing page
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
3eaa13a421 Add store for automation listing
[MAILPOET-4540]
2022-10-19 08:09:17 +03:00
80d2ab44a3 Release 3.101.0 2022-10-18 14:42:07 +02:00
86 changed files with 1825 additions and 485 deletions

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
.mailpoet-automation-listing-cell-actions {
align-items: center;
display: grid;
gap: 8px;
grid-auto-flow: column;
white-space: nowrap;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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);
}
});

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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)

View File

@ -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>

View File

@ -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({

View File

@ -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,

View File

@ -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) ?? ''}

View File

@ -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;
}
},
);

View File

@ -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>
);
}

View File

@ -0,0 +1,2 @@
export * from './edit-workflow';
export * from './undo-trash';

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -1,5 +1,4 @@
export * from './edit';
export * from './more';
export * from './actions';
export * from './name';
export * from './status';
export * from './subscribers';

View File

@ -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>
)}
/>
);
}

View File

@ -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} />;
}

View File

@ -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')}

View File

@ -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,
},
]}
/>
);
}

View File

@ -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>
),
};
};

View File

@ -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),
},
};
};

View File

@ -0,0 +1,4 @@
export * from './delete';
export * from './duplicate';
export * from './restore';
export * from './trash';

View File

@ -0,0 +1,9 @@
import { DropdownMenu } from '@wordpress/components';
import Control = DropdownMenu.Control;
export type Item = {
key: string;
control: Control;
slot?: JSX.Element;
};

View File

@ -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),
},
};
};

View File

@ -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>
),
};
};

View File

@ -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>
))}
</>
);
}

View File

@ -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} />,
},
];
}

View File

@ -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>
);
}

View 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;
}

View File

@ -0,0 +1 @@
export const storeName = 'mailpoet/automation-listing';

View File

@ -0,0 +1,3 @@
export * from './constants';
export * from './store';
export * from './types';

View File

@ -0,0 +1,5 @@
import { State } from './types';
export const getInitialState = (): State => ({
workflows: undefined,
});

View 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;
}
}

View 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;
}

View 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 };

View File

@ -0,0 +1,5 @@
import { Workflow } from '../workflow';
export type State = {
workflows?: Workflow[];
};

View File

@ -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 = {

View File

@ -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
};

View File

@ -26,4 +26,5 @@ export type Field = {
getLabel?: (segment: Segment) => string;
getCount?: (segment: Segment) => string;
transformChangedValue?: (arg: unknown) => Segment;
onWrapperClick?: () => void;
};

View File

@ -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>
);

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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,
};

View File

@ -57,6 +57,7 @@ function WooCommerceController({ isWizardStep = false }) {
loading={loading}
submitForm={submit}
isWizardStep={isWizardStep}
showCustomersImportSetting={window.mailpoet_show_customers_import}
/>
</WelcomeWizardStepLayout>
);

View File

@ -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(),
]);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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',

View File

@ -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,

View File

@ -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();

View File

@ -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);

View File

@ -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();
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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',

View File

@ -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;

View File

@ -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.$/'

View File

@ -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');
}

View File

@ -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');
}
}

View File

@ -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) {

View File

@ -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 dont 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"]';

View File

@ -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();

View File

@ -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);

View File

@ -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 %>

View File

@ -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:'),

View File

@ -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! Lets get you started on the right foot.'),
'welcomeWizardSenderText': __('Who is the sender of the emails youll be creating with MailPoet?'),
'welcomeWizardUsageTrackingStepTitle': __('Help MailPoet improve with anonymous usage tracking.'),
'welcomeWizardUsageTrackingStepSubTitle': __('Data we dont 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'),

View File

@ -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>

View File

@ -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'),
}) %>
}) %>