Compare commits

..

92 Commits

Author SHA1 Message Date
f0b779d724 Release 4.0.0 2022-11-15 15:06:53 +01:00
dee5ff38f5 Fix error when sending a preview email with MSS
With HTTPS and "$oneClickUnsubscribeUrl = false", we were sending "false" instead of an actual URL.

[MAILPOET-4813]
2022-11-15 15:05:03 +01:00
70aefb421b Clear error when rerunning migration
[MAILPOET-4811]
2022-11-14 20:12:35 +03:00
764e1a1bf0 Rename "Not active" state to "Draft" on automations listing page
[MAILPOET-4810]
2022-11-14 19:51:34 +03:00
b140011f92 Update minimal version check for WooCommerceCheckoutBlockCest
Since we support the Woo Blocks integration only for versions from 8.0.0
we need to update minimal version check code for acceptance test.
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
b48c9e437e Update plugins in tests for feature and trunk branches
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
f34efb4d6f Update acceptance test oldest job configuration
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
324c03e8b9 Print WordPress version when starting test environment
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
e718101425 Upgrade minimum required WordPress version to 5.8
[MAILPOET-4668]
2022-11-14 17:20:43 +01:00
080ce50fea Open automations Beacon links in a new tab instead of HelpScout Beacon
[MAILPOET-4809]
2022-11-14 18:53:20 +03:00
1b9eb223b0 Load templates late
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
ea2fa794ac Limit varchar length to 191 to avoid unique/primary key issues
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
c46c61a923 Simplify and optimize automation stats
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
1136431551 Use $wpdb->prepare() for automation stats
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
957be23212 Improve query formatting
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
91a88b3e91 Rename alias "w" to "a"
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
7c5d239267 Fix and unify cleanup in automation tests
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
c748b80447 Fix timestamp column problems with some MySQL versions and some SQL modes
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
725e0ecb00 Remove automation feature flag
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
eb71dd8a68 Remove automation testing buttons and hooks
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
ce1687cf97 Replace custom mutation hook with a @wordpress/api-fetch based one
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
2e328b6d7f Move temporary automation migrator to a new migration
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
8489e63d34 Delete automation testing system endpoints
[MAILPOET-4788]
2022-11-14 12:35:29 +02:00
f8c17730fc Update tracking campaign
[MAILPOET-4712]
2022-11-14 11:22:57 +01:00
93e43eee7a Update sales banner for Black Friday 2022
[MAILPOET-4712]
2022-11-14 11:22:57 +01:00
9063dc3079 Remove duplicities in naming
[MAILPOET-4793]
2022-11-14 12:14:35 +02:00
9d55d3f134 Use "automation" instead of "workflow"
[MAILPOET-4793]
2022-11-14 12:14:35 +02:00
d199c3768a Show Bounced and Unsubscribed badge for minimum of 100 newsletter sent
The reason is that at low volumes the percentages can be misleading. E.g. if you send 20 emails and 1 hard bounces - that’s a hard bounce rate of 5%.

The percentage is very high, but a single hard bounce doesn’t really mean anything, so to avoid confusion I’d hide the badge altogether for low volumes.

MAILPOET-4688
2022-11-14 10:47:04 +01:00
0e0c2447d9 Fix issue with badge stats
We were using MailPoet.Num.toLocaleFixed which returns a string locale of the number. This can cause issues with some locales e.g some locale returns 2,3 instead of 2.3

MAILPOET-4688
2022-11-14 10:47:04 +01:00
99dfb3d24b Add formatForStats method for Unsubscribed and Bounced Stats
MAILPOET-4688
2022-11-14 10:47:04 +01:00
c4366e009b Fix crashing page due to newsletter not found error
MAILPOET-4688
2022-11-14 10:47:04 +01:00
1d31202607 Add badge for Unsubscribe, bounced and opened stat to the Advanced email statistics page
MAILPOET-4688
2022-11-14 10:47:04 +01:00
78d25ffb69 Add Stats Badge for opened rate on email listing
MAILPOET-4688
2022-11-14 10:47:04 +01:00
b710682c66 Replace average with critical badge on email listing for opened and clicked stats
MAILPOET-4688
2022-11-14 10:47:04 +01:00
52c6b94315 Add support for critical tag in newsletter badge
MAILPOET-4688
2022-11-14 10:47:04 +01:00
fd6b49e598 Add type TagVariant for variant strings
MAILPOET-4688
2022-11-14 10:47:04 +01:00
df2982454e Add support for Critical Tag and critical tag color
MAILPOET-4688
2022-11-14 10:47:04 +01:00
8a19fd906f Update test to check for distinct one-click urls in bulk
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
99198e5c2d Fix MailPoet\WP\Functions newly added method name
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
9204f37560 Remove redundant injection
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
765aa6efab Augment tests to acknowledge oneclick unsubscribe url
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
50b613365f Make sure one-click unsubscribe url won't redirect internally
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
4fe8d10d6c Update tests for Subscription and pass the new argument
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
162dab790d Implement 1-click unsubscribe strategy for post requests
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
6e8c9731d8 Define new isSiteUsingHttps method for WP_Functions
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
235552f91d Define Request utility class with isPost method
[MAILPOET-4703]
2022-11-11 09:21:30 +01:00
4ee08c296b Fix unknown storage engine 'InnoDB' error
[MAILPOET-4802]
2022-11-10 12:59:24 +01:00
bda979ec4f Fix trash error when workflow status is draft
[MAILPOET-4792]
2022-11-09 19:58:44 +03:00
78f10f064e Fix clicking on no longer existing text
[MAILPOET-4796]
2022-11-09 15:58:36 +01:00
eca4a68fec Revert broken test fix
Reverts: https://github.com/mailpoet/mailpoet/pull/4511

[MAILPOET-4796]
2022-11-09 15:58:36 +01:00
9daa5d58c7 Adjust tests to new templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
6cc232fbad Make premium badge mailpoet orange
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
6a07bd44ff Improve 'Coming soon' styles
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
63cd326191 Update template descriptions
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
40b15b0eb1 Lower the grid size for the badge
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
3825e9cb11 Fix test
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
7db2942140 Show PremiumModal for premium templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
3a5d28f24a Export const premiumValidAndActive
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
d02c63844d Add badges to template items
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
c3045dba07 Add type to WorkflowTemplate definition
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
c239566e1e Add premium and coming-soon template stubs
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
dfdc8cfd09 Add WorkflowTemplate types
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
4977b7ffa2 Add free templates
[MAILPOET-4745]
2022-11-09 15:24:50 +03:00
8e69299a6a Adjust naming and use wp wrapper I/O of direct usage
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
903bcbb92e Update tests and adapt to the new unsubscribe key structure
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
8d6492ac8c Adjust constructor injections of instantiators of MailPoet
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
c39ae1fe1f Adjust the unsubscribe key to use the new structure
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
e3539b06a2 Define URL utility method to check if url is using https
[MAILPOET-4702]
2022-11-09 12:36:44 +01:00
d34a265ac2 Prevent leaving unsaved automation
[MAILPOET-4776]
2022-11-09 13:42:56 +03:00
9a72e361b1 Fix plugin activtion for the smtp plugin
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
a520e4bd93 Run full activation of user account on multisite
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
9ab6ebbe0d Use always SMTP to send wp_mail
[MAILPOET-4785]
2022-11-09 12:59:29 +03:00
6a3cfd05e7 Update CircleCI to wait for Woo jobs to complete before starting build_release_zip
In [MAILPOET-4572] we added new CircleCI jobs to run WooCommerce tests in
different scenarios. But we forgot to add those jobs to the list of jobs
that need to finish before starting the build_release_zip job.

This can be problematic as we don’t want the build_release_zip to start
before all the test jobs have finished and, even worst, we don’t want it
to run if one of the test jobs failed.

This commit adds the following jobs to the list of jobs that are
required to finish before running build_release_zip:

- integration_test_woo_cot_no_sync
- integration_test_woo_cot_off
- integration_test_woo_cot_sync
- acceptance_tests_woo_cot_sync
- acceptance_tests_woo_cot_off
- acceptance_tests_woo_cot_no_sync

[MAILPOET-4791]
2022-11-09 09:07:21 +01:00
f825e535e3 Save next step in trigger handler
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
a2c5420d7f Save updated at timestamp in workflow runs
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
c6d3573652 Save next step ID to workflow runs
[MAILPOET-4787]
2022-11-08 22:55:57 +02:00
e41bcd0d02 Manage ActivationPanel in store and close when sidebar opens
[MAILPOET-4769]
2022-11-08 23:31:36 +03:00
f805907954 Release 3.103.1 2022-11-08 17:35:04 +01:00
d970efc0da Reduce size of a database field to avoid error in some MySQL versions
This commit reduces the size of the `name` field from varchar(255) to
varchar(191) to avoid the following fatal error in some MySQL versions:

```
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes
```

See the ticket descriptions and the links in it for more information.

[MAILPOET-4790]
2022-11-08 12:27:01 -03:00
c0ce5944dc Use short table aliases
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
467f354eb1 Do not skip workflow trigger queries when trigger keys are empty
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
3f016b45f9 Don't escape table names when not needed
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
679f74e498 Unify query error checking
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
08b314e0b4 Unify table variable naming
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
cc5959805b Use "workflow_triggers" table instead of an inline JSON
[MAILPOET-4778]
2022-11-08 16:17:34 +02:00
407f3d1609 Hide duplicate workflow button
[MAILPOET-4786]
2022-11-08 16:48:35 +03:00
503df3584c Track errors
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
fa9510f0c1 Add getStepById method
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
cc92df4e7f Track Automations > Listing viewed event
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
d76c5d32f2 Track Workflow deactivated
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
c6198cba4c Track Workflow activated
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
e29dd4286e Track template selected
[MAILPOET-4756]
2022-11-08 16:41:47 +03:00
261 changed files with 4630 additions and 4422 deletions

View File

@ -179,10 +179,10 @@ jobs:
- run: - run:
name: Download additional WP Plugins for tests name: Download additional WP Plugins for tests
command: | command: |
./do download:woo-commerce-zip 6.8.2 ./do download:woo-commerce-zip 7.0.1
./do download:woo-commerce-subscriptions-zip 4.5.1 ./do download:woo-commerce-subscriptions-zip 4.6.0
./do download:woo-commerce-memberships-zip 1.23.0 ./do download:woo-commerce-memberships-zip 1.23.1
./do download:woo-commerce-blocks-zip 8.4.0 ./do download:woo-commerce-blocks-zip 8.8.2
- run: - run:
name: Dump tests ENV variables for acceptance tests name: Dump tests ENV variables for acceptance tests
command: | command: |
@ -749,6 +749,12 @@ workflows:
- js_tests - js_tests
- integration_test_woocommerce - integration_test_woocommerce
- integration_test_base - integration_test_base
- integration_test_woo_cot_no_sync
- integration_test_woo_cot_off
- integration_test_woo_cot_sync
- acceptance_tests_woo_cot_sync
- acceptance_tests_woo_cot_off
- acceptance_tests_woo_cot_no_sync
nightly: nightly:
triggers: triggers:
@ -773,14 +779,14 @@ workflows:
- acceptance_tests: - acceptance_tests:
<<: *slack-fail-post-step <<: *slack-fail-post-step
name: acceptance_oldest name: acceptance_oldest
woo_core_version: 6.2.2 woo_core_version: 6.8.0
woo_subscriptions_version: 4.3.0 woo_subscriptions_version: 4.3.0
woo_memberships_version: 1.21.0 woo_memberships_version: 1.21.0
woo_blocks_version: 5.3.2 woo_blocks_version: 6.8.0
mysql_command: --max_allowed_packet=100M mysql_command: --max_allowed_packet=100M
mysql_image_version: 5.7.36 mysql_image_version: 5.7.36
codeception_image_version: 7.4-cli_20210126.1 codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: wp-5.6_php7.2_20220406.1 wordpress_image_version: wp-5.8_php7.3_20221104.1
requires: requires:
- build - build
- unit_tests: - unit_tests:

View File

