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:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 6.8.2
./do download:woo-commerce-subscriptions-zip 4.5.1
./do download:woo-commerce-memberships-zip 1.23.0
./do download:woo-commerce-blocks-zip 8.4.0
./do download:woo-commerce-zip 7.0.1
./do download:woo-commerce-subscriptions-zip 4.6.0
./do download:woo-commerce-memberships-zip 1.23.1
./do download:woo-commerce-blocks-zip 8.8.2
- run:
name: Dump tests ENV variables for acceptance tests
command: |
@ -749,6 +749,12 @@ workflows:
- js_tests
- integration_test_woocommerce
- 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:
triggers:
@ -773,14 +779,14 @@ workflows:
- acceptance_tests:
<<: *slack-fail-post-step
name: acceptance_oldest
woo_core_version: 6.2.2
woo_core_version: 6.8.0
woo_subscriptions_version: 4.3.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_image_version: 5.7.36
codeception_image_version: 7.4-cli_20210126.1
wordpress_image_version: wp-5.6_php7.2_20220406.1
codeception_image_version: 7.4-cli_20220605.0
wordpress_image_version: wp-5.8_php7.3_20221104.1
requires:
- build
- unit_tests:

View File

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

View File

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

View File

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

View File

@ -119,6 +119,10 @@
color: $color-stats-average;
}
.mailpoet-statistics-value-number-critical {
color: $color-stats-critical;
}
.mailpoet-statistics-value-number-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 {
border-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-trigger';
@import './components-automation-editor/automation';
@import './components-automation-editor/block-icon';
@import './components-automation-editor/chip';
@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/panel';
@import './components-automation-editor/separator';
@import './components-automation-editor/status';
@import './components-automation-editor/step';
@import './components-automation-editor/step-card';
@import './components-automation-editor/workflow';
@import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal';

View File

@ -17,17 +17,30 @@ ul.mailpoet-automation-templates {
.mailpoet-automation-template-list-item {
button.components-button {
align-content: baseline;
align-items: flex-start;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
display: flex;
flex-direction: column;
display: grid;
grid-template-rows: 40px auto auto;
height: 100%;
padding: 24px 24px 26px;
text-align: left;
width: 100%;
&:disabled,
&[aria-disabled='true'] {
color: #787c82;
cursor: not-allowed;
opacity: 1;
h2 {
color: #787c82;
}
}
&:hover {
background: #fff;
border: 1px solid #dcdcde;
@ -49,7 +62,6 @@ ul.mailpoet-automation-templates {
h2 {
background: transparent;
border: none;
color: #2271b1;
font-size: 14px;
font-weight: 600;
line-height: 21px;
@ -78,4 +90,27 @@ ul.mailpoet-automation-templates {
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-good: #ff9f00;
$color-stats-excellent: #7ed321;
$color-stats-critical: #f00;
$color-stats-unknown: $color-primary-inactive;
// 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 { api } from '../config';
export * from './hooks';
const apiUrl = `${api.root}/mailpoet/v1/automation/`;
const apiUrl = `${api.root}/mailpoet/v1/`;
export type ApiError = {
code?: string;

View File

@ -1,24 +1,33 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { TopBarWithBeamer } from 'common/top_bar/top_bar';
import { Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { initializeApi, useMutation } from './api';
import { initializeApi } from './api';
import { registerTranslations } from './i18n';
import { createStore, storeName } from './listing/store';
import { AutomationListing, AutomationListingHeader } from './listing';
import { registerApiErrorHandler } from './listing/api-error-handler';
import { Notices } from './listing/components/notices';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { BuildYourOwnSection, HeroSection, TemplatesSection } from './sections';
import {
CreateEmptyWorkflowButton,
CreateWorkflowFromTemplateButton,
} from './testing';
import { MailPoet } from '../mailpoet';
const trackOpenEvent = () => {
MailPoet.trackEvent('Automations > Listing viewed');
};
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 =
count > 0 ? (
<>
@ -49,7 +58,7 @@ function Content(): JSX.Element {
);
}
function Workflows(): JSX.Element {
function Automations(): JSX.Element {
return (
<>
<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 {
return (
<SlotFillProvider>
<BrowserRouter>
<div>
<Workflows />
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
<CreateEmptyWorkflowButton />
<CreateWorkflowFromTemplateButton slug="simple-welcome-email">
Create testing workflow from template (welcome email)
</CreateWorkflowFromTemplateButton>
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
Create testing workflow from template (welcome sequence, only
premium)
</CreateWorkflowFromTemplateButton>
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
Create testing workflow from template (advanced welcome sequence,
only premium)
</CreateWorkflowFromTemplateButton>
<RecreateSchemaButton />
<DeleteSchemaButton />
</div>
<Popover.Slot />
</div>
<Automations />
<Popover.Slot />
</BrowserRouter>
</SlotFillProvider>
);

View File

@ -4,9 +4,9 @@ declare global {
root: string;
nonce: string;
};
mailpoet_workflow_count: number;
mailpoet_automation_count: number;
}
}
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 code = errorObject.code;
if (code === 'mailpoet_automation_workflow_not_valid') {
if (code === 'mailpoet_automation_not_valid') {
dispatch(storeName).setErrors({ steps: errorObject.data.errors });
return undefined;
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components';
import { createContext } from '@wordpress/element';
export const WorkflowCompositeContext =
export const AutomationCompositeContext =
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 { Icon, check } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks';
import { WorkflowCompositeContext } from './context';
import { EmptyWorkflow } from './empty-workflow';
import { AutomationCompositeContext } from './context';
import { EmptyAutomation } from './empty-automation';
import { Separator } from './separator';
import { Step } from './step';
import { Step as StepData } from './types';
@ -21,10 +21,10 @@ import {
RenderStepType,
} from '../../../types/filters';
export function Workflow(): JSX.Element {
const { workflowData, selectedStep } = useSelect(
export function Automation(): JSX.Element {
const { automationData, selectedStep } = useSelect(
(select) => ({
workflowData: select(storeName).getWorkflowData(),
automationData: select(storeName).getAutomationData(),
selectedStep: select(storeName).getSelectedStep(),
}),
[],
@ -36,9 +36,9 @@ export function Workflow(): JSX.Element {
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 stepArray = [stepMap.root];
@ -56,7 +56,7 @@ export function Workflow(): JSX.Element {
const renderStep = useMemo(
(): RenderStepType =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step',
'mailpoet.automation.render_step',
(stepData: StepData) =>
stepData.type === 'root' ? (
<AddTrigger step={stepData} />
@ -73,7 +73,7 @@ export function Workflow(): JSX.Element {
const renderSeparator = useMemo(
(): RenderStepSeparatorType =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step_separator',
'mailpoet.automation.render_step_separator',
(previousStepData: StepData) => (
<Separator previousStepId={previousStepData.id} />
),
@ -81,20 +81,20 @@ export function Workflow(): JSX.Element {
[],
);
if (!workflowData) {
return <EmptyWorkflow />;
if (!automationData) {
return <EmptyAutomation />;
}
return (
<WorkflowCompositeContext.Provider value={compositeState}>
<AutomationCompositeContext.Provider value={compositeState}>
<Composite
state={compositeState}
role="tree"
aria-label={__('Automation', 'mailpoet')}
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 />
{stepMap.root.next_steps.length === 0 ? (
<>
@ -119,13 +119,13 @@ export function Workflow(): JSX.Element {
</Fragment>
))}
<Icon
className="mailpoet-automation-editor-workflow-end"
className="mailpoet-automation-editor-automation-end"
icon={check}
/>
<div />
</div>
<InserterPopover />
</Composite>
</WorkflowCompositeContext.Provider>
</AutomationCompositeContext.Provider>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import { Errors } from './errors';
import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
import {
DeactivateImmediatelyModal,
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/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton({ onClick, label }): JSX.Element {
function ActivateButton({ label }): JSX.Element {
const { errors, isDeactivating } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
isDeactivating:
select(storeName).getWorkflowData().status ===
WorkflowStatus.DEACTIVATING,
select(storeName).getAutomationData().status ===
AutomationStatus.DEACTIVATING,
}),
[],
);
const { openActivationPanel } = useDispatch(storeName);
const button = (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={onClick}
onClick={openActivationPanel}
disabled={isDeactivating || !!errors}
>
{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.
delay={0}
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',
)}
>
@ -68,14 +69,14 @@ function ActivateButton({ onClick, label }): JSX.Element {
function UpdateButton(): JSX.Element {
const { save } = useDispatch(storeName);
const { workflow } = useSelect(
const { automation } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
if (workflow.stats.totals.in_progress === 0) {
if (automation.stats.totals.in_progress === 0) {
return (
<Button
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.
delay={0}
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',
)}
>
@ -125,7 +126,7 @@ function DeactivateButton(): JSX.Element {
const { hasUsersInProgress } = useSelect(
(select) => ({
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(
(select) => ({
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 = {
showInserterToggle: boolean;
toggleActivatePanel: () => void;
};
export function Header({
showInserterToggle,
toggleActivatePanel,
}: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect(
export function Header({ showInserterToggle }: Props): JSX.Element {
const { setAutomationName } = useDispatch(storeName);
const { automationName, automationStatus } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
workflowStatus: select(storeName).getWorkflowData().status,
automationName: select(storeName).getAutomationData().name,
automationStatus: select(storeName).getAutomationData().status,
}),
[],
);
@ -237,8 +234,8 @@ export function Header({
{__('Automation name', 'mailpoet')}
</div>
<TextControl
value={workflowName}
onChange={(newName) => setWorkflowName(newName)}
value={automationName}
onChange={(newName) => setAutomationName(newName)}
help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet',
@ -252,28 +249,22 @@ export function Header({
<div className="edit-site-header_end">
<div className="edit-site-header__actions">
<Errors />
{workflowStatus === WorkflowStatus.DRAFT && (
{automationStatus === AutomationStatus.DRAFT && (
<>
<SaveDraftButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Activate', 'mailpoet')}
/>
<ActivateButton label={__('Activate', 'mailpoet')} />
</>
)}
{workflowStatus === WorkflowStatus.ACTIVE && (
{automationStatus === AutomationStatus.ACTIVE && (
<>
<DeactivateButton />
<UpdateButton />
</>
)}
{workflowStatus === WorkflowStatus.DEACTIVATING && (
{automationStatus === AutomationStatus.DEACTIVATING && (
<>
<DeactivateNowButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Update & Activate', 'mailpoet')}
/>
<ActivateButton label={__('Update & Activate', 'mailpoet')} />
</>
)}
<PinnedItems.Slot scope={storeName} />

View File

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

View File

@ -5,7 +5,7 @@ import {
store as keyboardShortcutsStore,
} from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// 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 {
const sidebarToOpen = selectedStep()
? stepSidebarKey
: workflowSidebarKey;
: automationSidebarKey;
openSidebar(sidebarToOpen);
}
});

View File

@ -3,7 +3,7 @@ import { Button, Modal } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
type DeactivateImmediatelyModalProps = {
onClose: () => void;
@ -49,20 +49,20 @@ type DeactivateModalProps = {
export function DeactivateModal({
onClose,
}: DeactivateModalProps): JSX.Element {
const { workflowName } = useSelect(
const { automationName } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
automationName: select(storeName).getAutomationData().name,
}),
[],
);
const [selected, setSelected] = useState<
WorkflowStatus.DRAFT | WorkflowStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING);
AutomationStatus.DRAFT | AutomationStatus.DEACTIVATING
>(AutomationStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation.
const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'),
workflowName,
automationName,
);
return (
@ -79,7 +79,7 @@ export function DeactivateModal({
<li>
<label
className={
selected === WorkflowStatus.DEACTIVATING
selected === AutomationStatus.DEACTIVATING
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
@ -89,8 +89,8 @@ export function DeactivateModal({
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
checked={selected === AutomationStatus.DEACTIVATING}
onChange={() => setSelected(AutomationStatus.DEACTIVATING)}
/>
</span>
<span>
@ -107,7 +107,7 @@ export function DeactivateModal({
<li>
<label
className={
selected === WorkflowStatus.DRAFT
selected === AutomationStatus.DRAFT
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
@ -117,8 +117,8 @@ export function DeactivateModal({
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DRAFT}
onChange={() => setSelected(WorkflowStatus.DRAFT)}
checked={selected === AutomationStatus.DRAFT}
onChange={() => setSelected(AutomationStatus.DRAFT)}
/>
</span>
<span>
@ -140,7 +140,7 @@ export function DeactivateModal({
onClick={() => {
setIsBusy(true);
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 { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { AutomationStatus } from '../../../listing/automation';
import { MailPoet } from '../../../../mailpoet';
function PreStep({ onClose }): JSX.Element {
@ -58,9 +58,9 @@ function PreStep({ onClose }): JSX.Element {
}
function PostStep({ onClose }): JSX.Element {
const { workflow } = useSelect(
const { automation } = useSelect(
(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__section">
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)}
{sprintf(__('"%s" is now live.', 'mailpoet'), automation.name)}
</div>
<p>
<strong>{__('Whats next?', 'mailpoet')}</strong>
@ -100,29 +100,31 @@ function PostStep({ onClose }): JSX.Element {
);
}
export function ActivatePanel({ onClose }): JSX.Element {
const { workflow, errors } = useSelect(
export function ActivatePanel(): JSX.Element {
const { automation, errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
const { closeActivationPanel } = useDispatch(storeName);
useEffect(() => {
if (errors) {
onClose();
closeActivationPanel();
}
}, [errors, onClose]);
}, [errors, closeActivationPanel]);
if (errors) {
return null;
}
const isActive = workflow.status === WorkflowStatus.ACTIVE;
const isActive = automation.status === AutomationStatus.ACTIVE;
return (
<div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />}
{!isActive && <PreStep onClose={onClose} />}
{isActive && <PostStep onClose={closeActivationPanel} />}
{!isActive && <PreStep onClose={closeActivationPanel} />}
</div>
);
}

View File

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

View File

@ -1,7 +1,7 @@
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// 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 {
const { openSidebar } = useDispatch(storeName);
const openWorkflowSettings = () => openSidebar(workflowSidebarKey);
const openAutomationSettings = () => openSidebar(automationSidebarKey);
const openStepSettings = () => openSidebar(stepSidebarKey);
const [workflowAriaLabel, workflowActiveClass] =
sidebarKey === workflowSidebarKey
const [automationAriaLabel, automationActiveClass] =
sidebarKey === automationSidebarKey
? [__('Automation (selected)', 'mailpoet'), 'is-active']
: [__('Automation', 'mailpoet'), ''];
@ -30,9 +30,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
<ul>
<li>
<Button
onClick={openWorkflowSettings}
className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`}
aria-label={workflowAriaLabel}
onClick={openAutomationSettings}
className={`edit-site-sidebar__panel-tab ${automationActiveClass}`}
aria-label={automationAriaLabel}
data-label={__('Automation', 'mailpoet')}
>
{__('Automation', 'mailpoet')}

View File

@ -10,8 +10,8 @@ import {
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { Header } from './header';
import { StepSidebar } from './step';
import { WorkflowSidebar } from './workflow';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
import { AutomationSidebar } from './automation';
import { stepSidebarKey, storeName, automationSidebarKey } from '../../store';
// See:
// 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>;
export function Sidebar(props: Props): JSX.Element {
const { keyboardShortcut, sidebarKey, showIconLabels, workflowName } =
const { keyboardShortcut, sidebarKey, showIconLabels, automationName } =
useSelect(
(select) => ({
keyboardShortcut: select(
@ -36,9 +36,9 @@ export function Sidebar(props: Props): JSX.Element {
),
sidebarKey:
select(interfaceStore).getActiveComplementaryArea(storeName) ??
workflowSidebarKey,
automationSidebarKey,
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}
className="edit-site-sidebar mailpoet-automation-sidebar"
panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)', 'mailpoet')}
smallScreenTitle={automationName || __('(no title)', 'mailpoet')}
scope={storeName}
toggleShortcut={keyboardShortcut}
isActiveByDefault={sidebarActiveByDefault}
showIconLabels={showIconLabels}
{...props}
>
{sidebarKey === workflowSidebarKey && <WorkflowSidebar />}
{sidebarKey === automationSidebarKey && <AutomationSidebar />}
{sidebarKey === stepSidebarKey && <StepSidebar />}
</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 { EditorNotices } from './components/notices';
import { Sidebar } from './components/sidebar';
import { Workflow } from './components/workflow';
import { Automation } from './components/automation';
import { createStore, storeName } from './store';
import { initializeApi } from '../api';
import { initialize as initializeCoreIntegration } from '../integrations/core';
import { initialize as initializeMailPoetIntegration } from '../integrations/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 { ActivatePanel } from './components/panel/activate-panel';
import { registerTranslations } from '../i18n';
import { WorkflowStatus } from '../listing/workflow';
import { AutomationStatus } from '../listing/automation';
// See:
// 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;
/**
* Show temporary message that active workflows cant be updated
* Show temporary message that active automations cant be updated
*
* see MAILPOET-4744
*/
function updatingActiveWorkflowNotPossible() {
const workflow = globalSelect(storeName).getWorkflowData();
function updatingActiveAutomationNotPossible() {
const automation = globalSelect(storeName).getAutomationData();
if (
![WorkflowStatus.ACTIVE, WorkflowStatus.DEACTIVATING].includes(
workflow.status,
![AutomationStatus.ACTIVE, AutomationStatus.DEACTIVATING].includes(
automation.status,
)
) {
return;
}
if (workflow.stats.totals.in_progress === 0) {
if (automation.stats.totals.in_progress === 0) {
return;
}
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
void createNotice(
'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',
),
{
@ -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 {
const {
isFullscreenActive,
isInserterOpened,
isActivationPanelOpened,
isSidebarOpened,
showIconLabels,
workflow,
automation,
} = useSelect(
(select) => ({
isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'),
isInserterOpened: select(storeName).isInserterSidebarOpened(),
isSidebarOpened: select(storeName).isSidebarOpened(),
isActivationPanelOpened: select(storeName).isActivationPanelOpened(),
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
workflow: select(storeName).getWorkflowData(),
automation: select(storeName).getAutomationData(),
}),
[],
);
const [showActivatePanel, setShowActivatePanel] = useState(false);
const [isBooting, setIsBooting] = useState(true);
useConfirmUnsaved();
useEffect(() => {
if (!isBooting) {
return;
}
updatingActiveWorkflowNotPossible();
updatingActiveAutomationNotPossible();
setIsBooting(false);
}, [isBooting]);
const className = classnames('interface-interface-skeleton', {
@ -104,17 +126,13 @@ function Editor(): JSX.Element {
'show-icon-labels': showIconLabels,
});
if (workflow.status === 'trash') {
if (automation.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted]: workflow.id,
[LISTING_NOTICE_PARAMETERS.automationHadBeenDeleted]: automation.id,
});
return null;
}
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return (
<ShortcutProvider>
<SlotFillProvider>
@ -135,16 +153,11 @@ function Editor(): JSX.Element {
</div>
)
}
header={
<Header
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
header={<Header showInserterToggle={showInserterSidebar} />}
content={
<>
<EditorNotices />
<Workflow />
<Automation />
</>
}
sidebar={<ComplementaryArea.Slot scope={storeName} />}
@ -152,7 +165,7 @@ function Editor(): JSX.Element {
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
}
/>
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
{isActivationPanelOpened && <ActivatePanel />}
<Popover.Slot />
</SlotFillProvider>
</ShortcutProvider>

View File

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

View File

@ -1,4 +1,4 @@
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,11 +31,11 @@ export function EditNewsletter(): JSX.Element {
useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const { selectedStep, workflowId, workflowSaved, errors } = useSelect(
const { selectedStep, automationId, automationSaved, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id,
workflowSaved: select(storeName).getWorkflowSaved(),
automationId: select(storeName).getAutomationData().id,
automationSaved: select(storeName).getAutomationSaved(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
@ -44,7 +44,7 @@ export function EditNewsletter(): JSX.Element {
);
const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id;
const automationStepId = selectedStep.id;
const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? '';
@ -58,28 +58,28 @@ export function EditNewsletter(): JSX.Element {
type: 'automation',
subject: '',
options: {
workflowId,
workflowStepId,
automationId,
automationStepId,
},
},
});
dispatch(storeName).updateStepArgs(
workflowStepId,
automationStepId,
'email_id',
parseInt(response.data.id as string, 10),
);
dispatch(storeName).save();
}, [workflowId, workflowStepId]);
}, [automationId, automationStepId]);
// 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(() => {
if (redirectToTemplateSelection && emailId && workflowSaved) {
if (redirectToTemplateSelection && emailId && automationSaved) {
window.location.href = `admin.php?page=mailpoet-newsletters#/template/${emailId}`;
}
}, [emailId, workflowSaved, redirectToTemplateSelection]);
}, [emailId, automationSaved, redirectToTemplateSelection]);
if (!emailId || redirectToTemplateSelection) {
return (

View File

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

View File

@ -3,7 +3,7 @@ import { Hooks } from 'wp-js-hooks';
import { Icon } from './icon';
import { Edit } from './edit';
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 = {
key: 'mailpoet:send-email',
@ -20,6 +20,6 @@ export const step: StepType = {
Hooks.applyFilters(
'mailpoet.automation.send_email.create_step',
stepData,
state.workflowData.id,
state.automationData.id,
),
} as const;

View File

@ -2,10 +2,13 @@
* The types in this file document the expected return types of specific
* filters.
*/
import { Step } from '../../../editor/components/workflow/types';
import { Step } from '../../../editor/components/automation/types';
// 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
export type GoogleAnalyticsPanelBodyType = JSX.Element;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import { _x } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
import { Automation } from '../../automation';
import { Statistics } from '../../../components/statistics';
type Props = {
workflow: Workflow;
automation: Automation;
};
export function Subscribers({ workflow }: Props): JSX.Element {
export function Subscribers({ automation }: Props): JSX.Element {
return (
<Statistics
labelPosition="after"
@ -15,19 +15,19 @@ export function Subscribers({ workflow }: Props): JSX.Element {
key: 'entered',
// translators: Total number of subscribers who entered an automation
label: _x('Entered', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.entered,
value: automation.stats.totals.entered,
},
{
key: 'processing',
// translators: Total number of subscribers who are being processed in an automation
label: _x('Processing', 'automation stats', 'mailpoet'),
value: workflow.stats.totals.in_progress,
value: automation.stats.totals.in_progress,
},
{
key: 'exited',
// translators: Total number of subscribers who exited an automation, no matter the result
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 { Item } from './item';
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 { deleteWorkflow } = useDispatch(storeName);
const { deleteAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
if (automation.status !== AutomationStatus.TRASH) {
return undefined;
}
@ -27,7 +27,7 @@ export const useDeleteButton = (workflow: Workflow): Item | undefined => {
title={__('Permanently delete automation', 'mailpoet')}
confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => deleteWorkflow(workflow)}
onConfirm={() => deleteAutomation(automation)}
onCancel={() => setShowDialog(false)}
>
{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!',
'mailpoet',
),
workflow.name,
automation.name,
)}
</ConfirmDialog>
),

View File

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

View File

@ -2,12 +2,12 @@ import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
import { Automation, AutomationStatus } from '../../automation';
export const useRestoreButton = (workflow: Workflow): Item | undefined => {
const { restoreWorkflow } = useDispatch(storeName);
export const useRestoreButton = (automation: Automation): Item | undefined => {
const { restoreAutomation } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
if (automation.status !== AutomationStatus.TRASH) {
return undefined;
}
@ -16,7 +16,7 @@ export const useRestoreButton = (workflow: Workflow): Item | undefined => {
control: {
title: __('Restore', 'mailpoet'),
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 { Item } from './item';
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 { trashWorkflow } = useDispatch(storeName);
const { trashAutomation } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
if (automation.status === AutomationStatus.TRASH) {
return undefined;
}
@ -27,7 +27,7 @@ export const useTrashButton = (workflow: Workflow): Item | undefined => {
title={__('Trash automation', 'mailpoet')}
confirmButtonText={__('Yes, move to trash', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => trashWorkflow(workflow)}
onConfirm={() => trashAutomation(automation)}
onCancel={() => setShowDialog(false)}
>
{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?',
'mailpoet',
),
workflow.name,
automation.name,
)}
</ConfirmDialog>
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Workflow } from '../workflow';
import { Automation } from '../automation';
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`,
},
{
slug: 'customize-your-workflow',
slug: 'customize-your-automation',
title: __('Customize your automation', 'mailpoet'),
text: __(
'Choose steps and create a custom journey to best suit your needs.',
'mailpoet',
),
image: `${MailPoet.cdnUrl}automation/sections/customize-your-workflow.png`,
image: `${MailPoet.cdnUrl}automation/sections/customize-your-automation.png`,
},
{
slug: 'design-your-email',

View File

@ -1,11 +1,11 @@
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { MailPoet } from '../../mailpoet';
import { workflowTemplates } from '../templates/config';
import { automationTemplates } from '../templates/config';
import { TemplateListItem } from '../templates/components/template-list-item';
export function TemplatesSection(): JSX.Element {
const templates = workflowTemplates.slice(0, 3);
const templates = automationTemplates.slice(0, 3);
return (
<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 { addQueryArgs } from '@wordpress/url';
import { __ } from '@wordpress/i18n';
import { WorkflowTemplate } from '../config';
import { useMutation } from '../../api';
import { AutomationTemplate } from '../config';
import { MailPoet } from '../../../mailpoet';
import { Notice } from '../../../notices/notice';
import {
PremiumModal,
premiumValidAndActive,
} from '../../../common/premium_modal';
type TemplateListItemProps = {
template: WorkflowTemplate;
template: AutomationTemplate;
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({
template,
heading,
}: TemplateListItemProps): JSX.Element {
const [createWorkflowFromTemplate, { loading, error, data }] = useMutation(
'workflows/create-from-template',
{
method: 'POST',
body: JSON.stringify({
slug: template.slug,
}),
},
);
const [showPremium, setShowPremium] = useState(false);
const [createAutomationFromTemplate, { loading, error, data }] =
useCreateFromTemplate();
if (!error && data) {
MailPoet.trackEvent('Automations > Template selected', {
'Automation slug': template.slug,
});
window.location.href = addQueryArgs(MailPoet.urls.automationEditor, {
id: data.data.id,
});
@ -45,19 +74,50 @@ export function TemplateListItem({
const headingTag = heading ?? 'h2';
return (
<li className="mailpoet-automation-template-list-item">
<li
className={`mailpoet-automation-template-list-item mailpoet-automation-template-list-item-${template.type}`}
>
{notice}
<Button
isBusy={loading}
disabled={template.type === 'coming-soon'}
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 === 'h2' && <h2>{template.name}&nbsp;</h2>}
<p>{template.description}</p>
</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>
);
}

View File

@ -1,13 +1,14 @@
export type WorkflowTemplate = {
export type AutomationTemplate = {
slug: string;
name: string;
description: string;
type: 'default' | 'free-only' | 'premium' | 'coming-soon';
};
declare global {
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 { __ } from '@wordpress/i18n';
import { Flex } from '@wordpress/components';
import { workflowTemplates } from './config';
import { automationTemplates } from './config';
import { TemplateListItem } from './components/template-list-item';
import { initializeApi } from '../api';
import { registerTranslations } from '../i18n';
@ -23,7 +23,7 @@ function Templates(): JSX.Element {
</Flex>
<ul className="mailpoet-automation-templates">
{workflowTemplates.map((template) => (
{automationTemplates.map((template) => (
<TemplateListItem key={template.slug} template={template} />
))}
<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 { StoreConfig } from '@wordpress/data';
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';
export type MoreControlType = {
@ -20,17 +20,17 @@ export type MoreControlType = {
* APPLICATION HOOKS
*/
// mailpoet.automation.workflow.step.more-controls
// mailpoet.automation.step.more-controls
// mailpoet.automation.hero.actions
export type StepMoreControlsType = Record<string, MoreControlType>;
// mailpoet.automation.workflow.add_step_callback
// mailpoet.automation.add_step_callback
export type AddStepCallbackType = (item?: Item) => void;
// mailpoet.automation.workflow.render_step
// mailpoet.automation.render_step
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;
// mailpoet.automation.editor.create_store

View File

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

View File

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

View File

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

View File

@ -13,21 +13,39 @@ type StatsBadgeProps = {
const stats = {
opened: {
badgeRanges: [30, 10, 0],
badgeTypes: ['excellent', 'good', 'average'],
tooltipText: [
MailPoet.I18n.t('openedStatTooltipExcellent'),
MailPoet.I18n.t('openedStatTooltipGood'),
MailPoet.I18n.t('openedStatTooltipAverage'),
],
badgeTypes: ['excellent', 'good', 'critical'],
tooltipText: {
excellent: MailPoet.I18n.t('openedStatTooltipExcellent'),
good: MailPoet.I18n.t('openedStatTooltipGood'),
critical: MailPoet.I18n.t('openedStatTooltipCritical'),
},
},
clicked: {
badgeRanges: [3, 1, 0],
badgeTypes: ['excellent', 'good', 'average'],
tooltipText: [
MailPoet.I18n.t('clickedStatTooltipExcellent'),
MailPoet.I18n.t('clickedStatTooltipGood'),
MailPoet.I18n.t('clickedStatTooltipAverage'),
],
badgeTypes: ['excellent', 'good', 'critical'],
tooltipText: {
excellent: MailPoet.I18n.t('clickedStatTooltipExcellent'),
good: MailPoet.I18n.t('clickedStatTooltipGood'),
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'),
tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip'),
},
average: {
name: MailPoet.I18n.t('averageBadgeName'),
tooltipTitle: MailPoet.I18n.t('averageBadgeTooltip'),
critical: {
name: MailPoet.I18n.t('criticalBadgeName'),
tooltipTitle: MailPoet.I18n.t('criticalBadgeTooltip'),
},
};
@ -86,15 +104,15 @@ function StatsBadge(props: StatsBadgeProps) {
<div className="mailpoet-listing-stats-tooltip-content">
<Badge type="excellent" name={badges.excellent.name} />
{' : '}
{stat.tooltipText[0]}
{stat.tooltipText.excellent}
<br />
<Badge type="good" name={badges.good.name} />
{' : '}
{stat.tooltipText[1]}
{stat.tooltipText.good}
<br />
<Badge type="average" name={badges.average.name} />
<Badge type="critical" name={badges.critical.name} />
{' : '}
{stat.tooltipText[2]}
{stat.tooltipText.critical}
</div>
</div>
);

View File

@ -22,7 +22,8 @@ import {
UtmParams,
} from './upgrade_info';
const premiumValidAndActive = premiumFeaturesEnabled && MailPoet.premiumActive;
export const premiumValidAndActive =
premiumFeaturesEnabled && MailPoet.premiumActive;
type Props = Omit<ComponentProps<typeof Modal>, 'title' | 'onRequestClose'> & {
// 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
</Tag>
<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">
WordPress
</Tag>

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,9 @@ type Props = {
const minNewslettersSent = 20;
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
const formatWithOptimalPrecision = (value: number) => {
@ -24,6 +27,16 @@ const formatWithOptimalPrecision = (value: number) => {
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({
newsletter,
isWoocommerceActive,
@ -61,14 +74,35 @@ export function NewsletterGeneralStats({
totalSent >= minNewslettersSent &&
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 = (
<div className="mailpoet-statistics-value-small">
<span className="mailpoet-statistics-value-number">
{percentageOpenedDisplay}
{'% '}
</span>
{MailPoet.I18n.t('percentageOpened')}
</div>
<>
<div className="mailpoet-statistics-value-small">
<span
className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeOpened}`}
>
{percentageOpenedDisplay}
{'% '}
</span>
{MailPoet.I18n.t('percentageOpened')}
</div>
{displayBadges && (
<StatsBadge
isInverted={false}
stat="opened"
rate={percentageOpened}
tooltipId={`opened-${newsletter.id || '0'}`}
tooltipPlace="right"
/>
)}
</>
);
const machineOpened = (
@ -100,24 +134,60 @@ export function NewsletterGeneralStats({
</div>
);
const formattedPercentageUnsubscribed = formatForStats(
percentageUnsubscribed,
);
const badgeTypeUnsubscribed = displayUnsubscribedBadge
? getBadgeType('unsubscribed', formattedPercentageUnsubscribed)
: '';
const unsubscribed = (
<div className="mailpoet-statistics-value-small">
<span className="mailpoet-statistics-value-number">
{percentageUnsubscribedDisplay}
{'% '}
</span>
{MailPoet.I18n.t('percentageUnsubscribed')}
</div>
<>
<div className="mailpoet-statistics-value-small">
<span
className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeUnsubscribed}`}
>
{percentageUnsubscribedDisplay}
{'% '}
</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 = (
<div className="mailpoet-statistics-value-small">
<span className="mailpoet-statistics-value-number">
{percentageBouncedDisplay}
{'% '}
</span>
{MailPoet.I18n.t('percentageBounced')}
</div>
<>
<div className="mailpoet-statistics-value-small">
<span
className={`mailpoet-statistics-value-number mailpoet-statistics-value-number-${badgeTypeBounced}`}
>
{percentageBouncedDisplay}
{'% '}
</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);

View File

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

View File

@ -203,11 +203,11 @@ class NewsletterSendComponent extends Component {
: null;
const item = response.data;
// 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') {
const workflowId = item.options?.workflowId;
const goToUrl = workflowId
? `admin.php?page=mailpoet-automation-editor&id=${workflowId}`
const automationId = item.options?.automationId;
const goToUrl = automationId
? `admin.php?page=mailpoet-automation-editor&id=${automationId}`
: '/new';
return this.setState(
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,12 @@
namespace MailPoet\Analytics;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\Step;
use MailPoet\Automation\Engine\Data\Workflow;
use MailPoet\Automation\Engine\Storage\WorkflowStorage;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronTrigger;
use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Features\FeaturesController;
use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository;
@ -74,11 +73,8 @@ class Reporter {
/** @var SubscriberListingRepository */
private $subscriberListingRepository;
/** @var FeaturesController */
private $featuresController;
/** @var WorkflowStorage */
private $workflowStorage;
/** @var AutomationStorage */
private $automationStorage;
public function __construct(
NewslettersRepository $newslettersRepository,
@ -92,8 +88,7 @@ class Reporter {
SubscribersFeature $subscribersFeature,
TrackingConfig $trackingConfig,
SubscriberListingRepository $subscriberListingRepository,
FeaturesController $featuresController,
WorkflowStorage $workflowStorage
AutomationStorage $automationStorage
) {
$this->newslettersRepository = $newslettersRepository;
$this->segmentsRepository = $segmentsRepository;
@ -106,8 +101,7 @@ class Reporter {
$this->subscribersFeature = $subscribersFeature;
$this->trackingConfig = $trackingConfig;
$this->subscriberListingRepository = $subscriberListingRepository;
$this->featuresController = $featuresController;
$this->workflowStorage = $workflowStorage;
$this->automationStorage = $automationStorage;
}
public function getData() {
@ -231,43 +225,39 @@ class Reporter {
}
private function automationProperties(): array {
if (!$this->featuresController->isSupported(FeaturesController::AUTOMATION)) {
return [];
}
$workflows = $this->workflowStorage->getWorkflows();
$activeWorkflows = array_filter(
$workflows,
function(Workflow $workflow): bool {
return $workflow->getStatus() === Workflow::STATUS_ACTIVE;
$automations = $this->automationStorage->getAutomations();
$activeAutomations = array_filter(
$automations,
function(Automation $automation): bool {
return $automation->getStatus() === Automation::STATUS_ACTIVE;
}
);
$activeWorkflowCount = count($activeWorkflows);
$draftWorkflows = array_filter(
$workflows,
function(Workflow $workflow): bool {
return $workflow->getStatus() === Workflow::STATUS_DRAFT;
$activeAutomationCount = count($activeAutomations);
$draftAutomations = array_filter(
$automations,
function(Automation $automation): bool {
return $automation->getStatus() === Automation::STATUS_DRAFT;
}
);
$workflowsWithWordPressUserSubscribesTrigger = array_filter(
$activeWorkflows,
function(Workflow $workflow): bool {
return $workflow->getTrigger('mailpoet:wp-user-registered') !== null;
$automationsWithWordPressUserSubscribesTrigger = array_filter(
$activeAutomations,
function(Automation $automation): bool {
return $automation->getTrigger('mailpoet:wp-user-registered') !== null;
}
);
$workflowsWithSomeoneSubscribesTrigger = array_filter(
$activeWorkflows,
function(Workflow $workflow): bool {
return $workflow->getTrigger('mailpoet:someone-subscribes') !== null;
$automationsWithSomeoneSubscribesTrigger = array_filter(
$activeAutomations,
function(Automation $automation): bool {
return $automation->getTrigger('mailpoet:someone-subscribes') !== null;
}
);
$totalSteps = 0;
$minSteps = null;
$maxSteps = 0;
foreach ($activeWorkflows as $workflow) {
foreach ($activeAutomations as $automation) {
$steps = array_filter(
$workflow->getSteps(),
$automation->getSteps(),
function(Step $step): bool {
return $step->getType() === Step::TYPE_ACTION;
}
@ -277,16 +267,16 @@ class Reporter {
$maxSteps = max($maxSteps, $stepCount);
$totalSteps += $stepCount;
}
$averageSteps = $activeWorkflowCount > 0 ? $totalSteps / $activeWorkflowCount : 0;
$averageSteps = $activeAutomationCount > 0 ? $totalSteps / $activeAutomationCount : 0;
return [
'Automation > Number of active workflows' => $activeWorkflowCount,
'Automation > Number of draft workflows' => count($draftWorkflows),
'Automation > Number of "WordPress user registers" active workflows' => count($workflowsWithWordPressUserSubscribesTrigger),
'Automation > Number of "Someone subscribes" active workflows ' => count($workflowsWithSomeoneSubscribesTrigger),
'Automation > Number of steps in shortest active workflow' => $minSteps,
'Automation > Number of steps in longest active workflow' => $maxSteps,
'Automation > Average number of steps in active workflows' => $averageSteps,
'Automation > Number of active automations' => $activeAutomationCount,
'Automation > Number of draft automations' => count($draftAutomations),
'Automation > Number of "WordPress user registers" active automations' => count($automationsWithWordPressUserSubscribesTrigger),
'Automation > Number of "Someone subscribes" active automations ' => count($automationsWithSomeoneSubscribesTrigger),
'Automation > Number of steps in shortest active automation' => $minSteps,
'Automation > Number of steps in longest active automation' => $maxSteps,
'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\WordPress;
class API {
private const PREFIX = 'automation/';
class API extends MailPoetApi {
/** @var MailPoetApi */
private $api;
@ -30,22 +28,22 @@ class API {
}
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 {
$this->api->registerPostRoute(self::PREFIX . $route, $endpoint);
$this->api->registerPostRoute($route, $endpoint);
}
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 {
$this->api->registerPatchRoute(self::PREFIX . $route, $endpoint);
$this->api->registerPatchRoute($route, $endpoint);
}
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;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Data\NextStep;
use MailPoet\Automation\Engine\Data\Step;
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\AutomationStorage;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\Util\Security;
class DuplicateWorkflowController {
class DuplicateAutomationController {
/** @var WordPress */
private $wordPress;
/** @var WorkflowStorage */
private $workflowStorage;
/** @var AutomationStorage */
private $automationStorage;
public function __construct(
WordPress $wordPress,
WorkflowStorage $workflowStorage
AutomationStorage $automationStorage
) {
$this->wordPress = $wordPress;
$this->workflowStorage = $workflowStorage;
$this->automationStorage = $automationStorage;
}
public function duplicateWorkflow(int $id): Workflow {
$workflow = $this->workflowStorage->getWorkflow($id);
if (!$workflow) {
throw Exceptions::workflowNotFound($id);
public function duplicateAutomation(int $id): Automation {
$automation = $this->automationStorage->getAutomation($id);
if (!$automation) {
throw Exceptions::automationNotFound($id);
}
$duplicate = new Workflow(
$this->getName($workflow->getName()),
$this->getSteps($workflow->getSteps()),
$duplicate = new Automation(
$this->getName($automation->getName()),
$this->getSteps($automation->getSteps()),
$this->wordPress->wpGetCurrentUser()
);
$duplicate->setStatus(Workflow::STATUS_DRAFT);
$duplicate->setStatus(Automation::STATUS_DRAFT);
$workflowId = $this->workflowStorage->createWorkflow($duplicate);
$savedWorkflow = $this->workflowStorage->getWorkflow($workflowId);
if (!$savedWorkflow) {
throw new InvalidStateException('Workflow not found.');
$automationId = $this->automationStorage->createAutomation($duplicate);
$savedAutomation = $this->automationStorage->getAutomation($automationId);
if (!$savedAutomation) {
throw new InvalidStateException('Automation not found.');
}
return $savedWorkflow;
return $savedAutomation;
}
private function getName(string $name): string {
// translators: %s is the original automation name.
$newName = sprintf(__('Copy of %s', 'mailpoet'), $name);
$maxLength = $this->workflowStorage->getNameColumnLength();
$maxLength = $this->automationStorage->getNameColumnLength();
if (strlen($newName) > $maxLength) {
$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