@ -1,4 +1,4 @@
.mailpoet-automation-workflow-add-trigger { .mailpoet-automation-add-trigger {
align-items: center; align-items: center;
border: 1px dashed #c3c4c7; border: 1px dashed #c3c4c7;
border-radius: 4px; border-radius: 4px;

View File

@ -1,14 +1,14 @@
.mailpoet-automation-editor-workflow { .mailpoet-automation-editor-automation {
background: #fbfbfb; background: #fbfbfb;
flex-grow: 1; flex-grow: 1;
} }
.mailpoet-automation-editor-workflow-wrapper { .mailpoet-automation-editor-automation-wrapper {
display: grid; display: grid;
padding: 50px 20px; padding: 50px 20px;
} }
.mailpoet-automation-editor-workflow-end { .mailpoet-automation-editor-automation-end {
background: #8c8f94; background: #8c8f94;
border-radius: 999999px; border-radius: 999999px;
fill: white; fill: white;

View File

@ -1,4 +1,4 @@
.mailpoet-automation-editor-empty-workflow { .mailpoet-automation-editor-empty-automation {
align-items: center; align-items: center;
display: grid; display: grid;
height: 100%; height: 100%;

View File

@ -119,6 +119,10 @@
color: $color-stats-average; color: $color-stats-average;
} }
.mailpoet-statistics-value-number-critical {
color: $color-stats-critical;
}
.mailpoet-statistics-value-number-excellent { .mailpoet-statistics-value-number-excellent {
color: $color-stats-excellent; color: $color-stats-excellent;
} }

View File

@ -41,6 +41,15 @@
} }
} }
.mailpoet-tag-critical {
border-color: $color-stats-critical;
color: $color-stats-critical;
&.mailpoet-tag-inverted {
background: $color-stats-critical;
}
}
.mailpoet-tag-good { .mailpoet-tag-good {
border-color: $color-stats-good; border-color: $color-stats-good;
color: $color-stats-good; color: $color-stats-good;

View File

@ -11,17 +11,17 @@
@import './components-automation-editor/add-step-button'; @import './components-automation-editor/add-step-button';
@import './components-automation-editor/add-trigger'; @import './components-automation-editor/add-trigger';
@import './components-automation-editor/automation';
@import './components-automation-editor/block-icon'; @import './components-automation-editor/block-icon';
@import './components-automation-editor/chip'; @import './components-automation-editor/chip';
@import './components-automation-editor/dropdown'; @import './components-automation-editor/dropdown';
@import './components-automation-editor/empty-workflow'; @import './components-automation-editor/empty-automation';
@import './components-automation-editor/errors'; @import './components-automation-editor/errors';
@import './components-automation-editor/panel'; @import './components-automation-editor/panel';
@import './components-automation-editor/separator'; @import './components-automation-editor/separator';
@import './components-automation-editor/status'; @import './components-automation-editor/status';
@import './components-automation-editor/step'; @import './components-automation-editor/step';
@import './components-automation-editor/step-card'; @import './components-automation-editor/step-card';
@import './components-automation-editor/workflow';
@import './components-automation-editor/notices'; @import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal'; @import './components-automation-editor/deactivate-modal';

View File

@ -17,17 +17,30 @@ ul.mailpoet-automation-templates {
.mailpoet-automation-template-list-item { .mailpoet-automation-template-list-item {
button.components-button { button.components-button {
align-content: baseline;
align-items: flex-start;
background: #fff; background: #fff;
border: 1px solid #dcdcde; border: 1px solid #dcdcde;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
display: flex; display: grid;
flex-direction: column; grid-template-rows: 40px auto auto;
height: 100%; height: 100%;
padding: 24px 24px 26px; padding: 24px 24px 26px;
text-align: left; text-align: left;
width: 100%; width: 100%;
&:disabled,
&[aria-disabled='true'] {
color: #787c82;
cursor: not-allowed;
opacity: 1;
h2 {
color: #787c82;
}
}
&:hover { &:hover {
background: #fff; background: #fff;
border: 1px solid #dcdcde; border: 1px solid #dcdcde;
@ -49,7 +62,6 @@ ul.mailpoet-automation-templates {
h2 { h2 {
background: transparent; background: transparent;
border: none; border: none;
color: #2271b1;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 21px; line-height: 21px;
@ -78,4 +90,27 @@ ul.mailpoet-automation-templates {
fill: #dcdcde; fill: #dcdcde;
} }
} }
.badge {
text-align: right;
transform: translateX(24px);
span {
padding: 3px 8px;
}
}
}
.mailpoet-automation-template-list-item-coming-soon {
.badge span {
background: #ffe9cc;
color: #1d2327;
}
}
.mailpoet-automation-template-list-item-premium {
.badge span {
background: #ff5301;
color: #fff;
}
} }

View File

@ -69,6 +69,7 @@ $color-badge-video-guide: #46b450;
$color-stats-average: #f559c3; $color-stats-average: #f559c3;
$color-stats-good: #ff9f00; $color-stats-good: #ff9f00;
$color-stats-excellent: #7ed321; $color-stats-excellent: #7ed321;
$color-stats-critical: #f00;
$color-stats-unknown: $color-primary-inactive; $color-stats-unknown: $color-primary-inactive;
// Automation editor // Automation editor

View File

@ -1,81 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../config';
const API_URL = `${api.root}/mailpoet/v1/automation`;
export const request = (
path: string,
init?: RequestInit,
): ReturnType<typeof fetch> => fetch(`${API_URL}/${path}`, init);
type Error<T> = {
response?: Response;
data?: T;
};
type State<T> = {
data?: T;
loading: boolean;
error?: Error<T>;
};
type Result<T> = [(init?: RequestInit) => Promise<void>, State<T>];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Data = Record<string, any>;
export const useMutation = <T extends Data>(
path: string,
config?: RequestInit,
): Result<T> => {
const [state, setState] = useState<State<T>>({
data: undefined,
loading: false,
error: undefined,
});
const mutation = useCallback(
async (init?: RequestInit) => {
setState((prevState) => ({ ...prevState, loading: true }));
const response = await request(path, {
...config,
...init,
headers: {
'content-type': 'application/json',
...(init?.headers ?? {}),
'x-wp-nonce': api.nonce,
},
});
try {
const data = await response.json();
const error = response.ok ? null : { ...response, data };
setState((prevState) => ({ ...prevState, data, error }));
} catch (_) {
const error = { response };
setState((prevState) => ({ ...prevState, error }));
} finally {
setState((prevState) => ({ ...prevState, loading: false }));
}
},
[config, path],
);
return [mutation, state];
};
export const useQuery = <T extends Data>(
path: string,
init?: RequestInit,
): State<T> => {
const [mutation, result] = useMutation<T>(path, init);
useEffect(
() => {
void mutation();
},
[] /* eslint-disable-line react-hooks/exhaustive-deps -- request only on initial load */,
);
return result;
};

View File

@ -1,9 +1,7 @@
import apiFetch from '@wordpress/api-fetch'; import apiFetch from '@wordpress/api-fetch';
import { api } from '../config'; import { api } from '../config';
export * from './hooks'; const apiUrl = `${api.root}/mailpoet/v1/`;
const apiUrl = `${api.root}/mailpoet/v1/automation/`;
export type ApiError = { export type ApiError = {
code?: string; code?: string;

View File

@ -1,24 +1,33 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { TopBarWithBeamer } from 'common/top_bar/top_bar'; import { TopBarWithBeamer } from 'common/top_bar/top_bar';
import { Popover, SlotFillProvider } from '@wordpress/components'; import { Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { initializeApi, useMutation } from './api'; import { initializeApi } from './api';
import { registerTranslations } from './i18n'; import { registerTranslations } from './i18n';
import { createStore, storeName } from './listing/store'; import { createStore, storeName } from './listing/store';
import { AutomationListing, AutomationListingHeader } from './listing'; import { AutomationListing, AutomationListingHeader } from './listing';
import { registerApiErrorHandler } from './listing/api-error-handler'; import { registerApiErrorHandler } from './listing/api-error-handler';
import { Notices } from './listing/components/notices'; import { Notices } from './listing/components/notices';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { BuildYourOwnSection, HeroSection, TemplatesSection } from './sections'; import { BuildYourOwnSection, HeroSection, TemplatesSection } from './sections';
import { import { MailPoet } from '../mailpoet';
CreateEmptyWorkflowButton,
CreateWorkflowFromTemplateButton, const trackOpenEvent = () => {
} from './testing'; MailPoet.trackEvent('Automations > Listing viewed');
};
function Content(): JSX.Element { function Content(): JSX.Element {
const count = useSelect((select) => select(storeName).getWorkflowCount()); const [isBooting, setIsBooting] = useState(true);
const count = useSelect((select) => select(storeName).getAutomationCount());
useEffect(() => {
if (!isBooting || count === 0) {
return;
}
trackOpenEvent();
setIsBooting(false);
}, [isBooting, count]);
const content = const content =
count > 0 ? ( count > 0 ? (
<> <>
@ -49,7 +58,7 @@ function Content(): JSX.Element {
); );
} }
function Workflows(): JSX.Element { function Automations(): JSX.Element {
return ( return (
<> <>
<TopBarWithBeamer /> <TopBarWithBeamer />
@ -59,79 +68,12 @@ function Workflows(): JSX.Element {
); );
} }
function RecreateSchemaButton(): JSX.Element {
const [createSchema, { loading, error }] = useMutation('system/database', {
method: 'POST',
});
return (
<div>
<WorkflowListingNotices />
<button
className="button button-link-delete"
type="button"
onClick={() => createSchema()}
disabled={loading}
>
Recreate DB schema (data will be lost)
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
);
}
function DeleteSchemaButton(): JSX.Element {
const [deleteSchema, { loading, error }] = useMutation('system/database', {
method: 'DELETE',
});
return (
<div>
<button
className="button button-link-delete"
type="button"
onClick={async () => {
await deleteSchema();
window.location.href =
'/wp-admin/admin.php?page=mailpoet-experimental';
}}
disabled={loading}
>
Delete DB schema & deactivate feature
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
);
}
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<SlotFillProvider> <SlotFillProvider>
<BrowserRouter> <BrowserRouter>
<div> <Automations />
<Workflows /> <Popover.Slot />
<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>
</BrowserRouter> </BrowserRouter>
</SlotFillProvider> </SlotFillProvider>
); );

View File

@ -4,9 +4,9 @@ declare global {
root: string; root: string;
nonce: string; nonce: string;
}; };
mailpoet_workflow_count: number; mailpoet_automation_count: number;
} }
} }
export const api = window.mailpoet_automation_api; export const api = window.mailpoet_automation_api;
export const workflowCount = window.mailpoet_workflow_count; export const automationCount = window.mailpoet_automation_count;

View File

@ -19,7 +19,7 @@ export const registerApiErrorHandler = (): void =>
const status = errorObject.data?.status; const status = errorObject.data?.status;
const code = errorObject.code; const code = errorObject.code;
if (code === 'mailpoet_automation_workflow_not_valid') { if (code === 'mailpoet_automation_not_valid') {
dispatch(storeName).setErrors({ steps: errorObject.data.errors }); dispatch(storeName).setErrors({ steps: errorObject.data.errors });
return undefined; return undefined;
} }

View File

@ -9,9 +9,9 @@ import { storeName } from '../../store';
export function TrashButton(): JSX.Element { export function TrashButton(): JSX.Element {
const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { workflow } = useSelect( const { automation } = useSelect(
(select) => ({ (select) => ({
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
@ -33,7 +33,7 @@ export function TrashButton(): JSX.Element {
> >
{sprintf( {sprintf(
__('You are about to delete the automation "%s".', 'mailpoet'), __('You are about to delete the automation "%s".', 'mailpoet'),
workflow.name, automation.name,
)} )}
<br /> <br />
{__(' This will stop it for all subscribers immediately.', 'mailpoet')} {__(' This will stop it for all subscribers immediately.', 'mailpoet')}

View File

@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'; import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { Icon, plus } from '@wordpress/icons'; import { Icon, plus } from '@wordpress/icons';
import { WorkflowCompositeContext } from './context'; import { AutomationCompositeContext } from './context';
type Props = { type Props = {
onClick?: (element: HTMLButtonElement) => void; onClick?: (element: HTMLButtonElement) => void;
@ -9,7 +9,7 @@ type Props = {
}; };
export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element { export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(AutomationCompositeContext);
return ( return (
<CompositeItem <CompositeItem
state={compositeState} state={compositeState}

View File

@ -3,7 +3,7 @@ import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'
import { Icon, plus } from '@wordpress/icons'; import { Icon, plus } from '@wordpress/icons';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { WorkflowCompositeContext } from './context'; import { AutomationCompositeContext } from './context';
import { Step } from './types'; import { Step } from './types';
import { storeName } from '../../store'; import { storeName } from '../../store';
@ -12,14 +12,14 @@ type Props = {
}; };
export function AddTrigger({ step }: Props): JSX.Element { export function AddTrigger({ step }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(AutomationCompositeContext);
const { setInserterPopover } = useDispatch(storeName); const { setInserterPopover } = useDispatch(storeName);
return ( return (
<CompositeItem <CompositeItem
state={compositeState} state={compositeState}
role="treeitem" role="treeitem"
className="mailpoet-automation-workflow-add-trigger" className="mailpoet-automation-add-trigger"
data-previous-step-id={step.id} data-previous-step-id={step.id}
focusable focusable
onClick={(event) => { onClick={(event) => {

View File

@ -1,5 +1,5 @@
import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components'; import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components';
import { createContext } from '@wordpress/element'; import { createContext } from '@wordpress/element';
export const WorkflowCompositeContext = export const AutomationCompositeContext =
createContext<ReturnType<typeof useCompositeState>>(undefined); createContext<ReturnType<typeof useCompositeState>>(undefined);

View File

@ -0,0 +1,9 @@
import { __ } from '@wordpress/i18n';
export function EmptyAutomation(): JSX.Element {
return (
<div className="mailpoet-automation-editor-empty-automation">
{__('No automation data.', 'mailpoet')}
</div>
);
}

View File

@ -7,8 +7,8 @@ import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Icon, check } from '@wordpress/icons'; import { Icon, check } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks'; import { Hooks } from 'wp-js-hooks';
import { WorkflowCompositeContext } from './context'; import { AutomationCompositeContext } from './context';
import { EmptyWorkflow } from './empty-workflow'; import { EmptyAutomation } from './empty-automation';
import { Separator } from './separator'; import { Separator } from './separator';
import { Step } from './step'; import { Step } from './step';
import { Step as StepData } from './types'; import { Step as StepData } from './types';
@ -21,10 +21,10 @@ import {
RenderStepType, RenderStepType,
} from '../../../types/filters'; } from '../../../types/filters';
export function Workflow(): JSX.Element { export function Automation(): JSX.Element {
const { workflowData, selectedStep } = useSelect( const { automationData, selectedStep } = useSelect(
(select) => ({ (select) => ({
workflowData: select(storeName).getWorkflowData(), automationData: select(storeName).getAutomationData(),
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(storeName).getSelectedStep(),
}), }),
[], [],
@ -36,9 +36,9 @@ export function Workflow(): JSX.Element {
shift: true, shift: true,
}); });
const stepMap = workflowData?.steps ?? undefined; const stepMap = automationData?.steps ?? undefined;
// serialize steps (for now, we support only one trigger and linear workflows) // serialize steps (for now, we support only one trigger and linear automations)
const steps = useMemo(() => { const steps = useMemo(() => {
const stepArray = [stepMap.root]; const stepArray = [stepMap.root];
@ -56,7 +56,7 @@ export function Workflow(): JSX.Element {
const renderStep = useMemo( const renderStep = useMemo(
(): RenderStepType => (): RenderStepType =>
Hooks.applyFilters( Hooks.applyFilters(
'mailpoet.automation.workflow.render_step', 'mailpoet.automation.render_step',
(stepData: StepData) => (stepData: StepData) =>
stepData.type === 'root' ? ( stepData.type === 'root' ? (
<AddTrigger step={stepData} /> <AddTrigger step={stepData} />
@ -73,7 +73,7 @@ export function Workflow(): JSX.Element {
const renderSeparator = useMemo( const renderSeparator = useMemo(
(): RenderStepSeparatorType => (): RenderStepSeparatorType =>
Hooks.applyFilters( Hooks.applyFilters(
'mailpoet.automation.workflow.render_step_separator', 'mailpoet.automation.render_step_separator',
(previousStepData: StepData) => ( (previousStepData: StepData) => (
<Separator previousStepId={previousStepData.id} /> <Separator previousStepId={previousStepData.id} />
), ),
@ -81,20 +81,20 @@ export function Workflow(): JSX.Element {
[], [],
); );
if (!workflowData) { if (!automationData) {
return <EmptyWorkflow />; return <EmptyAutomation />;
} }
return ( return (
<WorkflowCompositeContext.Provider value={compositeState}> <AutomationCompositeContext.Provider value={compositeState}>
<Composite <Composite
state={compositeState} state={compositeState}
role="tree" role="tree"
aria-label={__('Automation', 'mailpoet')} aria-label={__('Automation', 'mailpoet')}
aria-orientation="vertical" aria-orientation="vertical"
className="mailpoet-automation-editor-workflow" className="mailpoet-automation-editor-automation"
> >
<div className="mailpoet-automation-editor-workflow-wrapper"> <div className="mailpoet-automation-editor-automation-wrapper">
<Statistics /> <Statistics />
{stepMap.root.next_steps.length === 0 ? ( {stepMap.root.next_steps.length === 0 ? (
<> <>
@ -119,13 +119,13 @@ export function Workflow(): JSX.Element {
</Fragment> </Fragment>
))} ))}
<Icon <Icon
className="mailpoet-automation-editor-workflow-end" className="mailpoet-automation-editor-automation-end"
icon={check} icon={check}
/> />
<div /> <div />
</div> </div>
<InserterPopover /> <InserterPopover />
</Composite> </Composite>
</WorkflowCompositeContext.Provider> </AutomationCompositeContext.Provider>
); );
} }

View File

@ -4,9 +4,9 @@ import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics'; import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element { export function Statistics(): JSX.Element {
const { workflow } = useSelect( const { automation } = useSelect(
(select) => ({ (select) => ({
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
@ -19,19 +19,19 @@ export function Statistics(): JSX.Element {
key: 'entered', key: 'entered',
// translators: Total number of subscribers who entered an automation // translators: Total number of subscribers who entered an automation
label: _x('Total Entered', 'automation stats', 'mailpoet'), label: _x('Total Entered', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.entered, value: automation.stats.totals.entered,
}, },
{ {
key: 'processing', key: 'processing',
// translators: Total number of subscribers who are being processed in an automation // translators: Total number of subscribers who are being processed in an automation
label: _x('Total Processing', 'automation stats', 'mailpoet'), label: _x('Total Processing', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.in_progress, value: automation.stats.totals.in_progress,
}, },
{ {
key: 'exited', key: 'exited',
// translators: Total number of subscribers who exited an automation, no matter the result // translators: Total number of subscribers who exited an automation, no matter the result
label: _x('Total Exited', 'automation stats', 'mailpoet'), label: _x('Total Exited', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.exited, value: automation.stats.totals.exited,
}, },
]} ]}
/> />

View File

@ -23,7 +23,7 @@ export function StepMoreMenu({ step }: Props): JSX.Element {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const moreControls: StepMoreControlsType = Hooks.applyFilters( const moreControls: StepMoreControlsType = Hooks.applyFilters(
'mailpoet.automation.workflow.step.more-controls', 'mailpoet.automation.step.more-controls',
{ {
delete: { delete: {
key: 'delete', key: 'delete',

View File

@ -4,7 +4,7 @@ import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'
import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { blockMeta } from '@wordpress/icons'; import { blockMeta } from '@wordpress/icons';
import { __, _x } from '@wordpress/i18n'; import { __, _x } from '@wordpress/i18n';
import { WorkflowCompositeContext } from './context'; import { AutomationCompositeContext } from './context';
import { StepMoreMenu } from './step-more-menu'; import { StepMoreMenu } from './step-more-menu';
import { Step as StepData } from './types'; import { Step as StepData } from './types';
import { Chip } from '../chip'; import { Chip } from '../chip';
@ -48,7 +48,7 @@ export function Step({ step, isSelected }: Props): JSX.Element {
[step], [step],
); );
const { openSidebar, selectStep } = useDispatch(storeName); const { openSidebar, selectStep } = useDispatch(storeName);
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(AutomationCompositeContext);
const { batch } = useRegistry(); const { batch } = useRegistry();
const compositeItemId = `step-${step.id}`; const compositeItemId = `step-${step.id}`;

View File

@ -1,4 +1,4 @@
import { WorkflowStatus } from '../../../listing/workflow'; import { AutomationStatus } from '../../../listing/automation';
export type NextStep = { export type NextStep = {
id: string; id: string;
@ -12,10 +12,10 @@ export type Step = {
next_steps: NextStep[]; next_steps: NextStep[];
}; };
export type Workflow = { export type Automation = {
id: number; id: number;
name: string; name: string;
status: WorkflowStatus; status: AutomationStatus;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
activated_at: string; activated_at: string;

View File

@ -10,7 +10,7 @@ import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { chevronDown } from '@wordpress/icons'; import { chevronDown } from '@wordpress/icons';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { AutomationStatus } from '../../../listing/automation';
// See: https://github.com/WordPress/gutenberg/blob/eff0cab2b3181c004dbd15398e570ecec28a3726/packages/edit-site/src/components/header/document-actions/index.js // See: https://github.com/WordPress/gutenberg/blob/eff0cab2b3181c004dbd15398e570ecec28a3726/packages/edit-site/src/components/header/document-actions/index.js
@ -22,10 +22,10 @@ const Dropdown: ComponentType<
> = WpDropdown; > = WpDropdown;
export function DocumentActions({ children }): JSX.Element { export function DocumentActions({ children }): JSX.Element {
const { workflowName, workflowStatus, showIconLabels } = useSelect( const { automationName, automationStatus, showIconLabels } = useSelect(
(select) => ({ (select) => ({
workflowName: select(storeName).getWorkflowData().name, automationName: select(storeName).getAutomationData().name,
workflowStatus: select(storeName).getWorkflowData().status, automationStatus: select(storeName).getAutomationData().status,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
}), }),
[], [],
@ -36,9 +36,9 @@ export function DocumentActions({ children }): JSX.Element {
const titleRef = useRef(); const titleRef = useRef();
let chipClass = 'mailpoet-automation-editor-chip-gray'; let chipClass = 'mailpoet-automation-editor-chip-gray';
if (workflowStatus === WorkflowStatus.ACTIVE) { if (automationStatus === AutomationStatus.ACTIVE) {
chipClass = 'mailpoet-automation-editor-chip-success'; chipClass = 'mailpoet-automation-editor-chip-success';
} else if (workflowStatus === WorkflowStatus.DEACTIVATING) { } else if (automationStatus === AutomationStatus.DEACTIVATING) {
chipClass = 'mailpoet-automation-editor-chip-danger'; chipClass = 'mailpoet-automation-editor-chip-danger';
} }
@ -66,18 +66,18 @@ export function DocumentActions({ children }): JSX.Element {
<VisuallyHidden as="span"> <VisuallyHidden as="span">
{__('Editing automation:', 'mailpoet')} {__('Editing automation:', 'mailpoet')}
</VisuallyHidden> </VisuallyHidden>
{workflowName} {automationName}
</Text> </Text>
<Text <Text
size="body" size="body"
className={`edit-site-document-actions__secondary-item ${chipClass}`} className={`edit-site-document-actions__secondary-item ${chipClass}`}
> >
{workflowStatus === WorkflowStatus.ACTIVE && {automationStatus === AutomationStatus.ACTIVE &&
__('Active', 'mailpoet')} __('Active', 'mailpoet')}
{workflowStatus === WorkflowStatus.DEACTIVATING && {automationStatus === AutomationStatus.DEACTIVATING &&
__('Deactivating', 'mailpoet')} __('Deactivating', 'mailpoet')}
{workflowStatus === WorkflowStatus.DRAFT && {automationStatus === AutomationStatus.DRAFT &&
__('Draft', 'mailpoet')} __('Draft', 'mailpoet')}
</Text> </Text>
</a> </a>

View File

@ -35,17 +35,17 @@ type StepErrorProps = {
function StepError({ stepId }: StepErrorProps): JSX.Element { function StepError({ stepId }: StepErrorProps): JSX.Element {
const compositeState = useContext(ErrorsCompositeContext); const compositeState = useContext(ErrorsCompositeContext);
const { steps, workflowData } = useSelect( const { steps, automationData } = useSelect(
(select) => ({ (select) => ({
steps: select(storeName).getSteps(), steps: select(storeName).getSteps(),
workflowData: select(storeName).getWorkflowData(), automationData: select(storeName).getAutomationData(),
}), }),
[], [],
); );
const { openSidebar, selectStep } = useDispatch(storeName); const { openSidebar, selectStep } = useDispatch(storeName);
const stepData = workflowData.steps[stepId]; const stepData = automationData.steps[stepId];
const step = steps.find(({ key }) => key === stepData.key); const step = steps.find(({ key }) => key === stepData.key);
return ( return (
@ -78,10 +78,10 @@ export function Errors(): JSX.Element | null {
shift: true, shift: true,
}); });
const { errors, workflowData } = useSelect( const { errors, automationData } = useSelect(
(select) => ({ (select) => ({
errors: select(storeName).getErrors(), errors: select(storeName).getErrors(),
workflowData: select(storeName).getWorkflowData(), automationData: select(storeName).getAutomationData(),
}), }),
[], [],
); );
@ -93,18 +93,18 @@ export function Errors(): JSX.Element | null {
} }
const visited = new Map<string, StepErrorType | undefined>(); const visited = new Map<string, StepErrorType | undefined>();
const ids = workflowData.steps.root.next_steps.map(({ id }) => id); const ids = automationData.steps.root.next_steps.map(({ id }) => id);
while (ids.length > 0) { while (ids.length > 0) {
const id = ids.shift(); const id = ids.shift();
if (!visited.has(id)) { if (!visited.has(id)) {
visited.set(id, errors.steps[id]); visited.set(id, errors.steps[id]);
workflowData.steps[id]?.next_steps?.forEach((step) => automationData.steps[id]?.next_steps?.forEach((step) =>
ids.push(step.id), ids.push(step.id),
); );
} }
} }
return [...visited.values()].filter((error) => !!error); return [...visited.values()].filter((error) => !!error);
}, [errors, workflowData]); }, [errors, automationData]);
// automatically open the popover when errors appear // automatically open the popover when errors appear
const hasErrors = stepErrors.length > 0; const hasErrors = stepErrors.length > 0;

View File

@ -13,7 +13,7 @@ import { Errors } from './errors';
import { InserterToggle } from './inserter_toggle'; import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu'; import { MoreMenu } from './more_menu';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { AutomationStatus } from '../../../listing/automation';
import { import {
DeactivateImmediatelyModal, DeactivateImmediatelyModal,
DeactivateModal, DeactivateModal,
@ -23,22 +23,23 @@ import {
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js // 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 // https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton({ onClick, label }): JSX.Element { function ActivateButton({ label }): JSX.Element {
const { errors, isDeactivating } = useSelect( const { errors, isDeactivating } = useSelect(
(select) => ({ (select) => ({
errors: select(storeName).getErrors(), errors: select(storeName).getErrors(),
isDeactivating: isDeactivating:
select(storeName).getWorkflowData().status === select(storeName).getAutomationData().status ===
WorkflowStatus.DEACTIVATING, AutomationStatus.DEACTIVATING,
}), }),
[], [],
); );
const { openActivationPanel } = useDispatch(storeName);
const button = ( const button = (
<Button <Button
variant="primary" variant="primary"
className="editor-post-publish-button" className="editor-post-publish-button"
onClick={onClick} onClick={openActivationPanel}
disabled={isDeactivating || !!errors} disabled={isDeactivating || !!errors}
> >
{label} {label}
@ -53,7 +54,7 @@ function ActivateButton({ onClick, label }): JSX.Element {
// The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does. // The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does.
delay={0} delay={0}
text={__( text={__(
'Editing an active workflow is temporarily unavailable. We are working on introducing this functionality.', 'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet', 'mailpoet',
)} )}
> >
@ -68,14 +69,14 @@ function ActivateButton({ onClick, label }): JSX.Element {
function UpdateButton(): JSX.Element { function UpdateButton(): JSX.Element {
const { save } = useDispatch(storeName); const { save } = useDispatch(storeName);
const { workflow } = useSelect( const { automation } = useSelect(
(select) => ({ (select) => ({
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
if (workflow.stats.totals.in_progress === 0) { if (automation.stats.totals.in_progress === 0) {
return ( return (
<Button <Button
variant="primary" variant="primary"
@ -93,7 +94,7 @@ function UpdateButton(): JSX.Element {
// The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does. // The following error seems to be a mismatch. It claims the 'delay' prop does not exist, but it does.
delay={0} delay={0}
text={__( text={__(
'Editing an active workflow is temporarily unavailable. We are working on introducing this functionality.', 'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet', 'mailpoet',
)} )}
> >
@ -125,7 +126,7 @@ function DeactivateButton(): JSX.Element {
const { hasUsersInProgress } = useSelect( const { hasUsersInProgress } = useSelect(
(select) => ({ (select) => ({
hasUsersInProgress: hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0, select(storeName).getAutomationData().stats.totals.in_progress > 0,
}), }),
[], [],
); );
@ -165,7 +166,7 @@ function DeactivateNowButton(): JSX.Element {
const { hasUsersInProgress } = useSelect( const { hasUsersInProgress } = useSelect(
(select) => ({ (select) => ({
hasUsersInProgress: hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0, select(storeName).getAutomationData().stats.totals.in_progress > 0,
}), }),
[], [],
); );
@ -201,18 +202,14 @@ function DeactivateNowButton(): JSX.Element {
type Props = { type Props = {
showInserterToggle: boolean; showInserterToggle: boolean;
toggleActivatePanel: () => void;
}; };
export function Header({ export function Header({ showInserterToggle }: Props): JSX.Element {
showInserterToggle, const { setAutomationName } = useDispatch(storeName);
toggleActivatePanel, const { automationName, automationStatus } = useSelect(
}: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect(
(select) => ({ (select) => ({
workflowName: select(storeName).getWorkflowData().name, automationName: select(storeName).getAutomationData().name,
workflowStatus: select(storeName).getWorkflowData().status, automationStatus: select(storeName).getAutomationData().status,
}), }),
[], [],
); );
@ -237,8 +234,8 @@ export function Header({
{__('Automation name', 'mailpoet')} {__('Automation name', 'mailpoet')}
</div> </div>
<TextControl <TextControl
value={workflowName} value={automationName}
onChange={(newName) => setWorkflowName(newName)} onChange={(newName) => setAutomationName(newName)}
help={__( help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`, `Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet', 'mailpoet',
@ -252,28 +249,22 @@ export function Header({
<div className="edit-site-header_end"> <div className="edit-site-header_end">
<div className="edit-site-header__actions"> <div className="edit-site-header__actions">
<Errors /> <Errors />
{workflowStatus === WorkflowStatus.DRAFT && ( {automationStatus === AutomationStatus.DRAFT && (
<> <>
<SaveDraftButton /> <SaveDraftButton />
<ActivateButton <ActivateButton label={__('Activate', 'mailpoet')} />
onClick={toggleActivatePanel}
label={__('Activate', 'mailpoet')}
/>
</> </>
)} )}
{workflowStatus === WorkflowStatus.ACTIVE && ( {automationStatus === AutomationStatus.ACTIVE && (
<> <>
<DeactivateButton /> <DeactivateButton />
<UpdateButton /> <UpdateButton />
</> </>
)} )}
{workflowStatus === WorkflowStatus.DEACTIVATING && ( {automationStatus === AutomationStatus.DEACTIVATING && (
<> <>
<DeactivateNowButton /> <DeactivateNowButton />
<ActivateButton <ActivateButton label={__('Update & Activate', 'mailpoet')} />
onClick={toggleActivatePanel}
label={__('Update & Activate', 'mailpoet')}
/>
</> </>
)} )}
<PinnedItems.Slot scope={storeName} /> <PinnedItems.Slot scope={storeName} />

View File

@ -22,7 +22,7 @@ export function InserterPopover(): JSX.Element | null {
const onInsert = useCallback((item: Item) => { const onInsert = useCallback((item: Item) => {
const addStepCallback: AddStepCallbackType = Hooks.applyFilters( const addStepCallback: AddStepCallbackType = Hooks.applyFilters(
'mailpoet.automation.workflow.add_step_callback', 'mailpoet.automation.add_step_callback',
() => { () => {
setShowModal(true); setShowModal(true);
}, },

View File

@ -5,7 +5,7 @@ import {
store as keyboardShortcutsStore, store as keyboardShortcutsStore,
} from '@wordpress/keyboard-shortcuts'; } from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js
@ -55,7 +55,7 @@ export function KeyboardShortcuts(): null {
} else { } else {
const sidebarToOpen = selectedStep() const sidebarToOpen = selectedStep()
? stepSidebarKey ? stepSidebarKey
: workflowSidebarKey; : automationSidebarKey;
openSidebar(sidebarToOpen); openSidebar(sidebarToOpen);
} }
}); });

View File

@ -3,7 +3,7 @@ import { Button, Modal } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { AutomationStatus } from '../../../listing/automation';
type DeactivateImmediatelyModalProps = { type DeactivateImmediatelyModalProps = {
onClose: () => void; onClose: () => void;
@ -49,20 +49,20 @@ type DeactivateModalProps = {
export function DeactivateModal({ export function DeactivateModal({
onClose, onClose,
}: DeactivateModalProps): JSX.Element { }: DeactivateModalProps): JSX.Element {
const { workflowName } = useSelect( const { automationName } = useSelect(
(select) => ({ (select) => ({
workflowName: select(storeName).getWorkflowData().name, automationName: select(storeName).getAutomationData().name,
}), }),
[], [],
); );
const [selected, setSelected] = useState< const [selected, setSelected] = useState<
WorkflowStatus.DRAFT | WorkflowStatus.DEACTIVATING AutomationStatus.DRAFT | AutomationStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING); >(AutomationStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false); const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation. // translators: %s is the name of the automation.
const title = sprintf( const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'), __('Deactivate the "%s" automation?', 'mailpoet'),
workflowName, automationName,
); );
return ( return (
@ -79,7 +79,7 @@ export function DeactivateModal({
<li> <li>
<label <label
className={ className={
selected === WorkflowStatus.DEACTIVATING selected === AutomationStatus.DEACTIVATING
? 'mailpoet-automation-option active' ? 'mailpoet-automation-option active'
: 'mailpoet-automation-option' : 'mailpoet-automation-option'
} }
@ -89,8 +89,8 @@ export function DeactivateModal({
type="radio" type="radio"
disabled={isBusy} disabled={isBusy}
name="deactivation-method" name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING} checked={selected === AutomationStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)} onChange={() => setSelected(AutomationStatus.DEACTIVATING)}
/> />
</span> </span>
<span> <span>
@ -107,7 +107,7 @@ export function DeactivateModal({
<li> <li>
<label <label
className={ className={
selected === WorkflowStatus.DRAFT selected === AutomationStatus.DRAFT
? 'mailpoet-automation-option active' ? 'mailpoet-automation-option active'
: 'mailpoet-automation-option' : 'mailpoet-automation-option'
} }
@ -117,8 +117,8 @@ export function DeactivateModal({
type="radio" type="radio"
disabled={isBusy} disabled={isBusy}
name="deactivation-method" name="deactivation-method"
checked={selected === WorkflowStatus.DRAFT} checked={selected === AutomationStatus.DRAFT}
onChange={() => setSelected(WorkflowStatus.DRAFT)} onChange={() => setSelected(AutomationStatus.DRAFT)}
/> />
</span> </span>
<span> <span>
@ -140,7 +140,7 @@ export function DeactivateModal({
onClick={() => { onClick={() => {
setIsBusy(true); setIsBusy(true);
dispatch(storeName).deactivate( dispatch(storeName).deactivate(
selected !== WorkflowStatus.DEACTIVATING, selected !== AutomationStatus.DEACTIVATING,
); );
}} }}
> >

View File

@ -4,7 +4,7 @@ import { Button, Spinner } from '@wordpress/components';
import { closeSmall } from '@wordpress/icons'; import { closeSmall } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { AutomationStatus } from '../../../listing/automation';
import { MailPoet } from '../../../../mailpoet'; import { MailPoet } from '../../../../mailpoet';
function PreStep({ onClose }): JSX.Element { function PreStep({ onClose }): JSX.Element {
@ -58,9 +58,9 @@ function PreStep({ onClose }): JSX.Element {
} }
function PostStep({ onClose }): JSX.Element { function PostStep({ onClose }): JSX.Element {
const { workflow } = useSelect( const { automation } = useSelect(
(select) => ({ (select) => ({
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
@ -81,7 +81,7 @@ function PostStep({ onClose }): JSX.Element {
<div className="mailpoet-automation-activate-panel__body"> <div className="mailpoet-automation-activate-panel__body">
<div className="mailpoet-automation-activate-panel__section"> <div className="mailpoet-automation-activate-panel__section">
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)} {sprintf(__('"%s" is now live.', 'mailpoet'), automation.name)}
</div> </div>
<p> <p>
<strong>{__('Whats next?', 'mailpoet')}</strong> <strong>{__('Whats next?', 'mailpoet')}</strong>
@ -100,29 +100,31 @@ function PostStep({ onClose }): JSX.Element {
); );
} }
export function ActivatePanel({ onClose }): JSX.Element { export function ActivatePanel(): JSX.Element {
const { workflow, errors } = useSelect( const { automation, errors } = useSelect(
(select) => ({ (select) => ({
errors: select(storeName).getErrors(), errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
const { closeActivationPanel } = useDispatch(storeName);
useEffect(() => { useEffect(() => {
if (errors) { if (errors) {
onClose(); closeActivationPanel();
} }
}, [errors, onClose]); }, [errors, closeActivationPanel]);
if (errors) { if (errors) {
return null; return null;
} }
const isActive = workflow.status === WorkflowStatus.ACTIVE; const isActive = automation.status === AutomationStatus.ACTIVE;
return ( return (
<div className="mailpoet-automation-activate-panel"> <div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />} {isActive && <PostStep onClose={closeActivationPanel} />}
{!isActive && <PreStep onClose={onClose} />} {!isActive && <PreStep onClose={closeActivationPanel} />}
</div> </div>
); );
} }

View File

@ -4,10 +4,10 @@ import { __ } from '@wordpress/i18n';
import { storeName } from '../../../store'; import { storeName } from '../../../store';
import { TrashButton } from '../../actions/trash-button'; import { TrashButton } from '../../actions/trash-button';
export function WorkflowSidebar(): JSX.Element { export function AutomationSidebar(): JSX.Element {
const { workflowData } = useSelect( const { automationData } = useSelect(
(select) => ({ (select) => ({
workflowData: select(storeName).getWorkflowData(), automationData: select(storeName).getAutomationData(),
}), }),
[], [],
); );
@ -22,30 +22,30 @@ export function WorkflowSidebar(): JSX.Element {
<PanelBody title={__('Automation details', 'mailpoet')} initialOpen> <PanelBody title={__('Automation details', 'mailpoet')} initialOpen>
<PanelRow> <PanelRow>
<strong>Date added</strong>{' '} <strong>Date added</strong>{' '}
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString( {new Date(Date.parse(automationData.created_at)).toLocaleDateString(
undefined, undefined,
dateOptions, dateOptions,
)} )}
</PanelRow> </PanelRow>
<PanelRow> <PanelRow>
<strong>Activated</strong>{' '} <strong>Activated</strong>{' '}
{workflowData.status === 'active' && {automationData.status === 'active' &&
new Date(Date.parse(workflowData.updated_at)).toLocaleDateString( new Date(Date.parse(automationData.updated_at)).toLocaleDateString(
undefined, undefined,
dateOptions, dateOptions,
)} )}
{workflowData.status !== 'active' && {automationData.status !== 'active' &&
workflowData.activated_at && automationData.activated_at &&
new Date(Date.parse(workflowData.activated_at)).toLocaleDateString( new Date(Date.parse(automationData.activated_at)).toLocaleDateString(
undefined, undefined,
dateOptions, dateOptions,
)} )}
{workflowData.status !== 'active' && !workflowData.activated_at && ( {automationData.status !== 'active' && !automationData.activated_at && (
<span className="mailpoet-deactive">Not activated yet.</span> <span className="mailpoet-deactive">Not activated yet.</span>
)} )}
</PanelRow> </PanelRow>
<PanelRow> <PanelRow>
<strong>Author</strong> {workflowData.author.name} <strong>Author</strong> {automationData.author.name}
</PanelRow> </PanelRow>
<PanelRow> <PanelRow>
<TrashButton /> <TrashButton />

View File

@ -1,7 +1,7 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/sidebar/settings-header/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/sidebar/settings-header/index.js
@ -13,11 +13,11 @@ type Props = {
export function Header({ sidebarKey }: Props): JSX.Element { export function Header({ sidebarKey }: Props): JSX.Element {
const { openSidebar } = useDispatch(storeName); const { openSidebar } = useDispatch(storeName);
const openWorkflowSettings = () => openSidebar(workflowSidebarKey); const openAutomationSettings = () => openSidebar(automationSidebarKey);
const openStepSettings = () => openSidebar(stepSidebarKey); const openStepSettings = () => openSidebar(stepSidebarKey);
const [workflowAriaLabel, workflowActiveClass] = const [automationAriaLabel, automationActiveClass] =
sidebarKey === workflowSidebarKey sidebarKey === automationSidebarKey
? [__('Automation (selected)', 'mailpoet'), 'is-active'] ? [__('Automation (selected)', 'mailpoet'), 'is-active']
: [__('Automation', 'mailpoet'), '']; : [__('Automation', 'mailpoet'), ''];
@ -30,9 +30,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
<ul> <ul>
<li> <li>
<Button <Button
onClick={openWorkflowSettings} onClick={openAutomationSettings}
className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`} className={`edit-site-sidebar__panel-tab ${automationActiveClass}`}
aria-label={workflowAriaLabel} aria-label={automationAriaLabel}
data-label={__('Automation', 'mailpoet')} data-label={__('Automation', 'mailpoet')}
> >
{__('Automation', 'mailpoet')} {__('Automation', 'mailpoet')}

View File

@ -10,8 +10,8 @@ import {
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { Header } from './header'; import { Header } from './header';
import { StepSidebar } from './step'; import { StepSidebar } from './step';
import { WorkflowSidebar } from './workflow'; import { AutomationSidebar } from './automation';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js // https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
@ -26,7 +26,7 @@ const sidebarActiveByDefault = Platform.select({
type Props = ComponentProps<typeof ComplementaryArea>; type Props = ComponentProps<typeof ComplementaryArea>;
export function Sidebar(props: Props): JSX.Element { export function Sidebar(props: Props): JSX.Element {
const { keyboardShortcut, sidebarKey, showIconLabels, workflowName } = const { keyboardShortcut, sidebarKey, showIconLabels, automationName } =
useSelect( useSelect(
(select) => ({ (select) => ({
keyboardShortcut: select( keyboardShortcut: select(
@ -36,9 +36,9 @@ export function Sidebar(props: Props): JSX.Element {
), ),
sidebarKey: sidebarKey:
select(interfaceStore).getActiveComplementaryArea(storeName) ?? select(interfaceStore).getActiveComplementaryArea(storeName) ??
workflowSidebarKey, automationSidebarKey,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
workflowName: select(storeName).getWorkflowData().name, automationName: select(storeName).getAutomationData().name,
}), }),
[], [],
); );
@ -53,14 +53,14 @@ export function Sidebar(props: Props): JSX.Element {
icon={cog} icon={cog}
className="edit-site-sidebar mailpoet-automation-sidebar" className="edit-site-sidebar mailpoet-automation-sidebar"
panelClassName="edit-site-sidebar" panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)', 'mailpoet')} smallScreenTitle={automationName || __('(no title)', 'mailpoet')}
scope={storeName} scope={storeName}
toggleShortcut={keyboardShortcut} toggleShortcut={keyboardShortcut}
isActiveByDefault={sidebarActiveByDefault} isActiveByDefault={sidebarActiveByDefault}
showIconLabels={showIconLabels} showIconLabels={showIconLabels}
{...props} {...props}
> >
{sidebarKey === workflowSidebarKey && <WorkflowSidebar />} {sidebarKey === automationSidebarKey && <AutomationSidebar />}
{sidebarKey === stepSidebarKey && <StepSidebar />} {sidebarKey === stepSidebarKey && <StepSidebar />}
</ComplementaryArea> </ComplementaryArea>
); );

View File

@ -1,9 +0,0 @@
import { __ } from '@wordpress/i18n';
export function EmptyWorkflow(): JSX.Element {
return (
<div className="mailpoet-automation-editor-empty-workflow">
{__('No automation data.', 'mailpoet')}
</div>
);
}

View File

@ -23,17 +23,17 @@ import { InserterSidebar } from './components/inserter-sidebar';
import { KeyboardShortcuts } from './components/keyboard-shortcuts'; import { KeyboardShortcuts } from './components/keyboard-shortcuts';
import { EditorNotices } from './components/notices'; import { EditorNotices } from './components/notices';
import { Sidebar } from './components/sidebar'; import { Sidebar } from './components/sidebar';
import { Workflow } from './components/workflow'; import { Automation } from './components/automation';
import { createStore, storeName } from './store'; import { createStore, storeName } from './store';
import { initializeApi } from '../api'; import { initializeApi } from '../api';
import { initialize as initializeCoreIntegration } from '../integrations/core'; import { initialize as initializeCoreIntegration } from '../integrations/core';
import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet'; import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet';
import { MailPoet } from '../../mailpoet'; import { MailPoet } from '../../mailpoet';
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices'; import { LISTING_NOTICE_PARAMETERS } from '../listing/automation-listing-notices';
import { registerApiErrorHandler } from './api-error-handler'; import { registerApiErrorHandler } from './api-error-handler';
import { ActivatePanel } from './components/panel/activate-panel'; import { ActivatePanel } from './components/panel/activate-panel';
import { registerTranslations } from '../i18n'; import { registerTranslations } from '../i18n';
import { WorkflowStatus } from '../listing/workflow'; import { AutomationStatus } from '../listing/automation';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
@ -43,27 +43,27 @@ import { WorkflowStatus } from '../listing/workflow';
const showInserterSidebar = false; const showInserterSidebar = false;
/** /**
* Show temporary message that active workflows cant be updated * Show temporary message that active automations cant be updated
* *
* see MAILPOET-4744 * see MAILPOET-4744
*/ */
function updatingActiveWorkflowNotPossible() { function updatingActiveAutomationNotPossible() {
const workflow = globalSelect(storeName).getWorkflowData(); const automation = globalSelect(storeName).getAutomationData();
if ( if (
![WorkflowStatus.ACTIVE, WorkflowStatus.DEACTIVATING].includes( ![AutomationStatus.ACTIVE, AutomationStatus.DEACTIVATING].includes(
workflow.status, automation.status,
) )
) { ) {
return; return;
} }
if (workflow.stats.totals.in_progress === 0) { if (automation.stats.totals.in_progress === 0) {
return; return;
} }
const { createNotice } = dispatch(noticesStore as StoreDescriptor); const { createNotice } = dispatch(noticesStore as StoreDescriptor);
void createNotice( void createNotice(
'success', 'success',
__( __(
'Editing an active workflow is temporarily unavailable. We are working on introducing this functionality.', 'Editing an active automation is temporarily unavailable. We are working on introducing this functionality.',
'mailpoet', 'mailpoet',
), ),
{ {
@ -72,31 +72,53 @@ function updatingActiveWorkflowNotPossible() {
); );
} }
function onUnload(event) {
if (!globalSelect(storeName).getAutomationSaved()) {
// eslint-disable-next-line no-param-reassign
event.returnValue = __(
'There are unsaved changes that will be lost. Do you want to continue?',
'mailpoet',
);
return event.returnValue;
}
return '';
}
function useConfirmUnsaved() {
useEffect(() => {
window.addEventListener('beforeunload', onUnload);
return () => window.removeEventListener('beforeunload', onUnload);
}, []);
}
function Editor(): JSX.Element { function Editor(): JSX.Element {
const { const {
isFullscreenActive, isFullscreenActive,
isInserterOpened, isInserterOpened,
isActivationPanelOpened,
isSidebarOpened, isSidebarOpened,
showIconLabels, showIconLabels,
workflow, automation,
} = useSelect( } = useSelect(
(select) => ({ (select) => ({
isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'), isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'),
isInserterOpened: select(storeName).isInserterSidebarOpened(), isInserterOpened: select(storeName).isInserterSidebarOpened(),
isSidebarOpened: select(storeName).isSidebarOpened(), isSidebarOpened: select(storeName).isSidebarOpened(),
isActivationPanelOpened: select(storeName).isActivationPanelOpened(),
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
workflow: select(storeName).getWorkflowData(), automation: select(storeName).getAutomationData(),
}), }),
[], [],
); );
const [showActivatePanel, setShowActivatePanel] = useState(false);
const [isBooting, setIsBooting] = useState(true); const [isBooting, setIsBooting] = useState(true);
useConfirmUnsaved();
useEffect(() => { useEffect(() => {
if (!isBooting) { if (!isBooting) {
return; return;
} }
updatingActiveWorkflowNotPossible(); updatingActiveAutomationNotPossible();
setIsBooting(false); setIsBooting(false);
}, [isBooting]); }, [isBooting]);
const className = classnames('interface-interface-skeleton', { const className = classnames('interface-interface-skeleton', {
@ -104,17 +126,13 @@ function Editor(): JSX.Element {
'show-icon-labels': showIconLabels, 'show-icon-labels': showIconLabels,
}); });
if (workflow.status === 'trash') { if (automation.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, { window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted]: workflow.id, [LISTING_NOTICE_PARAMETERS.automationHadBeenDeleted]: automation.id,
}); });
return null; return null;
} }
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return ( return (
<ShortcutProvider> <ShortcutProvider>
<SlotFillProvider> <SlotFillProvider>
@ -135,16 +153,11 @@ function Editor(): JSX.Element {
</div> </div>
) )
} }
header={ header={<Header showInserterToggle={showInserterSidebar} />}
<Header
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
content={ content={
<> <>
<EditorNotices /> <EditorNotices />
<Workflow /> <Automation />
</> </>
} }
sidebar={<ComplementaryArea.Slot scope={storeName} />} sidebar={<ComplementaryArea.Slot scope={storeName} />}
@ -152,7 +165,7 @@ function Editor(): JSX.Element {
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
} }
/> />
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />} {isActivationPanelOpened && <ActivatePanel />}
<Popover.Slot /> <Popover.Slot />
</SlotFillProvider> </SlotFillProvider>
</ShortcutProvider> </ShortcutProvider>

View File

@ -7,14 +7,42 @@ import { store as preferencesStore } from '@wordpress/preferences';
import { addQueryArgs } from '@wordpress/url'; import { addQueryArgs } from '@wordpress/url';
import { storeName } from './constants'; import { storeName } from './constants';
import { Feature, State } from './types'; import { Feature, State } from './types';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices'; import { LISTING_NOTICE_PARAMETERS } from '../../listing/automation-listing-notices';
import { MailPoet } from '../../../mailpoet'; import { MailPoet } from '../../../mailpoet';
import { WorkflowStatus } from '../../listing/workflow'; import { AutomationStatus } from '../../listing/automation';
export const openSidebar = const trackErrors = (errors) => {
(key) => if (!errors?.steps) {
({ registry }) => return;
}
const payload = Object.keys(errors.steps as object).map((stepId) => {
const error = errors.steps[stepId];
const stepKey = select(storeName).getStepById(stepId)?.key;
const fields = Object.keys(error.fields as object)
.map((field) => `${stepKey}/${field}`)
.reduce((prev, next) => prev.concat(next));
return fields;
});
MailPoet.trackEvent('Automations > Automation validation error', {
errors: payload,
});
};
export const openActivationPanel = () => ({
type: 'SET_ACTIVATION_PANEL_VISIBILITY',
value: true,
});
export const closeActivationPanel = () => ({
type: 'SET_ACTIVATION_PANEL_VISIBILITY',
value: false,
});
export const openSidebar = (key) => {
dispatch(storeName).closeActivationPanel();
return ({ registry }) =>
registry.dispatch(interfaceStore).enableComplementaryArea(storeName, key); registry.dispatch(interfaceStore).enableComplementaryArea(storeName, key);
};
export const closeSidebar = export const closeSidebar =
() => () =>
@ -46,23 +74,23 @@ export function selectStep(value) {
} as const; } as const;
} }
export function setWorkflowName(name) { export function setAutomationName(name) {
const workflow = select(storeName).getWorkflowData(); const automation = select(storeName).getAutomationData();
return { return {
type: 'UPDATE_WORKFLOW', type: 'UPDATE_AUTOMATION',
workflow: { automation: {
...workflow, ...automation,
name, name,
}, },
} as const; } as const;
} }
export function* save() { export function* save() {
const workflow = select(storeName).getWorkflowData(); const automation = select(storeName).getAutomationData();
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { ...workflow }, data: { ...automation },
}); });
const { createNotice } = dispatch(noticesStore as StoreDescriptor); const { createNotice } = dispatch(noticesStore as StoreDescriptor);
@ -78,23 +106,23 @@ export function* save() {
return { return {
type: 'SAVE', type: 'SAVE',
workflow: data?.data ?? workflow, automation: data?.data ?? automation,
} as const; } as const;
} }
export function* activate() { export function* activate() {
const workflow = select(storeName).getWorkflowData(); const automation = select(storeName).getAutomationData();
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, ...automation,
status: WorkflowStatus.ACTIVE, status: AutomationStatus.ACTIVE,
}, },
}); });
const { createNotice } = dispatch(noticesStore as StoreDescriptor); const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.ACTIVE) { if (data?.data.status === AutomationStatus.ACTIVE) {
void createNotice( void createNotice(
'success', 'success',
__('Well done! Automation is now activated!', 'mailpoet'), __('Well done! Automation is now activated!', 'mailpoet'),
@ -102,29 +130,33 @@ export function* activate() {
type: 'snackbar', type: 'snackbar',
}, },
); );
MailPoet.trackEvent('Automations > Automation activated');
} }
return { return {
type: 'ACTIVATE', type: 'ACTIVATE',
workflow: data?.data ?? workflow, automation: data?.data ?? automation,
} as const; } as const;
} }
export function* deactivate(deactivateWorkflowRuns = true) { export function* deactivate(deactivateAutomationRuns = true) {
const workflow = select(storeName).getWorkflowData(); const automation = select(storeName).getAutomationData();
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, ...automation,
status: deactivateWorkflowRuns status: deactivateAutomationRuns
? WorkflowStatus.DRAFT ? AutomationStatus.DRAFT
: WorkflowStatus.DEACTIVATING, : AutomationStatus.DEACTIVATING,
}, },
}); });
const { createNotice } = dispatch(noticesStore as StoreDescriptor); const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (deactivateWorkflowRuns && data?.data.status === WorkflowStatus.DRAFT) { if (
deactivateAutomationRuns &&
data?.data.status === AutomationStatus.DRAFT
) {
void createNotice( void createNotice(
'success', 'success',
__('Automation is now deactivated!', 'mailpoet'), __('Automation is now deactivated!', 'mailpoet'),
@ -132,10 +164,14 @@ export function* deactivate(deactivateWorkflowRuns = true) {
type: 'snackbar', type: 'snackbar',
}, },
); );
MailPoet.trackEvent('Automations > Automation deactivated', {
type: 'immediate',
});
} }
if ( if (
!deactivateWorkflowRuns && !deactivateAutomationRuns &&
data?.data.status === WorkflowStatus.DEACTIVATING data?.data.status === AutomationStatus.DEACTIVATING
) { ) {
void createNotice( void createNotice(
'success', 'success',
@ -147,36 +183,39 @@ export function* deactivate(deactivateWorkflowRuns = true) {
type: 'snackbar', type: 'snackbar',
}, },
); );
MailPoet.trackEvent('Automations > Automation deactivated', {
type: 'continuous',
});
} }
return { return {
type: 'DEACTIVATE', type: 'DEACTIVATE',
workflow: data?.data ?? workflow, automation: data?.data ?? automation,
} as const; } as const;
} }
export function* trash(onTrashed: () => void = undefined) { export function* trash(onTrashed: () => void = undefined) {
const workflow = select(storeName).getWorkflowData(); const automation = select(storeName).getAutomationData();
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, ...automation,
status: WorkflowStatus.TRASH, status: AutomationStatus.TRASH,
}, },
}); });
onTrashed?.(); onTrashed?.();
if (data?.status === WorkflowStatus.TRASH) { if (data?.status === AutomationStatus.TRASH) {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, { window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id, [LISTING_NOTICE_PARAMETERS.automationDeleted]: automation.id,
}); });
} }
return { return {
type: 'TRASH', type: 'TRASH',
workflow: data?.data ?? workflow, automation: data?.data ?? automation,
} as const; } as const;
} }
@ -197,6 +236,7 @@ export function updateStepArgs(stepId, name, value) {
} }
export function setErrors(errors) { export function setErrors(errors) {
trackErrors(errors);
return { return {
type: 'SET_ERRORS', type: 'SET_ERRORS',
errors, errors,

View File

@ -1,4 +1,4 @@
export const storeName = 'mailpoet/automation-editor'; export const storeName = 'mailpoet/automation-editor';
export const workflowSidebarKey = 'mailpoet/automation-editor/workflow'; export const automationSidebarKey = 'mailpoet/automation-editor/automation';
export const stepSidebarKey = 'mailpoet/automation-editor/step'; export const stepSidebarKey = 'mailpoet/automation-editor/step';

View File

@ -5,12 +5,15 @@ declare let window: AutomationEditorWindow;
export const getInitialState = (): State => ({ export const getInitialState = (): State => ({
context: { ...window.mailpoet_automation_context }, context: { ...window.mailpoet_automation_context },
stepTypes: {}, stepTypes: {},
workflowData: { ...window.mailpoet_automation_workflow }, automationData: { ...window.mailpoet_automation },
workflowSaved: true, automationSaved: true,
selectedStep: undefined, selectedStep: undefined,
inserterSidebar: { inserterSidebar: {
isOpened: false, isOpened: false,
}, },
activationPanel: {
isOpened: false,
},
inserterPopover: undefined, inserterPopover: undefined,
errors: undefined, errors: undefined,
}); });

View File

@ -3,6 +3,14 @@ import { State } from './types';
export function reducer(state: State, action: Action): State { export function reducer(state: State, action: Action): State {
switch (action.type) { switch (action.type) {
case 'SET_ACTIVATION_PANEL_VISIBILITY':
return {
...state,
activationPanel: {
...state.activationPanel,
isOpened: action.value,
},
};
case 'TOGGLE_INSERTER_SIDEBAR': case 'TOGGLE_INSERTER_SIDEBAR':
return { return {
...state, ...state,
@ -21,35 +29,35 @@ export function reducer(state: State, action: Action): State {
...state, ...state,
selectedStep: action.value, selectedStep: action.value,
}; };
case 'UPDATE_WORKFLOW': case 'UPDATE_AUTOMATION':
return { return {
...state, ...state,
workflowData: action.workflow, automationData: action.automation,
workflowSaved: false, automationSaved: false,
}; };
case 'SAVE': case 'SAVE':
return { return {
...state, ...state,
workflowData: action.workflow, automationData: action.automation,
workflowSaved: true, automationSaved: true,
}; };
case 'ACTIVATE': case 'ACTIVATE':
return { return {
...state, ...state,
workflowData: action.workflow, automationData: action.automation,
workflowSaved: true, automationSaved: true,
}; };
case 'DEACTIVATE': case 'DEACTIVATE':
return { return {
...state, ...state,
workflowData: action.workflow, automationData: action.automation,
workflowSaved: true, automationSaved: true,
}; };
case 'TRASH': case 'TRASH':
return { return {
...state, ...state,
workflowData: action.workflow, automationData: action.automation,
workflowSaved: true, automationSaved: true,
}; };
case 'REGISTER_STEP_TYPE': case 'REGISTER_STEP_TYPE':
return { return {
@ -60,7 +68,7 @@ export function reducer(state: State, action: Action): State {
}, },
}; };
case 'UPDATE_STEP_ARGS': { case 'UPDATE_STEP_ARGS': {
const prevArgs = state.workflowData.steps[action.stepId].args ?? {}; const prevArgs = state.automationData.steps[action.stepId].args ?? {};
const value = const value =
typeof action.value === 'function' typeof action.value === 'function'
@ -74,7 +82,7 @@ export function reducer(state: State, action: Action): State {
) )
: { ...prevArgs, [action.name]: value }; : { ...prevArgs, [action.name]: value };
const step = { ...state.workflowData.steps[action.stepId], args }; const step = { ...state.automationData.steps[action.stepId], args };
const stepErrors = Object.values(state.errors?.steps ?? {}).filter( const stepErrors = Object.values(state.errors?.steps ?? {}).filter(
({ step_id }) => step_id !== action.stepId, ({ step_id }) => step_id !== action.stepId,
@ -82,14 +90,14 @@ export function reducer(state: State, action: Action): State {
return { return {
...state, ...state,
workflowData: { automationData: {
...state.workflowData, ...state.automationData,
steps: { steps: {
...state.workflowData.steps, ...state.automationData.steps,
[action.stepId]: step, [action.stepId]: step,
}, },
}, },
workflowSaved: false, automationSaved: false,
selectedStep: step, selectedStep: step,
errors: errors:
stepErrors.length > 0 stepErrors.length > 0

View File

@ -4,7 +4,7 @@ import { store as preferencesStore } from '@wordpress/preferences';
import { storeName } from './constants'; import { storeName } from './constants';
import { Context, Errors, Feature, State, StepErrors, StepType } from './types'; import { Context, Errors, Feature, State, StepErrors, StepType } from './types';
import { Item } from '../components/inserter/item'; import { Item } from '../components/inserter/item';
import { Step, Workflow } from '../components/workflow/types'; import { Step, Automation } from '../components/automation/types';
export const isFeatureActive = createRegistrySelector( export const isFeatureActive = createRegistrySelector(
(select) => (select) =>
@ -21,6 +21,10 @@ export function isInserterSidebarOpened(state: State): boolean {
return state.inserterSidebar.isOpened; return state.inserterSidebar.isOpened;
} }
export function isActivationPanelOpened(state: State): boolean {
return state.activationPanel.isOpened;
}
export function getContext(state: State): Context { export function getContext(state: State): Context {
return state.context; return state.context;
} }
@ -54,18 +58,22 @@ export function getInserterPopover(
return state.inserterPopover; return state.inserterPopover;
} }
export function getWorkflowData(state: State): Workflow { export function getAutomationData(state: State): Automation {
return state.workflowData; return state.automationData;
} }
export function getWorkflowSaved(state: State): boolean { export function getAutomationSaved(state: State): boolean {
return state.workflowSaved; return state.automationSaved;
} }
export function getSelectedStep(state: State): Step | undefined { export function getSelectedStep(state: State): Step | undefined {
return state.selectedStep; return state.selectedStep;
} }
export function getStepById(state: State, id: string): Step | undefined {
return state.automationData.steps[id] ?? undefined;
}
export function getStepType(state: State, key: string): StepType | undefined { export function getStepType(state: State, key: string): StepType | undefined {
return state.stepTypes[key] ?? undefined; return state.stepTypes[key] ?? undefined;
} }

View File

@ -1,9 +1,9 @@
import { ComponentType } from 'react'; import { ComponentType } from 'react';
import { Step, Workflow } from '../components/workflow/types'; import { Step, Automation } from '../components/automation/types';
export interface AutomationEditorWindow extends Window { export interface AutomationEditorWindow extends Window {
mailpoet_automation_context: Context; mailpoet_automation_context: Context;
mailpoet_automation_workflow: Workflow; mailpoet_automation: Automation;
} }
export type Context = { export type Context = {
@ -48,12 +48,15 @@ export type Errors = {
export type State = { export type State = {
context: Context; context: Context;
stepTypes: Record<string, StepType>; stepTypes: Record<string, StepType>;
workflowData: Workflow; automationData: Automation;
workflowSaved: boolean; automationSaved: boolean;
selectedStep: Step | undefined; selectedStep: Step | undefined;
inserterSidebar: { inserterSidebar: {
isOpened: boolean; isOpened: boolean;
}; };
activationPanel: {
isOpened: boolean;
};
inserterPopover?: { inserterPopover?: {
anchor: HTMLElement; anchor: HTMLElement;
type: 'steps' | 'triggers'; type: 'steps' | 'triggers';

View File

@ -3,7 +3,7 @@ import { chartBar } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks'; import { Hooks } from 'wp-js-hooks';
import { MoreControlType, StepMoreControlsType } from '../../../types/filters'; import { MoreControlType, StepMoreControlsType } from '../../../types/filters';
import { StepType } from '../../../editor/store'; import { StepType } from '../../../editor/store';
import { Step } from '../../../editor/components/workflow/types'; import { Step } from '../../../editor/components/automation/types';
const emailStatisticsControl = (step: Step): MoreControlType => { const emailStatisticsControl = (step: Step): MoreControlType => {
const hasEmail = step.args?.email_id > 0; const hasEmail = step.args?.email_id > 0;
@ -28,7 +28,7 @@ const emailStatisticsControl = (step: Step): MoreControlType => {
export function registerStepControls() { export function registerStepControls() {
Hooks.addFilter( Hooks.addFilter(
'mailpoet.automation.workflow.step.more-controls', 'mailpoet.automation.step.more-controls',
'mailpoet', 'mailpoet',
( (
controls: StepMoreControlsType, controls: StepMoreControlsType,

View File

@ -31,11 +31,11 @@ export function EditNewsletter(): JSX.Element {
useState(false); useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false); const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const { selectedStep, workflowId, workflowSaved, errors } = useSelect( const { selectedStep, automationId, automationSaved, errors } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id, automationId: select(storeName).getAutomationData().id,
workflowSaved: select(storeName).getWorkflowSaved(), automationSaved: select(storeName).getAutomationSaved(),
errors: select(storeName).getStepError( errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id, select(storeName).getSelectedStep().id,
), ),
@ -44,7 +44,7 @@ export function EditNewsletter(): JSX.Element {
); );
const emailId = selectedStep?.args?.email_id as number | undefined; const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id; const automationStepId = selectedStep.id;
const errorFields = errors?.fields ?? {}; const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? ''; const emailIdError = errorFields?.email_id ?? '';
@ -58,28 +58,28 @@ export function EditNewsletter(): JSX.Element {
type: 'automation', type: 'automation',
subject: '', subject: '',
options: { options: {
workflowId, automationId,
workflowStepId, automationStepId,
}, },
}, },
}); });
dispatch(storeName).updateStepArgs( dispatch(storeName).updateStepArgs(
workflowStepId, automationStepId,
'email_id', 'email_id',
parseInt(response.data.id as string, 10), parseInt(response.data.id as string, 10),
); );
dispatch(storeName).save(); dispatch(storeName).save();
}, [workflowId, workflowStepId]); }, [automationId, automationStepId]);
// This component is rendered only when no email ID is set. Once we have the ID // This component is rendered only when no email ID is set. Once we have the ID
// and the workflow is saved, we can safely redirect to the email design flow. // and the automation is saved, we can safely redirect to the email design flow.
useEffect(() => { useEffect(() => {
if (redirectToTemplateSelection && emailId && workflowSaved) { if (redirectToTemplateSelection && emailId && automationSaved) {
window.location.href = `admin.php?page=mailpoet-newsletters#/template/${emailId}`; window.location.href = `admin.php?page=mailpoet-newsletters#/template/${emailId}`;
} }
}, [emailId, workflowSaved, redirectToTemplateSelection]); }, [emailId, automationSaved, redirectToTemplateSelection]);
if (!emailId || redirectToTemplateSelection) { if (!emailId || redirectToTemplateSelection) {
return ( return (

View File

@ -8,7 +8,6 @@ export function ShortcodeHelpText(): JSX.Element {
href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes" href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
data-beacon-article="59d662ef042863379ddc6faa"
> >
{__('MailPoet shortcodes', 'mailpoet')} {__('MailPoet shortcodes', 'mailpoet')}
</a> </a>

View File

@ -3,7 +3,7 @@ import { Hooks } from 'wp-js-hooks';
import { Icon } from './icon'; import { Icon } from './icon';
import { Edit } from './edit'; import { Edit } from './edit';
import { State, StepType } from '../../../../editor/store/types'; import { State, StepType } from '../../../../editor/store/types';
import { Step } from '../../../../editor/components/workflow/types'; import { Step } from '../../../../editor/components/automation/types';
export const step: StepType = { export const step: StepType = {
key: 'mailpoet:send-email', key: 'mailpoet:send-email',
@ -20,6 +20,6 @@ export const step: StepType = {
Hooks.applyFilters( Hooks.applyFilters(
'mailpoet.automation.send_email.create_step', 'mailpoet.automation.send_email.create_step',
stepData, stepData,
state.workflowData.id, state.automationData.id,
), ),
} as const; } as const;

View File

@ -2,10 +2,13 @@
* The types in this file document the expected return types of specific * The types in this file document the expected return types of specific
* filters. * filters.
*/ */
import { Step } from '../../../editor/components/workflow/types'; import { Step } from '../../../editor/components/automation/types';
// mailpoet.automation.send_email.create_step // mailpoet.automation.send_email.create_step
export type SendEmailCreateStepType = (step: Step, workflowId: number) => Step; export type SendEmailCreateStepType = (
step: Step,
automationId: number,
) => Step;
// mailpoet.automation.send_email.google_analytics_panel // mailpoet.automation.send_email.google_analytics_panel
export type GoogleAnalyticsPanelBodyType = JSX.Element; export type GoogleAnalyticsPanelBodyType = JSX.Element;

View File

@ -3,27 +3,30 @@ import { __ } from '@wordpress/i18n';
import { Notice } from '../../notices/notice'; import { Notice } from '../../notices/notice';
export const LISTING_NOTICE_PARAMETERS = { export const LISTING_NOTICE_PARAMETERS = {
workflowHadBeenDeleted: 'mailpoet-had-been-deleted', automationHadBeenDeleted: 'mailpoet-had-been-deleted',
workflowDeleted: 'mailpoet-workflow-deleted', automationDeleted: 'mailpoet-automation-deleted',
}; };
export function WorkflowListingNotices(): JSX.Element { export function AutomationListingNotices(): JSX.Element {
const workflowHadBeenDeleted = parseInt( const automationHadBeenDeleted = parseInt(
getQueryArg( getQueryArg(
window.location.href, window.location.href,
LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted, LISTING_NOTICE_PARAMETERS.automationHadBeenDeleted,
) as string, ) as string,
10, 10,
); );
const workflowDeleted = parseInt( const automationDeleted = parseInt(
getQueryArg( getQueryArg(
window.location.href, window.location.href,
LISTING_NOTICE_PARAMETERS.workflowDeleted, LISTING_NOTICE_PARAMETERS.automationDeleted,
) as string, ) as string,
10, 10,
); );
if (Number.isNaN(workflowHadBeenDeleted) && Number.isNaN(workflowDeleted)) { if (
Number.isNaN(automationHadBeenDeleted) &&
Number.isNaN(automationDeleted)
) {
return null; return null;
} }
@ -32,7 +35,7 @@ export function WorkflowListingNotices(): JSX.Element {
...Object.values(LISTING_NOTICE_PARAMETERS), ...Object.values(LISTING_NOTICE_PARAMETERS),
); );
window.history.pushState('', '', urlWithoutNotices); window.history.pushState('', '', urlWithoutNotices);
if (workflowHadBeenDeleted) { if (automationHadBeenDeleted) {
return ( return (
<Notice type="error" closable timeout={false}> <Notice type="error" closable timeout={false}>
<p> <p>
@ -44,7 +47,7 @@ export function WorkflowListingNotices(): JSX.Element {
</Notice> </Notice>
); );
} }
if (workflowDeleted) { if (automationDeleted) {
return ( return (
<Notice type="success" closable timeout={false}> <Notice type="success" closable timeout={false}>
<p>{__('1 automation moved to the Trash.', 'mailpoet')}</p> <p>{__('1 automation moved to the Trash.', 'mailpoet')}</p>

View File

@ -1,14 +1,14 @@
export enum WorkflowStatus { export enum AutomationStatus {
ACTIVE = 'active', ACTIVE = 'active',
DRAFT = 'draft', DRAFT = 'draft',
TRASH = 'trash', TRASH = 'trash',
DEACTIVATING = 'deactivating', DEACTIVATING = 'deactivating',
} }
export type Workflow = { export type Automation = {
id: number; id: number;
name: string; name: string;
status: WorkflowStatus; status: AutomationStatus;
stats: { stats: {
totals: { totals: {
entered: number; entered: number;

View File

@ -1,19 +1,19 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url'; import { addQueryArgs } from '@wordpress/url';
import { Workflow } from '../../workflow'; import { Automation } from '../../automation';
import { MailPoet } from '../../../../mailpoet'; import { MailPoet } from '../../../../mailpoet';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
label?: string; label?: string;
}; };
export function EditWorkflow({ workflow, label }: Props): JSX.Element { export function EditAutomation({ automation, label }: Props): JSX.Element {
return ( return (
<Button <Button
variant="link" variant="link"
href={addQueryArgs(MailPoet.urls.automationEditor, { id: workflow.id })} href={addQueryArgs(MailPoet.urls.automationEditor, { id: automation.id })}
> >
{label ?? __('Edit', 'mailpoet')} {label ?? __('Edit', 'mailpoet')}
</Button> </Button>

View File

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

View File

@ -2,23 +2,23 @@ import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { storeName } from '../../store/constants'; import { storeName } from '../../store/constants';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
previousStatus: WorkflowStatus; previousStatus: AutomationStatus;
}; };
export function UndoTrashButton({ export function UndoTrashButton({
workflow, automation,
previousStatus, previousStatus,
}: Props): JSX.Element { }: Props): JSX.Element {
const { restoreWorkflow } = useDispatch(storeName); const { restoreAutomation } = useDispatch(storeName);
return ( return (
<Button <Button
variant="link" variant="link"
onClick={() => restoreWorkflow(workflow, previousStatus)} onClick={() => restoreAutomation(automation, previousStatus)}
> >
{__('Undo', 'mailpoet')} {__('Undo', 'mailpoet')}
</Button> </Button>

View File

@ -2,32 +2,26 @@ import { Fragment } from 'react';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { DropdownMenu } from '@wordpress/components'; import { DropdownMenu } from '@wordpress/components';
import { moreVertical } from '@wordpress/icons'; import { moreVertical } from '@wordpress/icons';
import { import { useDeleteButton, useRestoreButton, useTrashButton } from '../menu';
useDeleteButton, import { Automation } from '../../automation';
useDuplicateButton, import { EditAutomation } from '../actions';
useRestoreButton,
useTrashButton,
} from '../menu';
import { Workflow } from '../../workflow';
import { EditWorkflow } from '../actions';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
}; };
export function Actions({ workflow }: Props): JSX.Element { export function Actions({ automation }: Props): JSX.Element {
// Menu items are using custom hooks because the "DropdownMenu" component uses the "controls" // 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. // attribute rather than child components, but we need to render modal confirmation dialogs.
const duplicate = useDuplicateButton(workflow); const trash = useTrashButton(automation);
const trash = useTrashButton(workflow); const restore = useRestoreButton(automation);
const restore = useRestoreButton(workflow); const del = useDeleteButton(automation);
const del = useDeleteButton(workflow);
const menuItems = [duplicate, trash, restore, del].filter((item) => item); const menuItems = [trash, restore, del].filter((item) => item);
return ( return (
<div className="mailpoet-automation-listing-cell-actions"> <div className="mailpoet-automation-listing-cell-actions">
<EditWorkflow workflow={workflow} /> <EditAutomation automation={automation} />
{menuItems.map(({ control, slot }) => ( {menuItems.map(({ control, slot }) => (
<Fragment key={control.title}>{slot}</Fragment> <Fragment key={control.title}>{slot}</Fragment>
))} ))}

View File

@ -1,10 +1,10 @@
import { EditWorkflow } from '../actions'; import { EditAutomation } from '../actions';
import { Workflow } from '../../workflow'; import { Automation } from '../../automation';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
}; };
export function Name({ workflow }: Props): JSX.Element { export function Name({ automation }: Props): JSX.Element {
return <EditWorkflow workflow={workflow} label={workflow.name} />; return <EditAutomation automation={automation} label={automation.name} />;
} }

View File

@ -1,16 +1,16 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
}; };
export function Status({ workflow }: Props): JSX.Element { export function Status({ automation }: Props): JSX.Element {
return ( return (
<div className="mailpoet-automation-listing-cell-status"> <div className="mailpoet-automation-listing-cell-status">
{workflow.status === WorkflowStatus.ACTIVE {automation.status === AutomationStatus.ACTIVE
? __('Active', 'mailpoet') ? __('Active', 'mailpoet')
: __('Not active', 'mailpoet')} : __('Draft', 'mailpoet')}
</div> </div>
); );
} }

View File

@ -1,12 +1,12 @@
import { _x } from '@wordpress/i18n'; import { _x } from '@wordpress/i18n';
import { Workflow } from '../../workflow'; import { Automation } from '../../automation';
import { Statistics } from '../../../components/statistics'; import { Statistics } from '../../../components/statistics';
type Props = { type Props = {
workflow: Workflow; automation: Automation;
}; };
export function Subscribers({ workflow }: Props): JSX.Element { export function Subscribers({ automation }: Props): JSX.Element {
return ( return (
<Statistics <Statistics
labelPosition="after" labelPosition="after"
@ -15,19 +15,19 @@ export function Subscribers({ workflow }: Props): JSX.Element {
key: 'entered', key: 'entered',
// translators: Total number of subscribers who entered an automation // translators: Total number of subscribers who entered an automation
label: _x('Entered', 'automation stats', 'mailpoet'), label: _x('Entered', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.entered, value: automation.stats.totals.entered,
}, },
{ {
key: 'processing', key: 'processing',
// translators: Total number of subscribers who are being processed in an automation // translators: Total number of subscribers who are being processed in an automation
label: _x('Processing', 'automation stats', 'mailpoet'), label: _x('Processing', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.in_progress, value: automation.stats.totals.in_progress,
}, },
{ {
key: 'exited', key: 'exited',
// translators: Total number of subscribers who exited an automation, no matter the result // translators: Total number of subscribers who exited an automation, no matter the result
label: _x('Exited', 'automation stats', 'mailpoet'), label: _x('Exited', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.exited, value: automation.stats.totals.exited,
}, },
]} ]}
/> />

View File

@ -4,13 +4,13 @@ import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { Item } from './item'; import { Item } from './item';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
export const useDeleteButton = (workflow: Workflow): Item | undefined => { export const useDeleteButton = (automation: Automation): Item | undefined => {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const { deleteWorkflow } = useDispatch(storeName); const { deleteAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) { if (automation.status !== AutomationStatus.TRASH) {
return undefined; return undefined;
} }
@ -27,7 +27,7 @@ export const useDeleteButton = (workflow: Workflow): Item | undefined => {
title={__('Permanently delete automation', 'mailpoet')} title={__('Permanently delete automation', 'mailpoet')}
confirmButtonText={__('Yes, permanently delete', 'mailpoet')} confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
__experimentalHideHeader={false} __experimentalHideHeader={false}
onConfirm={() => deleteWorkflow(workflow)} onConfirm={() => deleteAutomation(automation)}
onCancel={() => setShowDialog(false)} onCancel={() => setShowDialog(false)}
> >
{sprintf( {sprintf(
@ -36,7 +36,7 @@ export const useDeleteButton = (workflow: Workflow): Item | undefined => {
'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!', 'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!',
'mailpoet', 'mailpoet',
), ),
workflow.name, automation.name,
)} )}
</ConfirmDialog> </ConfirmDialog>
), ),

View File

@ -2,12 +2,14 @@ import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Item } from './item'; import { Item } from './item';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
export const useDuplicateButton = (workflow: Workflow): Item | undefined => { export const useDuplicateButton = (
const { duplicateWorkflow } = useDispatch(storeName); automation: Automation,
): Item | undefined => {
const { duplicateAutomation } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) { if (automation.status === AutomationStatus.TRASH) {
return undefined; return undefined;
} }
@ -16,7 +18,7 @@ export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
control: { control: {
title: __('Duplicate', 'mailpoet'), title: __('Duplicate', 'mailpoet'),
icon: null, icon: null,
onClick: () => duplicateWorkflow(workflow), onClick: () => duplicateAutomation(automation),
}, },
}; };
}; };

View File

@ -2,12 +2,12 @@ import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Item } from './item'; import { Item } from './item';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
export const useRestoreButton = (workflow: Workflow): Item | undefined => { export const useRestoreButton = (automation: Automation): Item | undefined => {
const { restoreWorkflow } = useDispatch(storeName); const { restoreAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) { if (automation.status !== AutomationStatus.TRASH) {
return undefined; return undefined;
} }
@ -16,7 +16,7 @@ export const useRestoreButton = (workflow: Workflow): Item | undefined => {
control: { control: {
title: __('Restore', 'mailpoet'), title: __('Restore', 'mailpoet'),
icon: null, icon: null,
onClick: () => restoreWorkflow(workflow, WorkflowStatus.DRAFT), onClick: () => restoreAutomation(automation, AutomationStatus.DRAFT),
}, },
}; };
}; };

View File

@ -4,13 +4,13 @@ import { useDispatch } from '@wordpress/data';
import { __, _x, sprintf } from '@wordpress/i18n'; import { __, _x, sprintf } from '@wordpress/i18n';
import { Item } from './item'; import { Item } from './item';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow'; import { Automation, AutomationStatus } from '../../automation';
export const useTrashButton = (workflow: Workflow): Item | undefined => { export const useTrashButton = (automation: Automation): Item | undefined => {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const { trashWorkflow } = useDispatch(storeName); const { trashAutomation } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) { if (automation.status === AutomationStatus.TRASH) {
return undefined; return undefined;
} }
@ -27,7 +27,7 @@ export const useTrashButton = (workflow: Workflow): Item | undefined => {
title={__('Trash automation', 'mailpoet')} title={__('Trash automation', 'mailpoet')}
confirmButtonText={__('Yes, move to trash', 'mailpoet')} confirmButtonText={__('Yes, move to trash', 'mailpoet')}
__experimentalHideHeader={false} __experimentalHideHeader={false}
onConfirm={() => trashWorkflow(workflow)} onConfirm={() => trashAutomation(automation)}
onCancel={() => setShowDialog(false)} onCancel={() => setShowDialog(false)}
> >
{sprintf( {sprintf(
@ -36,7 +36,7 @@ export const useTrashButton = (workflow: Workflow): Item | undefined => {
'Are you sure you want to move the automation "%s" to the Trash?', 'Are you sure you want to move the automation "%s" to the Trash?',
'mailpoet', 'mailpoet',
), ),
workflow.name, automation.name,
)} )}
</ConfirmDialog> </ConfirmDialog>
), ),

View File

@ -1,27 +1,27 @@
import { Workflow } from './workflow'; import { Automation } from './automation';
import { Actions, Name, Status, Subscribers } from './components/cells'; import { Actions, Name, Status, Subscribers } from './components/cells';
export function getRow(workflow: Workflow): object[] { export function getRow(automation: Automation): object[] {
return [ return [
{ {
id: workflow.id, id: automation.id,
value: workflow.name, value: automation.name,
display: <Name workflow={workflow} />, display: <Name automation={automation} />,
}, },
{ {
id: workflow.id, id: automation.id,
value: null, value: null,
display: <Subscribers workflow={workflow} />, display: <Subscribers automation={automation} />,
}, },
{ {
id: workflow.id, id: automation.id,
value: workflow.status, value: automation.status,
display: <Status workflow={workflow} />, display: <Status automation={automation} />,
}, },
{ {
id: workflow.id, id: automation.id,
value: null, value: null,
display: <Actions workflow={workflow} />, display: <Actions automation={automation} />,
}, },
]; ];
} }

View File

@ -7,7 +7,7 @@ import { useHistory, useLocation } from 'react-router-dom';
import { plusIcon } from 'common/button/icon/plus'; import { plusIcon } from 'common/button/icon/plus';
import { getRow } from './get-row'; import { getRow } from './get-row';
import { storeName } from './store'; import { storeName } from './store';
import { Workflow, WorkflowStatus } from './workflow'; import { Automation, AutomationStatus } from './automation';
import { MailPoet } from '../../mailpoet'; import { MailPoet } from '../../mailpoet';
const tabConfig = [ const tabConfig = [
@ -17,17 +17,17 @@ const tabConfig = [
className: 'mailpoet-tab-all', className: 'mailpoet-tab-all',
}, },
{ {
name: WorkflowStatus.ACTIVE, name: AutomationStatus.ACTIVE,
title: __('Active', 'mailpoet'), title: __('Active', 'mailpoet'),
className: 'mailpoet-tab-active', className: 'mailpoet-tab-active',
}, },
{ {
name: WorkflowStatus.DRAFT, name: AutomationStatus.DRAFT,
title: _x('Draft', 'noun', 'mailpoet'), title: _x('Draft', 'noun', 'mailpoet'),
className: 'mailpoet-tab-draft', className: 'mailpoet-tab-draft',
}, },
{ {
name: WorkflowStatus.TRASH, name: AutomationStatus.TRASH,
title: _x('Trash', 'noun', 'mailpoet'), title: _x('Trash', 'noun', 'mailpoet'),
className: 'mailpoet-tab-trash', className: 'mailpoet-tab-trash',
}, },
@ -68,14 +68,14 @@ export function AutomationListing(): JSX.Element {
[location], [location],
); );
const workflows = useSelect((select) => select(storeName).getWorkflows()); const automations = useSelect((select) => select(storeName).getAutomations());
const { loadWorkflows } = useDispatch(storeName); const { loadAutomations } = useDispatch(storeName);
const status = pageSearch.get('status'); const status = pageSearch.get('status');
useEffect(() => { useEffect(() => {
loadWorkflows(); loadAutomations();
}, [loadWorkflows]); }, [loadAutomations]);
// focus tab button on status change (needed due to the force re-mount below) // focus tab button on status change (needed due to the force re-mount below)
useLayoutEffect(() => { useLayoutEffect(() => {
@ -103,24 +103,24 @@ export function AutomationListing(): JSX.Element {
[pageSearch, history], [pageSearch, history],
); );
const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => { const groupedAutomations = useMemo<Record<string, Automation[]>>(() => {
const grouped = { all: [] }; const grouped = { all: [] };
(workflows ?? []).forEach((workflow) => { (automations ?? []).forEach((automation) => {
if (!grouped[workflow.status]) { if (!grouped[automation.status]) {
grouped[workflow.status] = []; grouped[automation.status] = [];
} }
grouped[workflow.status].push(workflow); grouped[automation.status].push(automation);
if (workflow.status !== WorkflowStatus.TRASH) { if (automation.status !== AutomationStatus.TRASH) {
grouped.all.push(workflow); grouped.all.push(automation);
} }
}); });
return grouped; return grouped;
}, [workflows]); }, [automations]);
const tabs = useMemo( const tabs = useMemo(
() => () =>
tabConfig.map((tab) => { tabConfig.map((tab) => {
const count = (groupedWorkflows[tab.name] ?? []).length; const count = (groupedAutomations[tab.name] ?? []).length;
return { return {
name: tab.name, name: tab.name,
title: ( title: (
@ -132,38 +132,39 @@ export function AutomationListing(): JSX.Element {
className: tab.className, className: tab.className,
}; };
}), }),
[groupedWorkflows], [groupedAutomations],
); );
const renderTabs = useCallback( const renderTabs = useCallback(
(tab) => { (tab) => {
const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? []; const filteredAutomations: Automation[] =
groupedAutomations[tab.name] ?? [];
const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10); const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10); const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10);
const start = (currentPage - 1) * rowsPerPage; const start = (currentPage - 1) * rowsPerPage;
const rows = filteredWorkflows const rows = filteredAutomations
.map((workflow) => getRow(workflow)) .map((automation) => getRow(automation))
.slice(start, start + rowsPerPage); .slice(start, start + rowsPerPage);
return ( return (
<TableCard <TableCard
className="mailpoet-automation-listing" className="mailpoet-automation-listing"
title="" title=""
isLoading={!workflows} isLoading={!automations}
headers={tableHeaders} headers={tableHeaders}
rows={rows} rows={rows}
rowKey={(_, i) => filteredWorkflows[i].id} rowKey={(_, i) => filteredAutomations[i].id}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
onQueryChange={(key) => (value) => { onQueryChange={(key) => (value) => {
updateUrlSearchString({ [key]: value }); updateUrlSearchString({ [key]: value });
}} }}
totalRows={filteredWorkflows.length} totalRows={filteredAutomations.length}
query={Object.fromEntries(pageSearch)} query={Object.fromEntries(pageSearch)}
showMenu={false} showMenu={false}
/> />
); );
}, },
[workflows, groupedWorkflows, pageSearch, updateUrlSearchString], [automations, groupedAutomations, pageSearch, updateUrlSearchString],
); );
return ( return (

View File

@ -2,8 +2,8 @@ import { dispatch, StoreDescriptor } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls'; import { apiFetch } from '@wordpress/data-controls';
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices'; import { store as noticesStore } from '@wordpress/notices';
import { Workflow, WorkflowStatus } from '../workflow'; import { Automation, AutomationStatus } from '../automation';
import { EditWorkflow, UndoTrashButton } from '../components/actions'; import { EditAutomation, UndoTrashButton } from '../components/actions';
const createSuccessNotice = (content: string, options?: unknown) => const createSuccessNotice = (content: string, options?: unknown) =>
dispatch(noticesStore as StoreDescriptor).createSuccessNotice( dispatch(noticesStore as StoreDescriptor).createSuccessNotice(
@ -14,78 +14,84 @@ const createSuccessNotice = (content: string, options?: unknown) =>
const removeNotice = (id: string) => const removeNotice = (id: string) =>
dispatch(noticesStore as StoreDescriptor).removeNotice(id); dispatch(noticesStore as StoreDescriptor).removeNotice(id);
export function* loadWorkflows() { export function* loadAutomations() {
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows`, path: `/automations`,
}); });
return { return {
type: 'SET_WORKFLOWS', type: 'SET_AUTOMATIONS',
workflows: data.data, automations: data.data,
} as const; } as const;
} }
export function* duplicateWorkflow(workflow: Workflow) { export function* duplicateAutomation(automation: Automation) {
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}/duplicate`, path: `/automations/${automation.id}/duplicate`,
method: 'POST', method: 'POST',
}); });
void createSuccessNotice( void createSuccessNotice(
// translators: %s is the automation name // translators: %s is the automation name
sprintf(__('Automation "%s" was duplicated.', 'mailpoet'), workflow.name), sprintf(__('Automation "%s" was duplicated.', 'mailpoet'), automation.name),
); );
return { return {
type: 'ADD_WORKFLOW', type: 'ADD_AUTOMATION',
workflow: data.data, automation: data.data,
} as const; } as const;
} }
export function* trashWorkflow(workflow: Workflow) { export function* trashAutomation(automation: Automation) {
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
status: WorkflowStatus.TRASH, status: AutomationStatus.TRASH,
}, },
}); });
const message = __('1 automation moved to the Trash.', 'mailpoet'); const message = __('1 automation moved to the Trash.', 'mailpoet');
void createSuccessNotice(message, { void createSuccessNotice(message, {
id: `workflow-trashed-${workflow.id}`, id: `automation-trashed-${automation.id}`,
__unstableHTML: ( __unstableHTML: (
<p> <p>
{message}{' '} {message}{' '}
<UndoTrashButton workflow={workflow} previousStatus={workflow.status} /> <UndoTrashButton
automation={automation}
previousStatus={automation.status}
/>
</p> </p>
), ),
}); });
return { return {
type: 'UPDATE_WORKFLOW', type: 'UPDATE_AUTOMATION',
workflow: data.data, automation: data.data,
} as const; } as const;
} }
export function* restoreWorkflow(workflow: Workflow, status: WorkflowStatus) { export function* restoreAutomation(
automation: Automation,
status: AutomationStatus,
) {
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
status, status,
}, },
}); });
void removeNotice(`workflow-trashed-${workflow.id}`); void removeNotice(`automation-trashed-${automation.id}`);
const message = __('1 automation restored from the Trash.', 'mailpoet'); const message = __('1 automation restored from the Trash.', 'mailpoet');
void createSuccessNotice(message, { void createSuccessNotice(message, {
__unstableHTML: ( __unstableHTML: (
<p> <p>
{message}{' '} {message}{' '}
<EditWorkflow <EditAutomation
workflow={workflow} automation={automation}
label={__('Edit automation', 'mailpoet')} label={__('Edit automation', 'mailpoet')}
/> />
</p> </p>
@ -93,14 +99,14 @@ export function* restoreWorkflow(workflow: Workflow, status: WorkflowStatus) {
}); });
return { return {
type: 'UPDATE_WORKFLOW', type: 'UPDATE_AUTOMATION',
workflow: data.data, automation: data.data,
} as const; } as const;
} }
export function* deleteWorkflow(workflow: Workflow) { export function* deleteAutomation(automation: Automation) {
yield apiFetch({ yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/automations/${automation.id}`,
method: 'DELETE', method: 'DELETE',
}); });
@ -109,7 +115,7 @@ export function* deleteWorkflow(workflow: Workflow) {
); );
return { return {
type: 'DELETE_WORKFLOW', type: 'DELETE_AUTOMATION',
workflow, automation,
} as const; } as const;
} }

View File

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

View File

@ -1,31 +1,33 @@
import { Action } from '@wordpress/data'; import { Action } from '@wordpress/data';
import { State } from './types'; import { State } from './types';
import { Workflow } from '../workflow'; import { Automation } from '../automation';
export function reducer(state: State, action: Action): State { export function reducer(state: State, action: Action): State {
switch (action.type) { switch (action.type) {
case 'SET_WORKFLOWS': case 'SET_AUTOMATIONS':
return { return {
...state, ...state,
workflows: action.workflows, automations: action.automations,
}; };
case 'ADD_WORKFLOW': case 'ADD_AUTOMATION':
return { return {
...state, ...state,
workflows: [action.workflow, ...state.workflows], automations: [action.automation, ...state.automations],
}; };
case 'UPDATE_WORKFLOW': case 'UPDATE_AUTOMATION':
return { return {
...state, ...state,
workflows: state.workflows.map((workflow: Workflow) => automations: state.automations.map((automation: Automation) =>
workflow.id === action.workflow.id ? action.workflow : workflow, automation.id === action.automation.id
? action.automation
: automation,
), ),
}; };
case 'DELETE_WORKFLOW': case 'DELETE_AUTOMATION':
return { return {
...state, ...state,
workflows: state.workflows.filter( automations: state.automations.filter(
(workflow: Workflow) => workflow.id !== action.workflow.id, (automation: Automation) => automation.id !== action.automation.id,
), ),
}; };
default: default:

View File

@ -1,11 +1,11 @@
import { State } from './types'; import { State } from './types';
import { Workflow } from '../workflow'; import { Automation } from '../automation';
import { workflowCount } from '../../config'; import { automationCount } from '../../config';
export function getWorkflows(state: State): Workflow[] { export function getAutomations(state: State): Automation[] {
return state.workflows; return state.automations;
} }
export function getWorkflowCount(state: State): number { export function getAutomationCount(state: State): number {
return state.workflows ? state.workflows.length : workflowCount; return state.automations ? state.automations.length : automationCount;
} }

View File

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

View File

@ -15,13 +15,13 @@ export function BuildYourOwnSection(): JSX.Element {
image: `${MailPoet.cdnUrl}automation/sections/start-with-a-trigger.png`, image: `${MailPoet.cdnUrl}automation/sections/start-with-a-trigger.png`,
}, },
{ {
slug: 'customize-your-workflow', slug: 'customize-your-automation',
title: __('Customize your automation', 'mailpoet'), title: __('Customize your automation', 'mailpoet'),
text: __( text: __(
'Choose steps and create a custom journey to best suit your needs.', 'Choose steps and create a custom journey to best suit your needs.',
'mailpoet', 'mailpoet',
), ),
image: `${MailPoet.cdnUrl}automation/sections/customize-your-workflow.png`, image: `${MailPoet.cdnUrl}automation/sections/customize-your-automation.png`,
}, },
{ {
slug: 'design-your-email', slug: 'design-your-email',

View File

@ -1,11 +1,11 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { MailPoet } from '../../mailpoet'; import { MailPoet } from '../../mailpoet';
import { workflowTemplates } from '../templates/config'; import { automationTemplates } from '../templates/config';
import { TemplateListItem } from '../templates/components/template-list-item'; import { TemplateListItem } from '../templates/components/template-list-item';
export function TemplatesSection(): JSX.Element { export function TemplatesSection(): JSX.Element {
const templates = workflowTemplates.slice(0, 3); const templates = automationTemplates.slice(0, 3);
return ( return (
<section className="mailpoet-automation-section"> <section className="mailpoet-automation-section">

View File

@ -1,30 +1,59 @@
import { useCallback, useState } from 'react';
import apiFetch from '@wordpress/api-fetch';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { addQueryArgs } from '@wordpress/url'; import { addQueryArgs } from '@wordpress/url';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { WorkflowTemplate } from '../config'; import { AutomationTemplate } from '../config';
import { useMutation } from '../../api';
import { MailPoet } from '../../../mailpoet'; import { MailPoet } from '../../../mailpoet';
import { Notice } from '../../../notices/notice'; import { Notice } from '../../../notices/notice';
import {
PremiumModal,
premiumValidAndActive,
} from '../../../common/premium_modal';
type TemplateListItemProps = { type TemplateListItemProps = {
template: WorkflowTemplate; template: AutomationTemplate;
heading?: 'h2' | 'h3'; heading?: 'h2' | 'h3';
}; };
const useCreateFromTemplate = () => {
const [state, setState] = useState({
data: undefined,
loading: false,
error: undefined,
});
const create = useCallback(async (slug: string) => {
setState((prevState) => ({ ...prevState, loading: true }));
try {
const data = await apiFetch({
path: `/automations/create-from-template`,
method: 'POST',
data: { slug },
});
setState((prevState) => ({ ...prevState, data }));
} catch (error) {
setState((prevState) => ({ ...prevState, error }));
} finally {
setState((prevState) => ({ ...prevState, loading: false }));
}
}, []);
return [create, state] as const;
};
export function TemplateListItem({ export function TemplateListItem({
template, template,
heading, heading,
}: TemplateListItemProps): JSX.Element { }: TemplateListItemProps): JSX.Element {
const [createWorkflowFromTemplate, { loading, error, data }] = useMutation( const [showPremium, setShowPremium] = useState(false);
'workflows/create-from-template', const [createAutomationFromTemplate, { loading, error, data }] =
{ useCreateFromTemplate();
method: 'POST',
body: JSON.stringify({
slug: template.slug,
}),
},
);
if (!error && data) { if (!error && data) {
MailPoet.trackEvent('Automations > Template selected', {
'Automation slug': template.slug,
});
window.location.href = addQueryArgs(MailPoet.urls.automationEditor, { window.location.href = addQueryArgs(MailPoet.urls.automationEditor, {
id: data.data.id, id: data.data.id,
}); });
@ -45,19 +74,50 @@ export function TemplateListItem({
const headingTag = heading ?? 'h2'; const headingTag = heading ?? 'h2';
return ( return (
<li className="mailpoet-automation-template-list-item"> <li
className={`mailpoet-automation-template-list-item mailpoet-automation-template-list-item-${template.type}`}
>
{notice} {notice}
<Button <Button
isBusy={loading} isBusy={loading}
disabled={template.type === 'coming-soon'}
onClick={() => { onClick={() => {
void createWorkflowFromTemplate(); if (template.type === 'premium' && !premiumValidAndActive) {
setShowPremium(true);
return;
}
void createAutomationFromTemplate(template.slug);
}} }}
> >
<div className="badge">
{template.type === 'coming-soon' && (
<span>{__('Coming soon', 'mailpoet')}</span>
)}
{template.type === 'premium' && (
<span>{__('Premium', 'mailpoet')}</span>
)}
</div>
{headingTag === 'h3' && <h3>{template.name}&nbsp;</h3>} {headingTag === 'h3' && <h3>{template.name}&nbsp;</h3>}
{headingTag === 'h2' && <h2>{template.name}&nbsp;</h2>} {headingTag === 'h2' && <h2>{template.name}&nbsp;</h2>}
<p>{template.description}</p> <p>{template.description}</p>
</Button> </Button>
{showPremium && (
<PremiumModal
onRequestClose={() => {
setShowPremium(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'automation_premium_template',
}}
>
{__(
'All templates and fully configurable automations are available in the premium version.',
'mailpoet',
)}
</PremiumModal>
)}
</li> </li>
); );
} }

View File

@ -1,13 +1,14 @@
export type WorkflowTemplate = { export type AutomationTemplate = {
slug: string; slug: string;
name: string; name: string;
description: string; description: string;
type: 'default' | 'free-only' | 'premium' | 'coming-soon';
}; };
declare global { declare global {
interface Window { interface Window {
mailpoet_automation_templates: WorkflowTemplate[]; mailpoet_automation_templates: AutomationTemplate[];
} }
} }
export const workflowTemplates = window.mailpoet_automation_templates; export const automationTemplates = window.mailpoet_automation_templates;

View File

@ -1,7 +1,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Flex } from '@wordpress/components'; import { Flex } from '@wordpress/components';
import { workflowTemplates } from './config'; import { automationTemplates } from './config';
import { TemplateListItem } from './components/template-list-item'; import { TemplateListItem } from './components/template-list-item';
import { initializeApi } from '../api'; import { initializeApi } from '../api';
import { registerTranslations } from '../i18n'; import { registerTranslations } from '../i18n';
@ -23,7 +23,7 @@ function Templates(): JSX.Element {
</Flex> </Flex>
<ul className="mailpoet-automation-templates"> <ul className="mailpoet-automation-templates">
{workflowTemplates.map((template) => ( {automationTemplates.map((template) => (
<TemplateListItem key={template.slug} template={template} /> <TemplateListItem key={template.slug} template={template} />
))} ))}
<FromScratchListItem /> <FromScratchListItem />

View File

@ -1,84 +0,0 @@
import { ReactNode } from 'react';
import { useMutation } from './api';
import { Step, Workflow } from './editor/components/workflow/types';
export const createRootStep = (): Step => ({
id: 'root',
type: 'root',
key: 'core:root',
args: {},
next_steps: [],
});
const createWorkflow = (): Partial<Workflow> => ({
name: 'Empty workflow',
steps: {
root: createRootStep(),
},
});
export function CreateEmptyWorkflowButton(): JSX.Element {
const [createSchema, { loading, error }] = useMutation('workflows', {
method: 'POST',
});
return (
<div>
<button
className="button"
type="button"
onClick={async () => {
await createSchema({
body: JSON.stringify(createWorkflow()),
});
window.location.reload();
}}
disabled={loading}
>
Create empty workflow (premium required)
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
);
}
type TemplateButtonProps = {
slug: string;
children?: ReactNode;
};
export function CreateWorkflowFromTemplateButton({
slug,
children,
}: TemplateButtonProps): JSX.Element {
const [createWorkflowFromTemplate, { loading, error }] = useMutation(
'workflows/create-from-template',
{
method: 'POST',
body: JSON.stringify({
slug,
}),
},
);
return (
<div>
<button
className="button button-primary"
type="button"
onClick={async () => {
await createWorkflowFromTemplate();
window.location.reload();
}}
disabled={loading}
>
{children}
</button>
{error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div>
);
}

View File

@ -7,7 +7,7 @@ import { Dispatch } from 'react';
import { DropdownMenu } from '@wordpress/components'; import { DropdownMenu } from '@wordpress/components';
import { StoreConfig } from '@wordpress/data'; import { StoreConfig } from '@wordpress/data';
import { Item } from '../editor/components/inserter/item'; import { Item } from '../editor/components/inserter/item';
import { Step } from '../editor/components/workflow/types'; import { Step } from '../editor/components/automation/types';
import { State } from '../editor/store/types'; import { State } from '../editor/store/types';
export type MoreControlType = { export type MoreControlType = {
@ -20,17 +20,17 @@ export type MoreControlType = {
* APPLICATION HOOKS * APPLICATION HOOKS
*/ */
// mailpoet.automation.workflow.step.more-controls // mailpoet.automation.step.more-controls
// mailpoet.automation.hero.actions // mailpoet.automation.hero.actions
export type StepMoreControlsType = Record<string, MoreControlType>; export type StepMoreControlsType = Record<string, MoreControlType>;
// mailpoet.automation.workflow.add_step_callback // mailpoet.automation.add_step_callback
export type AddStepCallbackType = (item?: Item) => void; export type AddStepCallbackType = (item?: Item) => void;
// mailpoet.automation.workflow.render_step // mailpoet.automation.render_step
export type RenderStepType = (step: Step) => JSX.Element; export type RenderStepType = (step: Step) => JSX.Element;
// mailpoet.automation.workflow.render_step_separator // mailpoet.automation.render_step_separator
export type RenderStepSeparatorType = (step: Step) => JSX.Element; export type RenderStepSeparatorType = (step: Step) => JSX.Element;
// mailpoet.automation.editor.create_store // mailpoet.automation.editor.create_store

View File

@ -6,14 +6,14 @@ MailPoet.I18n.add('excellentBadgeName', 'Excellent');
MailPoet.I18n.add('excellentBadgeTooltip', 'Congrats!'); MailPoet.I18n.add('excellentBadgeTooltip', 'Congrats!');
MailPoet.I18n.add('goodBadgeName', 'Good'); MailPoet.I18n.add('goodBadgeName', 'Good');
MailPoet.I18n.add('goodBadgeTooltip', 'Good stuff.'); MailPoet.I18n.add('goodBadgeTooltip', 'Good stuff.');
MailPoet.I18n.add('averageBadgeName', 'Average'); MailPoet.I18n.add('criticalBadgeName', 'Critical');
MailPoet.I18n.add('averageBadgeTooltip', 'Something to improve.'); MailPoet.I18n.add('criticalBadgeTooltip', 'Something to improve.');
MailPoet.I18n.add('openedStatTooltipExcellent', 'above 30%'); MailPoet.I18n.add('openedStatTooltipExcellent', 'above 30%');
MailPoet.I18n.add('openedStatTooltipGood', 'between 10 and 30%'); MailPoet.I18n.add('openedStatTooltipGood', 'between 10 and 30%');
MailPoet.I18n.add('openedStatTooltipAverage', 'under 10%'); MailPoet.I18n.add('openedStatTooltipCritical', 'under 10%');
MailPoet.I18n.add('clickedStatTooltipExcellent', 'above 3%'); MailPoet.I18n.add('clickedStatTooltipExcellent', 'above 3%');
MailPoet.I18n.add('clickedStatTooltipGood', 'between 1 and 3%'); MailPoet.I18n.add('clickedStatTooltipGood', 'between 1 and 3%');
MailPoet.I18n.add('clickedStatTooltipAverage', 'under 1%'); MailPoet.I18n.add('clickedStatTooltipCritical', 'under 1%');
MailPoet.I18n.add( MailPoet.I18n.add(
'revenueStatsTooltipShort', 'revenueStatsTooltipShort',
'Revenues by customer who clicked on this email in the last 2 weeks.', 'Revenues by customer who clicked on this email in the last 2 weeks.',

View File

@ -42,6 +42,12 @@ export function NewsletterStats({
rate={clicked} rate={clicked}
tooltipId={`clicked-${newsletterId || '0'}`} tooltipId={`clicked-${newsletterId || '0'}`}
/> />
<br />
<StatsBadge
stat="opened"
rate={opened}
tooltipId={`opened-${newsletterId || '0'}`}
/>
</div> </div>
)} )}
</div> </div>

View File

@ -8,7 +8,7 @@ type BadgeProps = {
tooltip?: string | ReactNode; tooltip?: string | ReactNode;
tooltipId?: string; tooltipId?: string;
tooltipPlace?: Place; tooltipPlace?: Place;
type?: 'average' | 'good' | 'excellent' | 'unknown'; type?: 'average' | 'good' | 'excellent' | 'critical' | 'unknown';
isInverted?: boolean; isInverted?: boolean;
}; };

View File

@ -13,21 +13,39 @@ type StatsBadgeProps = {
const stats = { const stats = {
opened: { opened: {
badgeRanges: [30, 10, 0], badgeRanges: [30, 10, 0],
badgeTypes: ['excellent', 'good', 'average'], badgeTypes: ['excellent', 'good', 'critical'],
tooltipText: [ tooltipText: {
MailPoet.I18n.t('openedStatTooltipExcellent'), excellent: MailPoet.I18n.t('openedStatTooltipExcellent'),
MailPoet.I18n.t('openedStatTooltipGood'), good: MailPoet.I18n.t('openedStatTooltipGood'),
MailPoet.I18n.t('openedStatTooltipAverage'), critical: MailPoet.I18n.t('openedStatTooltipCritical'),
], },
}, },
clicked: { clicked: {
badgeRanges: [3, 1, 0], badgeRanges: [3, 1, 0],
badgeTypes: ['excellent', 'good', 'average'], badgeTypes: ['excellent', 'good', 'critical'],
tooltipText: [ tooltipText: {
MailPoet.I18n.t('clickedStatTooltipExcellent'), excellent: MailPoet.I18n.t('clickedStatTooltipExcellent'),
MailPoet.I18n.t('clickedStatTooltipGood'), good: MailPoet.I18n.t('clickedStatTooltipGood'),
MailPoet.I18n.t('clickedStatTooltipAverage'), critical: MailPoet.I18n.t('clickedStatTooltipCritical'),
], },
},
bounced: {
badgeRanges: [1.5, 0.5, 0],
badgeTypes: ['critical', 'good', 'excellent'],
tooltipText: {
excellent: MailPoet.I18n.t('bouncedStatTooltipExcellent'),
good: MailPoet.I18n.t('bouncedStatTooltipGood'),
critical: MailPoet.I18n.t('bouncedStatTooltipCritical'),
},
},
unsubscribed: {
badgeRanges: [0.7, 0.3, 0],
badgeTypes: ['critical', 'good', 'excellent'],
tooltipText: {
excellent: MailPoet.I18n.t('unsubscribeStatTooltipExcellent'),
good: MailPoet.I18n.t('unsubscribeStatTooltipGood'),
critical: MailPoet.I18n.t('unsubscribeStatTooltipCritical'),
},
}, },
}; };
@ -60,9 +78,9 @@ function StatsBadge(props: StatsBadgeProps) {
name: MailPoet.I18n.t('goodBadgeName'), name: MailPoet.I18n.t('goodBadgeName'),
tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip'), tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip'),
}, },
average: { critical: {
name: MailPoet.I18n.t('averageBadgeName'), name: MailPoet.I18n.t('criticalBadgeName'),
tooltipTitle: MailPoet.I18n.t('averageBadgeTooltip'), tooltipTitle: MailPoet.I18n.t('criticalBadgeTooltip'),
}, },
}; };
@ -86,15 +104,15 @@ function StatsBadge(props: StatsBadgeProps) {
<div className="mailpoet-listing-stats-tooltip-content"> <div className="mailpoet-listing-stats-tooltip-content">
<Badge type="excellent" name={badges.excellent.name} /> <Badge type="excellent" name={badges.excellent.name} />
{' : '} {' : '}
{stat.tooltipText[0]} {stat.tooltipText.excellent}
<br /> <br />
<Badge type="good" name={badges.good.name} /> <Badge type="good" name={badges.good.name} />
{' : '} {' : '}
{stat.tooltipText[1]} {stat.tooltipText.good}
<br /> <br />
<Badge type="average" name={badges.average.name} /> <Badge type="critical" name={badges.critical.name} />
{' : '} {' : '}
{stat.tooltipText[2]} {stat.tooltipText.critical}
</div> </div>
</div> </div>
); );

View File

@ -22,7 +22,8 @@ import {
UtmParams, UtmParams,
} from './upgrade_info'; } from './upgrade_info';
const premiumValidAndActive = premiumFeaturesEnabled && MailPoet.premiumActive; export const premiumValidAndActive =
premiumFeaturesEnabled && MailPoet.premiumActive;
type Props = Omit<ComponentProps<typeof Modal>, 'title' | 'onRequestClose'> & { type Props = Omit<ComponentProps<typeof Modal>, 'title' | 'onRequestClose'> & {
// Fix type from "@types/wordpress__components" where it is defined as a union of event // Fix type from "@types/wordpress__components" where it is defined as a union of event

View File

@ -61,6 +61,20 @@ export function Tags() {
Excellent Excellent
</Tag> </Tag>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Tag dimension="large" variant="critical">
Critical
</Tag>
&nbsp;
<Tag dimension="large" variant="critical" isInverted>
Critical
</Tag>
<br />
<Tag variant="critical">Critical</Tag>
&nbsp;
<Tag variant="critical" isInverted>
Critical
</Tag>
<div className="mailpoet-gap" />
<Tag dimension="large" variant="wordpress"> <Tag dimension="large" variant="wordpress">
WordPress WordPress
</Tag> </Tag>

View File

@ -1,9 +1,18 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
export type TagVariant =
| 'average'
| 'good'
| 'excellent'
| 'critical'
| 'list'
| 'unknown'
| 'wordpress';
type Props = { type Props = {
children?: ReactNode; children?: ReactNode;
variant?: 'average' | 'good' | 'excellent' | 'list' | 'unknown' | 'wordpress'; variant?: TagVariant;
dimension?: 'large'; dimension?: 'large';
isInverted?: boolean; isInverted?: boolean;
className?: string; className?: string;

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Tag } from './tag'; import { Tag, TagVariant } from './tag';
import { Tooltip } from '../tooltip/tooltip'; import { Tooltip } from '../tooltip/tooltip';
import { MailPoet } from '../../mailpoet'; import { MailPoet } from '../../mailpoet';
@ -21,7 +21,7 @@ type Props = {
segments?: Segment[]; segments?: Segment[];
subscriberTags?: SubscriberTag[]; subscriberTags?: SubscriberTag[];
strings?: string[]; strings?: string[];
variant?: 'average' | 'good' | 'excellent' | 'list' | 'unknown' | 'wordpress'; variant?: TagVariant;
isInverted?: boolean; isInverted?: boolean;
}; };

View File

@ -122,7 +122,7 @@ Module.SaveView = Marionette.View.extend({
'click .mailpoet_save_activate_wc_customizer_button': 'click .mailpoet_save_activate_wc_customizer_button':
'activateWooCommerceCustomizer', 'activateWooCommerceCustomizer',
/* Automation email */ /* Automation email */
'click .mailpoet_save_go_to_workflow': 'saveAndGoToWorkflow', 'click .mailpoet_save_go_to_automation': 'saveAndGoToAutomation',
'click .mailpoet_show_preview': 'showPreview', 'click .mailpoet_show_preview': 'showPreview',
}, },
@ -316,13 +316,13 @@ Module.SaveView = Marionette.View.extend({
}); });
} }
}, },
saveAndGoToWorkflow: function () { saveAndGoToAutomation: function () {
this.hideSaveOptions(); this.hideSaveOptions();
Module._cancelAutosave(); Module._cancelAutosave();
Module.save().done(function () { Module.save().done(function () {
const newsletter = App.getNewsletter(); const newsletter = App.getNewsletter();
const workflowId = newsletter.get('options').get('workflowId'); const automationId = newsletter.get('options').get('automationId');
const goToUrl = `admin.php?page=mailpoet-automation-editor&id=${workflowId}`; const goToUrl = `admin.php?page=mailpoet-automation-editor&id=${automationId}`;
window.location.href = goToUrl; window.location.href = goToUrl;
}); });
}, },

View File

@ -18,8 +18,8 @@ const renderHeading = (newsletterType, newsletterOptions) => {
window.location = `admin.php?page=mailpoet-newsletters`; window.location = `admin.php?page=mailpoet-newsletters`;
}; };
if (newsletterType === 'automation') { if (newsletterType === 'automation') {
const workflowId = newsletterOptions.workflowId; const automationId = newsletterOptions.automationId;
const goToUrl = `admin.php?page=mailpoet-automation-editor&id=${workflowId}`; const goToUrl = `admin.php?page=mailpoet-automation-editor&id=${automationId}`;
onLogoClick = () => { onLogoClick = () => {
window.location = goToUrl; window.location = goToUrl;
}; };
@ -27,7 +27,7 @@ const renderHeading = (newsletterType, newsletterOptions) => {
const onClickPreview = () => const onClickPreview = () =>
document.querySelector('.mailpoet_show_preview').click(); document.querySelector('.mailpoet_show_preview').click();
const onClickSave = () => const onClickSave = () =>
document.querySelector('.mailpoet_save_go_to_workflow').click(); document.querySelector('.mailpoet_save_go_to_automation').click();
buttons = ( buttons = (
<> <>
<input <input

View File

@ -17,6 +17,9 @@ type Props = {
const minNewslettersSent = 20; const minNewslettersSent = 20;
const minNewslettersOpened = 5; const minNewslettersOpened = 5;
const minUnsubscribedStat = 5;
const minBouncedStat = 5;
const minNewslettersSentForBouncedAndUnsubscribed = 100;
// When percentage value is lower then 0.1 we want to display value with two decimal places // When percentage value is lower then 0.1 we want to display value with two decimal places
const formatWithOptimalPrecision = (value: number) => { const formatWithOptimalPrecision = (value: number) => {
@ -24,6 +27,16 @@ const formatWithOptimalPrecision = (value: number) => {
return MailPoet.Num.toLocaleFixed(value, precision); return MailPoet.Num.toLocaleFixed(value, precision);
}; };
/*
* FormatForStats
* always round-up to one decimal place
* in stats.tsx, we are comparing against 0, 0.3, 0.5, 0.7, 1.5, etc
*/
const formatForStats = (value: number): number => {
const numValue = +value;
return +numValue.toFixed(1);
};
export function NewsletterGeneralStats({ export function NewsletterGeneralStats({
newsletter, newsletter,
isWoocommerceActive, isWoocommerceActive,
@ -61,14 +74,35 @@ export function NewsletterGeneralStats({
totalSent >= minNewslettersSent && totalSent >= minNewslettersSent &&
newsletter.statistics.opened >= minNewslettersOpened; newsletter.statistics.opened >= minNewslettersOpened;
const displayUnsubscribedBadge =
newsletter.statistics.unsubscribed >= minUnsubscribedStat &&
totalSent >= minNewslettersSentForBouncedAndUnsubscribed;
const displayBouncedBadge =
newsletter.statistics.bounced >= minBouncedStat &&
totalSent >= minNewslettersSentForBouncedAndUnsubscribed;
const badgeTypeOpened = getBadgeType('opened', percentageOpened) || '';
const opened = ( const opened = (
<div className="mailpoet-statistics-value-small"> <>
<span className="mailpoet-statistics-value-number"> <div className="mailpoet-statistics-value-small">
{percentageOpenedDisplay} <span
{'% '} className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeOpened}`}
</span> >
{MailPoet.I18n.t('percentageOpened')} {percentageOpenedDisplay}
</div> {'% '}
</span>
{MailPoet.I18n.t('percentageOpened')}
</div>
{displayBadges && (
<StatsBadge
isInverted={false}
stat="opened"
rate={percentageOpened}
tooltipId={`opened-${newsletter.id || '0'}`}
tooltipPlace="right"
/>
)}
</>
); );
const machineOpened = ( const machineOpened = (
@ -100,24 +134,60 @@ export function NewsletterGeneralStats({
</div> </div>
); );
const formattedPercentageUnsubscribed = formatForStats(
percentageUnsubscribed,
);
const badgeTypeUnsubscribed = displayUnsubscribedBadge
? getBadgeType('unsubscribed', formattedPercentageUnsubscribed)
: '';
const unsubscribed = ( const unsubscribed = (
<div className="mailpoet-statistics-value-small"> <>
<span className="mailpoet-statistics-value-number"> <div className="mailpoet-statistics-value-small">
{percentageUnsubscribedDisplay} <span
{'% '} className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeUnsubscribed}`}
</span> >
{MailPoet.I18n.t('percentageUnsubscribed')} {percentageUnsubscribedDisplay}
</div> {'% '}
</span>
{MailPoet.I18n.t('percentageUnsubscribed')}
</div>
{displayUnsubscribedBadge && (
<StatsBadge
isInverted={false}
stat="unsubscribed"
rate={formattedPercentageUnsubscribed}
tooltipId={`unsubscribed-${newsletter.id || '0'}`}
tooltipPlace="right"
/>
)}
</>
); );
const formattedPercentageBounced = formatForStats(percentageBounced);
const badgeTypeBounced = displayBouncedBadge
? getBadgeType('bounced', formattedPercentageBounced)
: '';
const bounced = ( const bounced = (
<div className="mailpoet-statistics-value-small"> <>
<span className="mailpoet-statistics-value-number"> <div className="mailpoet-statistics-value-small">
{percentageBouncedDisplay} <span
{'% '} className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeBounced}`}
</span> >
{MailPoet.I18n.t('percentageBounced')} {percentageBouncedDisplay}
</div> {'% '}
</span>
{MailPoet.I18n.t('percentageBounced')}
</div>
{displayBouncedBadge && (
<StatsBadge
isInverted={false}
stat="bounced"
rate={formattedPercentageBounced}
tooltipId={`bounced-${newsletter.id || '0'}`}
tooltipPlace="right"
/>
)}
</>
); );
const badgeTypeClicked = getBadgeType('clicked', percentageClicked); const badgeTypeClicked = getBadgeType('clicked', percentageClicked);

View File

@ -99,6 +99,10 @@ function CampaignStatsPageComponent({ match, history, location }: Props) {
); );
} }
if (!newsletter) {
return <h3> {MailPoet.I18n.t('emailDoesNotExist')} </h3>;
}
return ( return (
<> <>
<HideScreenOptions /> <HideScreenOptions />

View File

@ -203,11 +203,11 @@ class NewsletterSendComponent extends Component {
: null; : null;
const item = response.data; const item = response.data;
// Automation type emails should redirect // Automation type emails should redirect
// to an associated workflow from the send page // to an associated automation from the send page
if (item.type === 'automation') { if (item.type === 'automation') {
const workflowId = item.options?.workflowId; const automationId = item.options?.automationId;
const goToUrl = workflowId const goToUrl = automationId
? `admin.php?page=mailpoet-automation-editor&id=${workflowId}` ? `admin.php?page=mailpoet-automation-editor&id=${automationId}`
: '/new'; : '/new';
return this.setState( return this.setState(
{ {

View File

@ -317,9 +317,9 @@ class NewsletterTemplates extends Component {
let buttons = null; let buttons = null;
let onClick; let onClick;
if (this.state.emailType === 'automation') { if (this.state.emailType === 'automation') {
const workflowId = this.state.emailOptions?.workflowId; const automationId = this.state.emailOptions?.automationId;
const goToUrl = workflowId const goToUrl = automationId
? `admin.php?page=mailpoet-automation-editor&id=${workflowId}` ? `admin.php?page=mailpoet-automation-editor&id=${automationId}`
: 'admin.php?page=mailpoet-automation'; : 'admin.php?page=mailpoet-automation';
onClick = () => { onClick = () => {
window.location = goToUrl; window.location = goToUrl;

View File

@ -5,7 +5,6 @@ namespace MailPoet\API\MP\v1;
use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder; use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder;
use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberEntity;
use MailPoet\Features\FeaturesController;
use MailPoet\Listing\ListingDefinition; use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\Scheduler\WelcomeScheduler; use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
@ -54,9 +53,6 @@ class Subscribers {
/** @var SubscriberSaveController */ /** @var SubscriberSaveController */
private $subscriberSaveController; private $subscriberSaveController;
/** @var FeaturesController */
private $featuresController;
/** @var RequiredCustomFieldValidator */ /** @var RequiredCustomFieldValidator */
private $requiredCustomFieldsValidator; private $requiredCustomFieldsValidator;
@ -76,7 +72,6 @@ class Subscribers {
SubscriberSaveController $subscriberSaveController, SubscriberSaveController $subscriberSaveController,
SubscribersResponseBuilder $subscribersResponseBuilder, SubscribersResponseBuilder $subscribersResponseBuilder,
WelcomeScheduler $welcomeScheduler, WelcomeScheduler $welcomeScheduler,
FeaturesController $featuresController,
RequiredCustomFieldValidator $requiredCustomFieldsValidator, RequiredCustomFieldValidator $requiredCustomFieldsValidator,
SubscriberListingRepository $subscriberListingRepository, SubscriberListingRepository $subscriberListingRepository,
WPFunctions $wp WPFunctions $wp
@ -90,7 +85,6 @@ class Subscribers {
$this->subscriberSaveController = $subscriberSaveController; $this->subscriberSaveController = $subscriberSaveController;
$this->subscribersResponseBuilder = $subscribersResponseBuilder; $this->subscribersResponseBuilder = $subscribersResponseBuilder;
$this->welcomeScheduler = $welcomeScheduler; $this->welcomeScheduler = $welcomeScheduler;
$this->featuresController = $featuresController;
$this->requiredCustomFieldsValidator = $requiredCustomFieldsValidator; $this->requiredCustomFieldsValidator = $requiredCustomFieldsValidator;
$this->wp = $wp; $this->wp = $wp;
$this->subscriberListingRepository = $subscriberListingRepository; $this->subscriberListingRepository = $subscriberListingRepository;
@ -202,10 +196,7 @@ class Subscribers {
} }
// when global status changes to subscribed, fire subscribed hook for all subscribed segments // when global status changes to subscribed, fire subscribed hook for all subscribed segments
if ( if ($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {
$this->featuresController->isSupported(FeaturesController::AUTOMATION)
&& $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED
) {
$subscriberSegments = $subscriber->getSubscriberSegments(); $subscriberSegments = $subscriber->getSubscriberSegments();
foreach ($subscriberSegments as $subscriberSegment) { foreach ($subscriberSegments as $subscriberSegment) {
if ($subscriberSegment->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) { if ($subscriberSegment->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {

View File

@ -3,10 +3,9 @@
namespace MailPoet\AdminPages\Pages; namespace MailPoet\AdminPages\Pages;
use MailPoet\AdminPages\PageRenderer; use MailPoet\AdminPages\PageRenderer;
use MailPoet\Automation\Engine\Data\WorkflowTemplate; use MailPoet\Automation\Engine\Data\AutomationTemplate;
use MailPoet\Automation\Engine\Migrations\Migrator; use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\Storage\WorkflowStorage; use MailPoet\Automation\Engine\Storage\AutomationTemplateStorage;
use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage;
use MailPoet\Form\AssetsController; use MailPoet\Form\AssetsController;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
@ -14,51 +13,42 @@ class Automation {
/** @var AssetsController */ /** @var AssetsController */
private $assetsController; private $assetsController;
/** @var Migrator */
private $migrator;
/** @var PageRenderer */ /** @var PageRenderer */
private $pageRenderer; private $pageRenderer;
/** @var WPFunctions */ /** @var WPFunctions */
private $wp; private $wp;
/** @var WorkflowStorage */ /** @var AutomationStorage */
private $workflowStorage; private $automationStorage;
/** @var WorkflowTemplateStorage */ /** @var AutomationTemplateStorage */
private $templateStorage; private $templateStorage;
public function __construct( public function __construct(
AssetsController $assetsController, AssetsController $assetsController,
Migrator $migrator,
PageRenderer $pageRenderer, PageRenderer $pageRenderer,
WPFunctions $wp, WPFunctions $wp,
WorkflowStorage $workflowStorage, AutomationStorage $automationStorage,
WorkflowTemplateStorage $templateStorage AutomationTemplateStorage $templateStorage
) { ) {
$this->assetsController = $assetsController; $this->assetsController = $assetsController;
$this->migrator = $migrator;
$this->pageRenderer = $pageRenderer; $this->pageRenderer = $pageRenderer;
$this->wp = $wp; $this->wp = $wp;
$this->workflowStorage = $workflowStorage; $this->automationStorage = $automationStorage;
$this->templateStorage = $templateStorage; $this->templateStorage = $templateStorage;
} }
public function render() { public function render() {
$this->assetsController->setupAutomationListingDependencies(); $this->assetsController->setupAutomationListingDependencies();
if (!$this->migrator->hasSchema()) {
$this->migrator->createSchema();
}
$this->pageRenderer->displayPage('automation.html', [ $this->pageRenderer->displayPage('automation.html', [
'api' => [ 'api' => [
'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'), 'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'),
'nonce' => $this->wp->wpCreateNonce('wp_rest'), 'nonce' => $this->wp->wpCreateNonce('wp_rest'),
], ],
'workflowCount' => $this->workflowStorage->getWorkflowCount(), 'automationCount' => $this->automationStorage->getAutomationCount(),
'templates' => array_map( 'templates' => array_map(
function(WorkflowTemplate $template): array { function(AutomationTemplate $template): array {
return $template->toArray(); return $template->toArray();
}, },
$this->templateStorage->getTemplates() $this->templateStorage->getTemplates()

View File

@ -3,11 +3,11 @@
namespace MailPoet\AdminPages\Pages; namespace MailPoet\AdminPages\Pages;
use MailPoet\AdminPages\PageRenderer; use MailPoet\AdminPages\PageRenderer;
use MailPoet\Automation\Engine\Data\Workflow; use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Hooks; use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\Mappers\WorkflowMapper; use MailPoet\Automation\Engine\Mappers\AutomationMapper;
use MailPoet\Automation\Engine\Registry; use MailPoet\Automation\Engine\Registry;
use MailPoet\Automation\Engine\Storage\WorkflowStorage; use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Form\AssetsController; use MailPoet\Form\AssetsController;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
@ -17,11 +17,11 @@ class AutomationEditor {
/** @var AssetsController */ /** @var AssetsController */
private $assetsController; private $assetsController;
/** @var WorkflowMapper */ /** @var AutomationMapper */
private $workflowMapper; private $automationMapper;
/** @var WorkflowStorage */ /** @var AutomationStorage */
private $workflowStorage; private $automationStorage;
/** @var PageRenderer */ /** @var PageRenderer */
private $pageRenderer; private $pageRenderer;
@ -37,16 +37,16 @@ class AutomationEditor {
public function __construct( public function __construct(
AssetsController $assetsController, AssetsController $assetsController,
WorkflowMapper $workflowMapper, AutomationMapper $automationMapper,
WorkflowStorage $workflowStorage, AutomationStorage $automationStorage,
PageRenderer $pageRenderer, PageRenderer $pageRenderer,
Registry $registry, Registry $registry,
SegmentsRepository $segmentsRepository, SegmentsRepository $segmentsRepository,
WPFunctions $wp WPFunctions $wp
) { ) {
$this->assetsController = $assetsController; $this->assetsController = $assetsController;
$this->workflowMapper = $workflowMapper; $this->automationMapper = $automationMapper;
$this->workflowStorage = $workflowStorage; $this->automationStorage = $automationStorage;
$this->pageRenderer = $pageRenderer; $this->pageRenderer = $pageRenderer;
$this->registry = $registry; $this->registry = $registry;
$this->segmentsRepository = $segmentsRepository; $this->segmentsRepository = $segmentsRepository;
@ -60,8 +60,8 @@ class AutomationEditor {
$this->wp->doAction(Hooks::EDITOR_BEFORE_LOAD, (int)$id); $this->wp->doAction(Hooks::EDITOR_BEFORE_LOAD, (int)$id);
$workflow = $id ? $this->workflowStorage->getWorkflow($id) : null; $automation = $id ? $this->automationStorage->getAutomation($id) : null;
if (!$workflow) { if (!$automation) {
$notice = new WPNotice( $notice = new WPNotice(
WPNotice::TYPE_ERROR, WPNotice::TYPE_ERROR,
__('Automation not found.', 'mailpoet') __('Automation not found.', 'mailpoet')
@ -71,7 +71,7 @@ class AutomationEditor {
return; return;
} }
if ($workflow->getStatus() === Workflow::STATUS_TRASH) { if ($automation->getStatus() === Automation::STATUS_TRASH) {
$this->wp->wpSafeRedirect($this->wp->adminUrl('admin.php?page=mailpoet-automation&status=trash')); $this->wp->wpSafeRedirect($this->wp->adminUrl('admin.php?page=mailpoet-automation&status=trash'));
exit(); exit();
} }
@ -83,7 +83,7 @@ class AutomationEditor {
$roles = new \WP_Roles(); $roles = new \WP_Roles();
$this->pageRenderer->displayPage('automation/editor.html', [ $this->pageRenderer->displayPage('automation/editor.html', [
'context' => $this->buildContext(), 'context' => $this->buildContext(),
'workflow' => $this->workflowMapper->buildWorkflow($workflow), 'automation' => $this->automationMapper->buildAutomation($automation),
'sub_menu' => 'mailpoet-automation', 'sub_menu' => 'mailpoet-automation',
'api' => [ 'api' => [
'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'), 'root' => rtrim($this->wp->escUrlRaw($this->wp->restUrl()), '/'),

View File

@ -3,8 +3,8 @@
namespace MailPoet\AdminPages\Pages; namespace MailPoet\AdminPages\Pages;
use MailPoet\AdminPages\PageRenderer; use MailPoet\AdminPages\PageRenderer;
use MailPoet\Automation\Engine\Data\WorkflowTemplate; use MailPoet\Automation\Engine\Data\AutomationTemplate;
use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage; use MailPoet\Automation\Engine\Storage\AutomationTemplateStorage;
use MailPoet\Form\AssetsController; use MailPoet\Form\AssetsController;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
@ -15,7 +15,7 @@ class AutomationTemplates {
/** @var PageRenderer */ /** @var PageRenderer */
private $pageRenderer; private $pageRenderer;
/** @var WorkflowTemplateStorage */ /** @var AutomationTemplateStorage */
private $templateStorage; private $templateStorage;
/** @var WPFunctions */ /** @var WPFunctions */
@ -24,7 +24,7 @@ class AutomationTemplates {
public function __construct( public function __construct(
AssetsController $assetsController, AssetsController $assetsController,
PageRenderer $pageRenderer, PageRenderer $pageRenderer,
WorkflowTemplateStorage $templateStorage, AutomationTemplateStorage $templateStorage,
WPFunctions $wp WPFunctions $wp
) { ) {
$this->assetsController = $assetsController; $this->assetsController = $assetsController;
@ -45,7 +45,7 @@ class AutomationTemplates {
'nonce' => $this->wp->wpCreateNonce('wp_rest'), 'nonce' => $this->wp->wpCreateNonce('wp_rest'),
], ],
'templates' => array_map( 'templates' => array_map(
function(WorkflowTemplate $template): array { function(AutomationTemplate $template): array {
return $template->toArray(); return $template->toArray();
}, },
$this->templateStorage->getTemplates() $this->templateStorage->getTemplates()

View File

@ -2,13 +2,12 @@
namespace MailPoet\Analytics; namespace MailPoet\Analytics;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\Step; use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Data\Workflow; use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
use MailPoet\Config\ServicesChecker; use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronTrigger; use MailPoet\Cron\CronTrigger;
use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Features\FeaturesController;
use MailPoet\Listing\ListingDefinition; use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\NewslettersRepository; use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository; use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository;
@ -74,11 +73,8 @@ class Reporter {
/** @var SubscriberListingRepository */ /** @var SubscriberListingRepository */
private $subscriberListingRepository; private $subscriberListingRepository;
/** @var FeaturesController */ /** @var AutomationStorage */
private $featuresController; private $automationStorage;
/** @var WorkflowStorage */
private $workflowStorage;
public function __construct( public function __construct(
NewslettersRepository $newslettersRepository, NewslettersRepository $newslettersRepository,
@ -92,8 +88,7 @@ class Reporter {
SubscribersFeature $subscribersFeature, SubscribersFeature $subscribersFeature,
TrackingConfig $trackingConfig, TrackingConfig $trackingConfig,
SubscriberListingRepository $subscriberListingRepository, SubscriberListingRepository $subscriberListingRepository,
FeaturesController $featuresController, AutomationStorage $automationStorage
WorkflowStorage $workflowStorage
) { ) {
$this->newslettersRepository = $newslettersRepository; $this->newslettersRepository = $newslettersRepository;
$this->segmentsRepository = $segmentsRepository; $this->segmentsRepository = $segmentsRepository;
@ -106,8 +101,7 @@ class Reporter {
$this->subscribersFeature = $subscribersFeature; $this->subscribersFeature = $subscribersFeature;
$this->trackingConfig = $trackingConfig; $this->trackingConfig = $trackingConfig;
$this->subscriberListingRepository = $subscriberListingRepository; $this->subscriberListingRepository = $subscriberListingRepository;
$this->featuresController = $featuresController; $this->automationStorage = $automationStorage;
$this->workflowStorage = $workflowStorage;
} }
public function getData() { public function getData() {
@ -231,43 +225,39 @@ class Reporter {
} }
private function automationProperties(): array { private function automationProperties(): array {
if (!$this->featuresController->isSupported(FeaturesController::AUTOMATION)) { $automations = $this->automationStorage->getAutomations();
return []; $activeAutomations = array_filter(
} $automations,
function(Automation $automation): bool {
$workflows = $this->workflowStorage->getWorkflows(); return $automation->getStatus() === Automation::STATUS_ACTIVE;
$activeWorkflows = array_filter(
$workflows,
function(Workflow $workflow): bool {
return $workflow->getStatus() === Workflow::STATUS_ACTIVE;
} }
); );
$activeWorkflowCount = count($activeWorkflows); $activeAutomationCount = count($activeAutomations);
$draftWorkflows = array_filter( $draftAutomations = array_filter(
$workflows, $automations,
function(Workflow $workflow): bool { function(Automation $automation): bool {
return $workflow->getStatus() === Workflow::STATUS_DRAFT; return $automation->getStatus() === Automation::STATUS_DRAFT;
} }
); );
$workflowsWithWordPressUserSubscribesTrigger = array_filter( $automationsWithWordPressUserSubscribesTrigger = array_filter(
$activeWorkflows, $activeAutomations,
function(Workflow $workflow): bool { function(Automation $automation): bool {
return $workflow->getTrigger('mailpoet:wp-user-registered') !== null; return $automation->getTrigger('mailpoet:wp-user-registered') !== null;
} }
); );
$workflowsWithSomeoneSubscribesTrigger = array_filter( $automationsWithSomeoneSubscribesTrigger = array_filter(
$activeWorkflows, $activeAutomations,
function(Workflow $workflow): bool { function(Automation $automation): bool {
return $workflow->getTrigger('mailpoet:someone-subscribes') !== null; return $automation->getTrigger('mailpoet:someone-subscribes') !== null;
} }
); );
$totalSteps = 0; $totalSteps = 0;
$minSteps = null; $minSteps = null;
$maxSteps = 0; $maxSteps = 0;
foreach ($activeWorkflows as $workflow) { foreach ($activeAutomations as $automation) {
$steps = array_filter( $steps = array_filter(
$workflow->getSteps(), $automation->getSteps(),
function(Step $step): bool { function(Step $step): bool {
return $step->getType() === Step::TYPE_ACTION; return $step->getType() === Step::TYPE_ACTION;
} }
@ -277,16 +267,16 @@ class Reporter {
$maxSteps = max($maxSteps, $stepCount); $maxSteps = max($maxSteps, $stepCount);
$totalSteps += $stepCount; $totalSteps += $stepCount;
} }
$averageSteps = $activeWorkflowCount > 0 ? $totalSteps / $activeWorkflowCount : 0; $averageSteps = $activeAutomationCount > 0 ? $totalSteps / $activeAutomationCount : 0;
return [ return [
'Automation > Number of active workflows' => $activeWorkflowCount, 'Automation > Number of active automations' => $activeAutomationCount,
'Automation > Number of draft workflows' => count($draftWorkflows), 'Automation > Number of draft automations' => count($draftAutomations),
'Automation > Number of "WordPress user registers" active workflows' => count($workflowsWithWordPressUserSubscribesTrigger), 'Automation > Number of "WordPress user registers" active automations' => count($automationsWithWordPressUserSubscribesTrigger),
'Automation > Number of "Someone subscribes" active workflows ' => count($workflowsWithSomeoneSubscribesTrigger), 'Automation > Number of "Someone subscribes" active automations ' => count($automationsWithSomeoneSubscribesTrigger),
'Automation > Number of steps in shortest active workflow' => $minSteps, 'Automation > Number of steps in shortest active automation' => $minSteps,
'Automation > Number of steps in longest active workflow' => $maxSteps, 'Automation > Number of steps in longest active automation' => $maxSteps,
'Automation > Average number of steps in active workflows' => $averageSteps, 'Automation > Average number of steps in active automations' => $averageSteps,
]; ];
} }

View File

@ -6,9 +6,7 @@ use MailPoet\API\REST\API as MailPoetApi;
use MailPoet\Automation\Engine\Hooks; use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\WordPress; use MailPoet\Automation\Engine\WordPress;
class API { class API extends MailPoetApi {
private const PREFIX = 'automation/';
/** @var MailPoetApi */ /** @var MailPoetApi */
private $api; private $api;
@ -30,22 +28,22 @@ class API {
} }
public function registerGetRoute(string $route, string $endpoint): void { public function registerGetRoute(string $route, string $endpoint): void {
$this->api->registerGetRoute(self::PREFIX . $route, $endpoint); $this->api->registerGetRoute($route, $endpoint);
} }
public function registerPostRoute(string $route, string $endpoint): void { public function registerPostRoute(string $route, string $endpoint): void {
$this->api->registerPostRoute(self::PREFIX . $route, $endpoint); $this->api->registerPostRoute($route, $endpoint);
} }
public function registerPutRoute(string $route, string $endpoint): void { public function registerPutRoute(string $route, string $endpoint): void {
$this->api->registerPutRoute(self::PREFIX . $route, $endpoint); $this->api->registerPutRoute($route, $endpoint);
} }
public function registerPatchRoute(string $route, string $endpoint): void { public function registerPatchRoute(string $route, string $endpoint): void {
$this->api->registerPatchRoute(self::PREFIX . $route, $endpoint); $this->api->registerPatchRoute($route, $endpoint);
} }
public function registerDeleteRoute(string $route, string $endpoint): void { public function registerDeleteRoute(string $route, string $endpoint): void {
$this->api->registerDeleteRoute(self::PREFIX . $route, $endpoint); $this->api->registerDeleteRoute($route, $endpoint);
} }
} }

View File

@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Builder;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\Storage\AutomationTemplateStorage;
use MailPoet\Automation\Engine\Validation\AutomationValidator;
class CreateAutomationFromTemplateController {
/** @var AutomationStorage */
private $storage;
/** @var AutomationTemplateStorage */
private $templateStorage;
/** @var AutomationValidator */
private $automationValidator;
public function __construct(
AutomationStorage $storage,
AutomationTemplateStorage $templateStorage,
AutomationValidator $automationValidator
) {
$this->storage = $storage;
$this->templateStorage = $templateStorage;
$this->automationValidator = $automationValidator;
}
public function createAutomation(string $slug): Automation {
$template = $this->templateStorage->getTemplateBySlug($slug);
if (!$template) {
throw Exceptions::automationTemplateNotFound($slug);
}
$automation = $template->getAutomation();
$this->automationValidator->validate($automation);
$automationId = $this->storage->createAutomation($automation);
$savedAutomation = $this->storage->getAutomation($automationId);
if (!$savedAutomation) {
throw new InvalidStateException('Automation not found.');
}
return $savedAutomation;
}
}

View File

@ -1,47 +0,0 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Builder;
use MailPoet\Automation\Engine\Data\Workflow;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage;
use MailPoet\Automation\Engine\Validation\WorkflowValidator;
class CreateWorkflowFromTemplateController {
/** @var WorkflowStorage */
private $storage;
/** @var WorkflowTemplateStorage */
private $templateStorage;
/** @var WorkflowValidator */
private $workflowValidator;
public function __construct(
WorkflowStorage $storage,
WorkflowTemplateStorage $templateStorage,
WorkflowValidator $workflowValidator
) {
$this->storage = $storage;
$this->templateStorage = $templateStorage;
$this->workflowValidator = $workflowValidator;
}
public function createWorkflow(string $slug): Workflow {
$template = $this->templateStorage->getTemplateBySlug($slug);
if (!$template) {
throw Exceptions::workflowTemplateNotFound($slug);
}
$workflow = $template->getWorkflow();
$this->workflowValidator->validate($workflow);
$workflowId = $this->storage->createWorkflow($workflow);
$savedWorkflow = $this->storage->getWorkflow($workflowId);
if (!$savedWorkflow) {
throw new InvalidStateException('Automation not found.');
}
return $savedWorkflow;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Builder;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
class DeleteAutomationController {
/** @var AutomationStorage */
private $automationStorage;
public function __construct(
AutomationStorage $automationStorage
) {
$this->automationStorage = $automationStorage;
}
public function deleteAutomation(int $id): Automation {
$automation = $this->automationStorage->getAutomation($id);
if (!$automation) {
throw Exceptions::automationNotFound($id);
}
if ($automation->getStatus() !== Automation::STATUS_TRASH) {
throw Exceptions::automationNotTrashed($id);
}
$this->automationStorage->deleteAutomation($automation);
return $automation;
}
}

View File

@ -1,32 +0,0 @@
<?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\Builder;
use MailPoet\Automation\Engine\Data\Workflow;
use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
class DeleteWorkflowController {
/** @var WorkflowStorage */
private $workflowStorage;
public function __construct(
WorkflowStorage $workflowStorage
) {
$this->workflowStorage = $workflowStorage;
}
public function deleteWorkflow(int $id): Workflow {
$workflow = $this->workflowStorage->getWorkflow($id);
if (!$workflow) {
throw Exceptions::workflowNotFound($id);
}
if ($workflow->getStatus() !== Workflow::STATUS_TRASH) {
throw Exceptions::workflowNotTrashed($id);
}
$this->workflowStorage->deleteWorkflow($workflow);
return $workflow;
}
}

View File

@ -2,55 +2,55 @@
namespace MailPoet\Automation\Engine\Builder; namespace MailPoet\Automation\Engine\Builder;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\NextStep; use MailPoet\Automation\Engine\Data\NextStep;
use MailPoet\Automation\Engine\Data\Step; use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Data\Workflow;
use MailPoet\Automation\Engine\Exceptions; use MailPoet\Automation\Engine\Exceptions;
use MailPoet\Automation\Engine\Exceptions\InvalidStateException; use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
use MailPoet\Automation\Engine\Storage\WorkflowStorage; use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Engine\WordPress; use MailPoet\Automation\Engine\WordPress;
use MailPoet\Util\Security; use MailPoet\Util\Security;
class DuplicateWorkflowController { class DuplicateAutomationController {
/** @var WordPress */ /** @var WordPress */
private $wordPress; private $wordPress;
/** @var WorkflowStorage */ /** @var AutomationStorage */
private $workflowStorage; private $automationStorage;
public function __construct( public function __construct(
WordPress $wordPress, WordPress $wordPress,
WorkflowStorage $workflowStorage AutomationStorage $automationStorage
) { ) {
$this->wordPress = $wordPress; $this->wordPress = $wordPress;
$this->workflowStorage = $workflowStorage; $this->automationStorage = $automationStorage;
} }
public function duplicateWorkflow(int $id): Workflow { public function duplicateAutomation(int $id): Automation {
$workflow = $this->workflowStorage->getWorkflow($id); $automation = $this->automationStorage->getAutomation($id);
if (!$workflow) { if (!$automation) {
throw Exceptions::workflowNotFound($id); throw Exceptions::automationNotFound($id);
} }
$duplicate = new Workflow( $duplicate = new Automation(
$this->getName($workflow->getName()), $this->getName($automation->getName()),
$this->getSteps($workflow->getSteps()), $this->getSteps($automation->getSteps()),
$this->wordPress->wpGetCurrentUser() $this->wordPress->wpGetCurrentUser()
); );
$duplicate->setStatus(Workflow::STATUS_DRAFT); $duplicate->setStatus(Automation::STATUS_DRAFT);
$workflowId = $this->workflowStorage->createWorkflow($duplicate); $automationId = $this->automationStorage->createAutomation($duplicate);
$savedWorkflow = $this->workflowStorage->getWorkflow($workflowId); $savedAutomation = $this->automationStorage->getAutomation($automationId);
if (!$savedWorkflow) { if (!$savedAutomation) {
throw new InvalidStateException('Workflow not found.'); throw new InvalidStateException('Automation not found.');
} }
return $savedWorkflow; return $savedAutomation;
} }
private function getName(string $name): string { private function getName(string $name): string {
// translators: %s is the original automation name. // translators: %s is the original automation name.
$newName = sprintf(__('Copy of %s', 'mailpoet'), $name); $newName = sprintf(__('Copy of %s', 'mailpoet'), $name);
$maxLength = $this->workflowStorage->getNameColumnLength(); $maxLength = $this->automationStorage->getNameColumnLength();
if (strlen($newName) > $maxLength) { if (strlen($newName) > $maxLength) {
$append = '…'; $append = '…';
return substr($newName, 0, $maxLength - strlen($append)) . $append; return substr($newName, 0, $maxLength - strlen($append)) . $append;

Some files were not shown because too many files have changed in this diff Show More