Compare commits

..

1 Commits

Author SHA1 Message Date
6dbcdd5638 Release 3.101.0 2022-10-17 14:57:59 +02:00
363 changed files with 5435 additions and 8472 deletions

View File

@ -71,6 +71,13 @@ anchors:
- trunk
- release
only_trunk_and_cot: &only_trunk_and_cot
filters:
branches:
only:
- trunk
- /^cot-.*/
multisite_acceptance_config: &multisite_acceptance_config
multisite: 1
requires:
@ -349,6 +356,9 @@ jobs:
enable_cot_sync:
type: integer
default: 0
allow_fail:
type: integer
default: 0
environment:
MYSQL_COMMAND: << parameters.mysql_command >>
MYSQL_IMAGE_VERSION: << parameters.mysql_image_version >>
@ -400,9 +410,7 @@ jobs:
name: Group acceptance tests
command: |
# Convert test result filename values to be relative paths because the circleci CLI's split command requires exact matches
if [ -e $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json ]; then
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
fi
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
# `circleci tests split` returns different values based on the container it's run on
# in case group is defined find only tests containing the group
if [[ -n '<< parameters.group >>' ]]; then
@ -424,6 +432,9 @@ jobs:
--xml
-g circleci_split_group
)
if [[ << parameters.allow_fail >> == 1 ]]; then
args+=(--no-exit)
fi
docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
@ -431,13 +442,18 @@ jobs:
-e ENABLE_COT=<< parameters.enable_cot >> \
-e ENABLE_COT_SYNC=<< parameters.enable_cot_sync >> \
codeception_acceptance "${args[@]}"
- run:
name: Check exceptions
command: |
if [ "$(ls tests/_output/exceptions/*.html)" ]; then
echo "There were some exceptions during the tests run"
exit 1
fi
- when:
condition:
not:
equal: [1, << parameters.allow_fail >>]
steps:
- run:
name: Check exceptions
command: |
if [ "$(ls tests/_output/exceptions/*.html)" ]; then
echo "There were some exceptions during the tests run"
exit 1
fi
- store_artifacts:
path: tests/_output
- store_test_results:
@ -504,6 +520,9 @@ jobs:
woo_core_version:
type: string
default: ''
allow_fail:
type: integer
default: 0
steps:
- attach_workspace:
at: /home/circleci
@ -537,6 +556,9 @@ jobs:
if [[ -n '<< parameters.skip_group >>' ]]; then
args+=(--skip-group << parameters.skip_group >>)
fi
if [[ << parameters.allow_fail >> == 1 ]]; then
args+=(--no-exit)
fi
docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
@ -637,11 +659,13 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_sync
group: woo
enable_cot: 1
enable_cot_sync: 1
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -649,11 +673,13 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_no_sync
group: woo
enable_cot: 1
enable_cot_sync: 0
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -661,9 +687,10 @@ workflows:
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_off
group: woo
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
@ -684,10 +711,12 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 1
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_sync
requires:
- unit_tests
@ -696,10 +725,12 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 0
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_no_sync
requires:
- unit_tests
@ -708,8 +739,9 @@ workflows:
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
woo_core_version: 7.1.0-rc.2 # Temporarily force COT beta version
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_off
requires:
- unit_tests

View File

@ -28,7 +28,6 @@ Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by Wo
- [Add List (addList)](api_methods/AddList.md)
- [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md)
- [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
- [Delete List (deleteList)](api_methods/DeleteList.md)
- [Get Lists (getLists)](api_methods/GetLists.md)
- [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
- [Get Subscribers (getSubscribers)](api_methods/GetSubscribers.md)
@ -39,7 +38,6 @@ Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by Wo
- [Subscribe to Lists (subscribeToLists)](api_methods/SubscribeToLists.md)
- [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
- [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)
- [Update List (updateList)](api_methods/UpdateList.md)
### Usage examples

View File

@ -1,27 +0,0 @@
[back to list](../Readme.md)
# Delete List
## `bool deleteList(string $list_id)`
This method provides functionality for deleting a list that is of the type 'default'.
It returns a boolean value.
## Error handling
All expected errors from the API are exceptions of class `\MailPoet\API\MP\v1\APIException`.
Code of the exception is populated to distinguish between different errors.
An exception of base class `\Exception` can be thrown when something unexpected happens.
Codes description:
| Code | Description |
| ---- | --------------------------------------------------------------- |
| 5 | List does not exist |
| 18 | List id is empty |
| 20 | List cannot be deleted because its used for an automatic email |
| 21 | List cannot be deleted because its used for a form |
| 22 | The list couldnt be deleted from the database |
| 23 | Only lists of the type 'default' can be deleted |

View File

@ -18,8 +18,8 @@ This method returns a list of subscribers. To see the subscriber data structure,
Filter argument supports following array keys.
| Key | Type | Description |
| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` |
| listId | int | List id or dynamic segment id |
| minUpdatedAt | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers |
| Key | Type | Description |
| -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| status | string | Specific status of subscribers. One of values: `unconfirmed`, `subscribed`, `unsubscribed`, `bounced`, `inactive` |
| list_id | int | List id or dynamic segment id |
| min_updated_at | DateTime\int | DateTime object or timestamp of the minimal last update of subscribers |

View File

@ -1,39 +0,0 @@
[back to list](../Readme.md)
# Add Subscriber
## `array updateList(array $list)`
This method provides functionality for updating a list name or description. Only lists of type 'default' are supported.
It returns the updated list. See [Get Lists](GetLists.md) for a list data structure description.
## Arguments
### `$list` (required)
An associative array which contains list data.
| Property | Type | Limits | Description |
| ---------------------- | ------------ | --------- | -------------------------- |
| id (required) | string | 11 chars | A id of the list. |
| name (required) | string | 90 chars | A name of the list. |
| description (optional) | string\|null | 250 chars | A description of the list. |
## Error handling
All expected errors from the API are exceptions of class `\MailPoet\API\MP\v1\APIException`.
Code of the exception is populated to distinguish between different errors.
An exception of base class `\Exception` can be thrown when something unexpected happens.
Codes description:
| Code | Description |
| ---- | ----------------------------------------------- |
| 5 | The list was not found by id |
| 14 | Missing list name |
| 15 | Trying to use a list name that is already used |
| 18 | Missing list id |
| 19 | The list couldnt be updated in the database |
| 23 | Only lists of the type 'default' can be updated |

View File

@ -5,6 +5,7 @@
"@babel/preset-env"
],
"plugins": [
"babel-plugin-typescript-to-proptypes",
[
"@babel/plugin-transform-runtime",
{

View File

@ -117,45 +117,11 @@ class RoboFile extends \Robo\Tasks {
}
public function translationsBuild() {
$exclude = implode(',', [
'.mp_svn',
'assets/css',
'assets/img',
'assets/js',
'generated',
'lang',
'lib-3rd-party',
'mailpoet-premium',
'node_modules',
'plugin_repository',
'prefixer',
'tasks',
'temp',
'tests',
'tools',
'vendor',
'vendor-prefixed',
]);
$headers = escapeshellarg(
json_encode([
'Report-Msgid-Bugs-To' => 'http://support.mailpoet.com/',
'Last-Translator' => 'MailPoet i18n (https://www.transifex.com/organization/wysija)',
'Language-Team' => 'MailPoet i18n <https://www.transifex.com/organization/wysija>',
'Plural-Forms' => 'nplurals=2; plural=(n != 1);',
])
);
$this->collectionBuilder()
->taskExec('mkdir -p ' . __DIR__ . '/lang')
// HTML, HBS
->taskExec("php -d memory_limit=-1 tasks/makepot/makepot-views.php . > lang/mailpoet.pot")
// PHP, JS/TS
->taskExec("vendor/bin/wp i18n make-pot --merge --slug=mailpoet --domain=mailpoet --exclude=$exclude --headers=$headers . lang/mailpoet.pot")
->run();
->taskExec(
'php -d memory_limit=-1 tasks/makepot/grunt-makepot.php wp-plugin . lang/mailpoet.pot mailpoet .mp_svn,assets,lang,node_modules,plugin_repository,tasks,tests,vendor'
)->run();
}
public function translationsGetPotFileFromBuild() {
@ -385,25 +351,6 @@ class RoboFile extends \Robo\Tasks {
$this->say("Validator metadata generated to: $validatorMetadataDir");
}
public function migrationsNew() {
$generator = new \MailPoet\Migrator\Repository();
$result = $generator->create();
$path = realpath($result['path']);
$this->output->writeln('MAILPOET DATABASE MIGRATIONS');
$this->output->writeln("============================\n");
$this->output->writeln("New migration created ✔\n");
$this->output->writeln(" Name: {$result['name']}");
$this->output->writeln(" Path: $path");
}
public function migrationsStatus() {
return $this->taskExec('vendor/bin/wp mailpoet:migrations:status');
}
public function migrationsRun() {
return $this->taskExec('vendor/bin/wp mailpoet:migrations:run');
}
public function qa() {
$collection = $this->collectionBuilder();
$collection->addCode([$this, 'qaPhp']);
@ -1140,10 +1087,20 @@ class RoboFile extends \Robo\Tasks {
}
public function downloadWooCommerceZip($tag = null) {
if ($tag === 'woo-cot-beta') {
$this->downloadWooCommerceCotZip();
return;
}
$this->createWpOrgDownloader('woocommerce')
->downloadPluginZip('woocommerce.zip', __DIR__ . '/tests/plugins/', $tag);
}
public function downloadWooCommerceCotZip() {
$cotBuildUrl = 'https://github.com/woocommerce/woocommerce/files/9706609/woocommerce.zip';
file_put_contents(__DIR__ . '/tests/plugins/woocommerce.zip', file_get_contents($cotBuildUrl));
file_put_contents(__DIR__ . '/tests/plugins/woocommerce.zip-info', $cotBuildUrl);
}
public function generateData($generatorName = null, $threads = 1) {
require_once __DIR__ . '/tests/DataGenerator/_bootstrap.php';
$generator = new \MailPoet\Test\DataGenerator\DataGenerator(new \Codeception\Lib\Console\Output([]));

View File

@ -1,44 +0,0 @@
.mailpoet-automatoin-deactivate-modal {
color: #1d2327;
font-size: 13px;
line-height: 21px;
max-width: 480px;
.mailpoet-automation-options {
li {
margin-bottom: 12px;
}
}
.mailpoet-automation-option {
border: 2px solid #dcdcde;
border-radius: 4px;
color: #646970;
display: grid;
font-size: 12px;
grid-gap: 8px;
grid-template-columns: 20px auto;
line-height: 16px;
padding: 8px;
&.active {
border-color: #2271b1;
}
strong {
color: #1d2327;
display: block;
font-size: 13px;
font-weight: normal;
line-height: 21px;
}
}
.components-button {
float: right;
&.is-tertiary {
margin-right: 12px;
}
}
}

View File

@ -30,31 +30,3 @@
outline: none;
}
}
.mailpoet-automation-field__error {
position: relative;
input,
select,
textarea {
background: right top/26px no-repeat url('../../img/icons/alert.svg');
padding-right: 26px;
}
select,
input[type=number] {
background-position-x: calc(100% - 26px);
padding-right: 8px !important;
}
.components-base-control__help,
.mailpoet-automation-field-message {
color: #d63638;
}
.components-button.mailpoet-automation-button-sidebar-primary,
.components-button.mailpoet-automation-button-sidebar-primary.has-text,
.components-button.mailpoet-automation-button-sidebar-primary.has-icon {
background: #d63638;
}
}

View File

@ -12,10 +12,6 @@
font-weight: 500;
line-height: normal;
padding: 16px 48px 16px 16px;
label & {
padding: 0;
}
}
.mailpoet-automation-panel-plain-body-title-action {
@ -61,78 +57,3 @@
color: #757575;
font-style: italic;
}
.mailpoet-automation-activate-panel {
animation: mailpoet-automation-activate-panel-animation .1s forwards;
background: #fff;
border-left: 1px solid #ddd;
bottom: 0;
height: 100%;
left: auto;
overflow: auto;
position: fixed;
right: 0;
top: 0;
transform: translateX(100%);
width: 281px;
z-index: 999999;
button {
justify-content: center;
width: 100%;
}
}
.mailpoet-automation-activate-panel__header {
align-content: space-between;
align-items: center;
display: flex;
height: 61px;
.has-icon {
margin-left: auto;
width: auto;
}
}
.mailpoet-automation-activate-panel__header,
.mailpoet-automation-activate-panel__section {
border-bottom: 1px solid #ddd;
}
.mailpoet-automation-activate-panel__header,
.mailpoet-automation-activate-panel__body {
padding-left: 16px;
padding-right: 16px;
.components-spinner {
display: block;
margin: 100px auto 0;
}
}
.mailpoet-automation-activate-panel__section {
margin-left: -16px;
margin-right: -16px;
padding: 16px;
}
.mailpoet-automation-activate-panel__header-activate-button,
.mailpoet-automation-activate-panel__header-cancel-button {
flex-grow: 1;
max-width: 160px;
}
.mailpoet-automation-activate-panel__header-activate-button {
padding-right: 4px;
}
.mailpoet-automation-activate-panel__header-cancel-button {
padding-left: 4px;
}
@keyframes mailpoet-automation-activate-panel-animation {
100% {
transform: translateX(0);
}
}

View File

@ -17,21 +17,3 @@
padding: 3px;
width: 18px;
}
.mailpoet-automation-editor-stats {
margin: 0 auto 32px;
max-width: 480px;
width: 100%;
.mailpoet-automation-stats-item {
line-height: 22px;
}
.mailpoet-automation-stats-label {
color: #787c82;
}
.mailpoet-automation-stats-value {
font-size: 14px;
}
}

View File

@ -4,64 +4,18 @@
.mailpoet-add-new-button {
padding-right: 12px;
}
}
.mailpoet-automation-is-onboarding {
.notice {
display: none;
}
}
.mailpoet-automation-listing-heading {
margin-bottom: 16px;
}
.mailpoet-automation-listing {
box-shadow: none;
margin-bottom: 0;
}
.mailpoet-automation-listing-cell-name {
position: relative;
width: 100%;
> a:only-child {
bottom: 2px;
display: flex;
left: 0;
padding: 16px 24px;
position: absolute;
right: 0;
top: 0;
.mailpoet-automation-listing {
/* Prevent border radius beneath tabs */
border-radius: 0 0 1px 1px;
}
}
.mailpoet-filter-tab-panel {
background-color: #fff;
border: 1px solid #dcdcde;
border-radius: 2px;
.components-tab-panel__tabs {
box-shadow: inset 0 -1px 0 0 #dcdcde;
}
.components-tab-panel__tabs-item:focus {
box-shadow: none;
}
.components-tab-panel__tabs-item.is-active {
box-shadow: inset 0 -4px 0 0 var(--wp-admin-theme-color);
}
.components-tab-panel__tabs-item:focus-visible {
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
}
.components-tab-panel__tabs-item.is-active:focus-visible {
box-shadow:
inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),
inset 0 -4px 0 0 var(--wp-admin-theme-color);
}
border-radius: 1px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
outline: none;
.count {
background-color: #f0f0f1;
@ -73,13 +27,6 @@
}
}
.mailpoet-automation-listing-more-button button.components-button {
height: 36px;
padding: 0;
width: 36px;
svg {
height: 28px;
width: 28px;
}
.mailpoet-automation-listing-heading {
margin-bottom: 16px;
}

View File

@ -1,201 +0,0 @@
@mixin full-width {
margin-left: -20px;
padding-left: 104px;
padding-right: 104px;
width: calc(100% + 60px);
@media screen and (max-width: 782px) {
margin-left: -10px;
width: calc(100% + 34px);
}
}
.mailpoet-automation-section {
@include full-width;
}
.mailpoet-automation-white-background {
background: #fff;
}
.mailpoet-automation-section-content {
display: block;
margin: auto;
max-width: 1072px;
padding: 65px 0;
h2 {
font-size: 23px;
font-weight: 400;
line-height: 32px;
margin: 0;
padding: 0 0 8px;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 22px;
margin: 0;
padding: 0 0 40px;
}
}
.mailpoet-automation-section-hero {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: -20px;
h1 {
font-size: 32px;
font-weight: 400;
line-height: 40px;
}
p {
font-size: 14px;
line-height: 22px;
margin-bottom: 32px;
}
> div {
width: 400px;
}
img {
margin-top: 16px;
max-width: 100%;
width: 532px;
@media screen and (min-width: 1305px) {
height: 100%;
margin-top: 0;
max-height: 294px;
width: auto;
}
}
}
.mailpoet-automation-preheading {
display: block;
font-size: 11px;
letter-spacing: .2px;
line-height: 16px;
margin-bottom: 32px;
text-transform: uppercase;
}
.mailpoet-section-templates {
padding: 48px 0;
.components-button {
display: block;
font-size: 16px;
font-weight: 400;
line-height: 25px;
text-align: center;
text-underline-offset: 5px;
}
}
.mailpoet-section-template-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 40px;
> li {
flex-grow: 1;
margin-right: 8px;
max-width: 336px;
&:last-child {
margin-right: 0;
}
button {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 0;
color: #1d2327;
cursor: pointer;
padding: 24px;
text-align: left;
h3 {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
}
}
.mailpoet-section-build-list-button {
background: transparent;
border: 0;
color: #000;
cursor: pointer;
font-size: 16px;
font-weight: 400;
line-height: 24px;
padding: 0;
text-align: left;
width: 100%;
}
.mailpoet-section-build-your-own {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
ol {
list-style: decimal-leading-zero inside;
margin: 0;
max-width: 373px;
padding: 0;
> li {
border-bottom: 1px solid #dcdcde;
display: grid;
grid-gap: 16px;
grid-template-columns: 16px auto;
margin-bottom: 16px;
padding-bottom: 16px;
&.open {
p {
display: block;
}
.mailpoet-section-build-list-button {
font-weight: 600;
}
}
&:last-of-type {
border: 0;
}
}
.marker {
color: #ff5301;
display: inline-block;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
p {
display: none;
padding: 0;
}
}
img {
height: auto;
max-width: 400px;
width: 100%;
}
}

View File

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

View File

@ -1,8 +1,6 @@
.mailpoet-automation-listing-cell-status {
align-items: center;
display: grid;
grid-auto-flow: column;
white-space: nowrap;
display: flex;
> div.components-base-control > div.components-base-control__field {
margin-bottom: 0;

View File

@ -1,25 +0,0 @@
.mailpoet-option-button {
display: flex;
margin-top: 8px;
position: relative;
}
.mailpoet-option-button-main {
border-radius: 2px 0 0 2px;
margin-right: 1px;
}
.mailpoet-option-button-opener {
background: var(--wp-admin-theme-color);
border-radius: 0 2px 2px 0;
color: white;
}
.mailpoet-option-button-opener svg {
fill: white;
}
.mailpoet-option-button-opener .is-opened svg {
transform: scale(-1, -1);
transform-origin: center 12.5px;
}

View File

@ -1,32 +0,0 @@
.mailpoet-automation-stats {
display: grid;
grid-auto-flow: column;
justify-content: space-between;
}
.mailpoet-automation-stats-item {
color: $color-wordpress-heading;
display: grid;
font-size: 12px;
line-height: 16px;
text-align: center;
}
.mailpoet-automation-stats-label {
color: #646970;
display: block;
&.display-after {
order: 1;
}
}
.mailpoet-automation-stats-value {
font-weight: 600;
}
.mailpoet-automation-stats-item-separator {
color: #a7aaad;
font-size: 20px;
margin: 0 16px;
}

View File

@ -232,7 +232,3 @@ progress::-moz-progress-bar {
.mailpoet-form-field-tags label.components-form-token-field__label {
display: none;
}
.mailpoet-form-field-disabled {
cursor: not-allowed;
}

View File

@ -63,11 +63,6 @@ $form-line-height: 1.4;
.mailpoet-has-font-size {
line-height: $form-line-height;
}
.mailpoet_submit {
white-space: normal;
word-wrap: break-word;
}
}
/* Reset fieldset styles in form for backward compatibility. */

View File

@ -0,0 +1,63 @@
.mailpoet-automation-stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin: auto;
@at-root #mailpoet_automation_editor #{&} {
justify-content: center;
margin-bottom: 32px;
max-width: 560px;
}
.mailpoet-automation-stats-item {
color: $color-wordpress-heading;
font-size: 12px;
font-weight: 600;
line-height: 16px;
padding: 0 16px;
position: relative;
text-align: center;
@at-root #mailpoet_automation_editor #{&} {
flex-grow: 1;
}
&:first-of-type {
@at-root #mailpoet_automation #{&} {
padding-left: 0;
}
}
@at-root #mailpoet_automation_editor #{&} {
font-size: 14px;
line-height: 22px;
}
&:after {
align-items: center;
color: #a7aaad;
content: '';
display: flex;
font-size: 20px;
font-weight: normal;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
}
&:last-of-type:after {
content: '';
}
.mailpoet-automation-stats-label {
color: #646970;
display: block;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
}
}

View File

@ -2,13 +2,6 @@
@import '../../../node_modules/@wordpress/edit-site/build-style/style';
@import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles
@import 'settings/colors';
// automation components
@import './components-automation/statistics';
// automation editor
@import './components-automation-editor/add-step-button';
@import './components-automation-editor/add-trigger';
@import './components-automation-editor/block-icon';
@ -23,8 +16,8 @@
@import './components-automation-editor/step-card';
@import './components-automation-editor/workflow';
@import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal';
// integrations
@import './components-automation-integrations/mailpoet';
@import './components/automation_statistics';

View File

@ -13,16 +13,13 @@ ul.mailpoet-automation-templates {
margin: auto;
max-width: 982px;
padding: 48px 0;
}
.mailpoet-automation-template-list-item {
button.components-button {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
display: flex;
flex-direction: column;
display: block;
height: 100%;
padding: 24px 24px 26px;
text-align: left;
@ -40,10 +37,6 @@ ul.mailpoet-automation-templates {
box-shadow: 0 3px 6px rgba(0, 0, 0, .15);
color: inherit;
}
>* {
width: 100%
}
}
h2 {
@ -61,7 +54,7 @@ ul.mailpoet-automation-templates {
margin: 8px 0 0;
}
&.mailpoet-automation-from-scratch {
.mailpoet-automation-from-scratch {
button {
align-content: center;
border: 2px dashed #dcdcde;

View File

@ -1,17 +1,7 @@
@import '../../../node_modules/@woocommerce/components/build-style/style';
@import 'settings/colors';
// automation components
@import './components-automation/statistics';
@import './components-automation/option-button';
// automation listing
@import './components-automation-listing/sections';
@import './components-automation-listing/listing';
@import './components-automation-listing/header';
@import './components-automation-listing/search';
@import './components-automation-listing/cells/actions';
@import './components-automation-listing/cells/status';
@import './mailpoet-automation-templates';
@import './components/automation_statistics';

View File

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.75C7.99594 4.75 4.75 7.99594 4.75 12C4.75 16.0041 7.99594 19.25 12 19.25C16.0041 19.25 19.25 16.0041 19.25 12C19.25 7.99594 16.0041 4.75 12 4.75ZM3.25 12C3.25 7.16751 7.16751 3.25 12 3.25C16.8325 3.25 20.75 7.16751 20.75 12C20.75 16.8325 16.8325 20.75 12 20.75C7.16751 20.75 3.25 16.8325 3.25 12Z" fill="#d63638"/>
<path d="M13 7H11V13H13V7Z" fill="#d63638"/>
<path d="M13 15H11V17H13V15Z" fill="#d63638"/>
</svg>

Before

Width:  |  Height:  |  Size: 565 B

View File

@ -1,51 +1,35 @@
import { useEffect } 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 { 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 { plusIcon } from 'common/button/icon/plus';
import { Button, Flex } from '@wordpress/components';
import { Workflow } from './listing/workflow';
import { AutomationListing } from './listing';
import { Onboarding } from './onboarding';
import {
CreateEmptyWorkflowButton,
CreateWorkflowFromTemplateButton,
} from './testing';
import { useMutation, useQuery } from './api';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { MailPoet } from '../mailpoet';
function Content(): JSX.Element {
const count = useSelect((select) => select(storeName).getWorkflowCount());
const content =
count > 0 ? (
<>
<AutomationListingHeader />
<AutomationListing />
</>
) : (
<HeroSection />
);
const { data, loading, error } = useQuery<{ data: Workflow[] }>('workflows');
// Hide notices on onboarding screen
useEffect(() => {
const onboardingClass = 'mailpoet-automation-is-onboarding';
const element = document.querySelector('body');
if (count === 0 && !element.classList.contains(onboardingClass)) {
element.classList.add(onboardingClass);
}
if (count > 0 && element.classList.contains(onboardingClass)) {
element.classList.remove(onboardingClass);
}
}, [count]);
return (
<>
{content}
<TemplatesSection />
<BuildYourOwnSection />
</>
if (error) {
return <div>Error: {error}</div>;
}
if (loading) {
return <div>Loading workflows...</div>;
}
const workflows = data?.data ?? [];
return workflows.length > 0 ? (
<AutomationListing workflows={workflows} loading={loading} />
) : (
<Onboarding />
);
}
@ -53,7 +37,17 @@ function Workflows(): JSX.Element {
return (
<>
<TopBarWithBeamer />
<Notices />
<Flex className="mailpoet-automation-listing-heading">
<h1 className="wp-heading-inline">Automations</h1>
<Button
href={MailPoet.urls.automationTemplates}
icon={plusIcon}
variant="primary"
className="mailpoet-add-new-button"
>
New automation
</Button>
</Flex>
<Content />
</>
);
@ -110,41 +104,33 @@ function DeleteSchemaButton(): JSX.Element {
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 />
<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>
</BrowserRouter>
</SlotFillProvider>
</div>
</BrowserRouter>
);
}
window.addEventListener('DOMContentLoaded', () => {
createStore();
const root = document.getElementById('mailpoet_automation');
if (root) {
registerTranslations();
registerApiErrorHandler();
initializeApi();
ReactDOM.render(<App />, root);
}
});

View File

@ -1,44 +0,0 @@
import { Button, DropdownMenu } from '@wordpress/components';
import { chevronDown } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Fragment } from '@wordpress/element';
import { StepMoreControlsType } from '../../types/filters';
type OptionButtonPropType = {
variant: Button.ButtonVariant;
controls: StepMoreControlsType;
title: string;
onClick: () => void;
};
export function OptionButton({
controls,
title,
onClick,
variant,
}: OptionButtonPropType): JSX.Element {
const slots = Object.values(controls).filter((item) => item.slot);
return (
<div className="mailpoet-option-button">
<Button
variant={variant}
className="mailpoet-option-button-main"
onClick={onClick}
>
{title}
</Button>
{slots.length > 0 &&
slots.map(({ key, slot }) => (
<Fragment key={`slot-${key}`}>{slot}</Fragment>
))}
{Object.values(controls).length > 0 && (
<DropdownMenu
className="mailpoet-option-button-opener"
label={__('More', 'mailpoet')}
icon={chevronDown}
controls={Object.values(controls).map((item) => item.control)}
popoverProps={{ position: 'bottom left' }}
/>
)}
</div>
);
}

View File

@ -1,41 +0,0 @@
import { Fragment } from '@wordpress/element';
type Item = {
key: string;
label: string;
value: number;
};
type Props = {
items: Item[];
labelPosition?: 'before' | 'after';
};
export function Statistics({
items,
labelPosition = 'before',
}: Props): JSX.Element {
const intl = new Intl.NumberFormat();
return (
<div className="mailpoet-automation-stats">
{items.map((item, i) => (
<Fragment key={item.key}>
<div key={item.key} className="mailpoet-automation-stats-item">
<span
className={`mailpoet-automation-stats-label display-${labelPosition}`}
>
{item.label}
</span>
<span className="mailpoet-automation-stats-value">
{intl.format(item.value)}
</span>
</div>
{i < items.length - 1 && (
<div className="mailpoet-automation-stats-item-separator"></div>
)}
</Fragment>
))}
</div>
);
}

View File

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

View File

@ -30,7 +30,6 @@ export const registerApiErrorHandler = (): void =>
message ?? __('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
dispatch(storeName).setErrors({ steps: [] });
return undefined;
}

View File

@ -4,7 +4,6 @@ import {
Button,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store';
export function TrashButton(): JSX.Element {
@ -21,8 +20,8 @@ export function TrashButton(): JSX.Element {
<>
<ConfirmDialog
isOpen={showConfirmDialog}
title={__('Delete automation', 'mailpoet')}
confirmButtonText={__('Yes, delete', 'mailpoet')}
title="Delete workflow"
confirmButtonText="Yes, delete"
onConfirm={async () => {
trash(() => {
setShowConfirmDialog(false);
@ -31,12 +30,7 @@ export function TrashButton(): JSX.Element {
onCancel={() => setShowConfirmDialog(false)}
__experimentalHideHeader={false}
>
{sprintf(
__('You are about to delete the automation "%s".', 'mailpoet'),
workflow.name,
)}
<br />
{__(' This will stop it for all subscribers immediately.', 'mailpoet')}
You are about to delete the {workflow.name} workflow.
</ConfirmDialog>
<Button
@ -44,7 +38,7 @@ export function TrashButton(): JSX.Element {
isDestructive
onClick={() => setShowConfirmDialog(true)}
>
{__('Move to Trash', 'mailpoet')}
Move to Trash
</Button>
</>
);

View File

@ -1,9 +1,9 @@
import { ComponentProps, ComponentType, Ref } from 'react';
import {
__experimentalText as Text,
Button,
Dropdown as WpDropdown,
Button,
VisuallyHidden,
__experimentalText as Text,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element';
@ -38,7 +38,7 @@ export function DocumentActions({ children }): JSX.Element {
let chipClass = 'mailpoet-automation-editor-chip-gray';
if (workflowStatus === WorkflowStatus.ACTIVE) {
chipClass = 'mailpoet-automation-editor-chip-success';
} else if (workflowStatus === WorkflowStatus.DEACTIVATING) {
} else if (workflowStatus === WorkflowStatus.INACTIVE) {
chipClass = 'mailpoet-automation-editor-chip-danger';
}
@ -64,7 +64,7 @@ export function DocumentActions({ children }): JSX.Element {
as="h1"
>
<VisuallyHidden as="span">
{__('Editing automation:', 'mailpoet')}
{__('Editing workflow: ')}
</VisuallyHidden>
{workflowName}
</Text>
@ -73,12 +73,10 @@ export function DocumentActions({ children }): JSX.Element {
size="body"
className={`edit-site-document-actions__secondary-item ${chipClass}`}
>
{workflowStatus === WorkflowStatus.ACTIVE &&
__('Active', 'mailpoet')}
{workflowStatus === WorkflowStatus.DEACTIVATING &&
__('Deactivating', 'mailpoet')}
{workflowStatus === WorkflowStatus.DRAFT &&
__('Draft', 'mailpoet')}
{workflowStatus === WorkflowStatus.ACTIVE && __('Active')}
{workflowStatus === WorkflowStatus.INACTIVE &&
__('Inactive')}
{workflowStatus === WorkflowStatus.DRAFT && __('Draft')}
</Text>
</a>
<Button
@ -87,9 +85,9 @@ export function DocumentActions({ children }): JSX.Element {
aria-expanded={isOpen}
aria-haspopup="true"
onClick={onToggle}
label={__('Change automation name', 'mailpoet')}
label={__('Change workflow name')}
>
{showIconLabels && __('Rename', 'mailpoet')}
{showIconLabels && __('Rename')}
</Button>
</>
)}

View File

@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
import { Chip } from '../chip';
import { ColoredIcon } from '../icons';
import {
StepErrors as StepErrorType,
StepError as StepErrorType,
stepSidebarKey,
storeName,
} from '../../store';
@ -151,14 +151,11 @@ export function Errors(): JSX.Element | null {
<Composite
state={compositeState}
role="list"
aria-label={__('Automation errors', 'mailpoet')}
aria-label={__('Workflow errors', 'mailpoet')}
className="mailpoet-automation-errors"
>
<div className="mailpoet-automation-errors-header">
{
// translators: Label for a list of automation steps that are incomplete or have errors
__('The following steps are not fully set:', 'mailpoet')
}
{__('The following steps are not fully set:', 'mailpoet')}
</div>
{stepErrors.map((error) => (
<StepError key={error.step_id} stepId={error.step_id} />

View File

@ -1,11 +1,5 @@
import { useState } from 'react';
import {
Button,
NavigableMenu,
TextControl,
Tooltip,
} from '@wordpress/components';
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import { Button, NavigableMenu, TextControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { PinnedItems } from '@wordpress/interface';
import { __ } from '@wordpress/i18n';
import { DocumentActions } from './document_actions';
@ -14,98 +8,44 @@ import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import {
DeactivateImmediatelyModal,
DeactivateModal,
} from '../modals/deactivate-modal';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton({ onClick, label }): JSX.Element {
const { errors, isDeactivating } = useSelect(
function ActivateButton(): JSX.Element {
const { errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
isDeactivating:
select(storeName).getWorkflowData().status ===
WorkflowStatus.DEACTIVATING,
}),
[],
);
const button = (
const { activate } = useDispatch(storeName);
return (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={onClick}
disabled={isDeactivating || !!errors}
onClick={activate}
disabled={!!errors}
>
{label}
Activate
</Button>
);
if (isDeactivating) {
return (
<Tooltip
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// 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.',
'mailpoet',
)}
>
{button}
</Tooltip>
);
}
return button;
}
function UpdateButton(): JSX.Element {
const { save } = useDispatch(storeName);
const { activate } = useDispatch(storeName);
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
if (workflow.stats.totals.in_progress === 0) {
return (
<Button
variant="primary"
className="editor-post-publish-button"
onClick={save}
>
{__('Update', 'mailpoet')}
</Button>
);
}
return (
<Tooltip
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// 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.',
'mailpoet',
)}
<Button
variant="primary"
className="editor-post-publish-button"
onClick={activate}
>
<Button
variant="primary"
className="editor-post-publish-button"
onClick={save}
disabled
>
{__('Update', 'mailpoet')}
</Button>
</Tooltip>
Update
</Button>
);
}
@ -114,100 +54,16 @@ function SaveDraftButton(): JSX.Element {
return (
<Button variant="tertiary" onClick={save}>
{__('Save draft', 'mailpoet')}
{__('Save draft')}
</Button>
);
}
function DeactivateButton(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
}),
[],
);
const deactivateOrShowModal = () => {
if (hasUsersInProgress) {
setShowDeactivateModal(true);
return;
}
setIsBusy(true);
void dispatch(storeName).deactivate();
};
return (
<>
{showDeactivateModal && (
<DeactivateModal
onClose={() => {
setShowDeactivateModal(false);
}}
/>
)}
<Button
isBusy={isBusy}
variant="tertiary"
onClick={deactivateOrShowModal}
>
{__('Deactivate', 'mailpoet')}
</Button>
</>
);
}
function DeactivateNowButton(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
}),
[],
);
const deactivateOrShowModal = () => {
if (hasUsersInProgress) {
setShowDeactivateModal(true);
return;
}
setIsBusy(true);
void dispatch(storeName).deactivate();
};
return (
<>
{showDeactivateModal && (
<DeactivateImmediatelyModal
onClose={() => {
setShowDeactivateModal(false);
}}
/>
)}
<Button
isBusy={isBusy}
variant="tertiary"
onClick={deactivateOrShowModal}
>
{__('Deactivate now', 'mailpoet')}
</Button>
</>
);
}
type Props = {
showInserterToggle: boolean;
toggleActivatePanel: () => void;
};
export function Header({
showInserterToggle,
toggleActivatePanel,
}: Props): JSX.Element {
export function Header({ showInserterToggle }: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect(
(select) => ({
@ -234,14 +90,13 @@ export function Header({
{() => (
<div className="mailpoet-automation-editor-dropdown-name-edit">
<div className="mailpoet-automation-editor-dropdown-name-edit-title">
{__('Automation name', 'mailpoet')}
{__('Automation name')}
</div>
<TextControl
value={workflowName}
onChange={(newName) => setWorkflowName(newName)}
help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet',
)}
/>
</div>
@ -252,30 +107,9 @@ export function Header({
<div className="edit-site-header_end">
<div className="edit-site-header__actions">
<Errors />
{workflowStatus === WorkflowStatus.DRAFT && (
<>
<SaveDraftButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Activate', 'mailpoet')}
/>
</>
)}
{workflowStatus === WorkflowStatus.ACTIVE && (
<>
<DeactivateButton />
<UpdateButton />
</>
)}
{workflowStatus === WorkflowStatus.DEACTIVATING && (
<>
<DeactivateNowButton />
<ActivateButton
onClick={toggleActivatePanel}
label={__('Update & Activate', 'mailpoet')}
/>
</>
)}
<SaveDraftButton />
{workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />}
{workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />}
<PinnedItems.Slot scope={storeName} />
<MoreMenu />
</div>

View File

@ -1,6 +1,6 @@
import { Button, ToolbarItem } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { __, _x } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { storeName } from '../../store';
@ -28,11 +28,13 @@ export function InserterToggle(): JSX.Element {
onMouseDown={(event) => event.preventDefault()}
onClick={toggleInserterSidebar}
icon={plus}
label={__('Toggle step inserter', 'mailpoet')}
label={_x(
'Toggle step inserter',
'Generic label for step inserter button',
)}
showTooltip={!showIconLabels}
>
{showIconLabels &&
(!isInserterOpened ? __('Add', 'mailpoet') : __('Close', 'mailpoet'))}
{showIconLabels && (!isInserterOpened ? __('Add') : __('Close'))}
</ToolbarItem>
);
}

View File

@ -20,14 +20,14 @@ export function MoreMenu(): JSX.Element {
>
{() => (
<>
<MenuGroup label={_x('View', 'noun', 'mailpoet')}>
<MenuGroup label={_x('View', 'noun')}>
<PreferenceToggleMenuItem
scope={storeName}
name="fullscreenMode"
label={__('Fullscreen mode', 'mailpoet')}
info={__('Work without distraction', 'mailpoet')}
messageActivated={__('Fullscreen mode activated', 'mailpoet')}
messageDeactivated={__('Fullscreen mode deactivated', 'mailpoet')}
label={__('Fullscreen mode')}
info={__('Work without distraction')}
messageActivated={__('Fullscreen mode activated')}
messageDeactivated={__('Fullscreen mode deactivated')}
shortcut={displayShortcut.secondary('f')}
/>
</MenuGroup>

View File

@ -13,10 +13,7 @@ export const InserterListboxGroup = forwardRef<HTMLDivElement, Props>(
useEffect(() => {
if (shouldSpeak) {
speak(
// translators: Moving through automation step list using keyboard
__('Use left and right arrow keys to move through steps', 'mailpoet'),
);
speak(__('Use left and right arrow keys to move through blocks'));
}
}, [shouldSpeak]);

View File

@ -7,7 +7,6 @@ import { PremiumModal } from 'common/premium_modal';
import { Inserter } from '../inserter';
import { Item } from '../inserter/item';
import { storeName } from '../../store';
import { AddStepCallbackType } from '../../../types/filters';
export function InserterPopover(): JSX.Element | null {
const popoverRef = useRef<HTMLDivElement>();
@ -21,7 +20,7 @@ export function InserterPopover(): JSX.Element | null {
const { setInserterPopover } = useDispatch(storeName);
const onInsert = useCallback((item: Item) => {
const addStepCallback: AddStepCallbackType = Hooks.applyFilters(
const addStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.add_step_callback',
() => {
setShowModal(true);

View File

@ -2,7 +2,7 @@ import { forwardRef, Fragment, useCallback, useMemo } from 'react';
import { SearchControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useRef, useImperativeHandle, useState } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { blockDefault, Icon } from '@wordpress/icons';
import { Group } from './group';
import { Item } from './item';
@ -41,26 +41,21 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
{
type: 'triggers',
title: undefined,
// translators: Label for a list of automation steps of type trigger
label: _x('Triggers', 'automation steps', 'mailpoet'),
label: __('Triggers', 'mailpoet'),
items: steps.filter(({ group }) => group === 'triggers'),
},
]
: [
{
type: 'actions',
// translators: Label for a list of automation steps of type action
title: _x('Actions', 'automation steps', 'mailpoet'),
// translators: Label for a list of automation steps of type action
label: _x('Actions', 'automation steps', 'mailpoet'),
title: __('Actions', 'mailpoet'),
label: __('Actions', 'mailpoet'),
items: steps.filter(({ group }) => group === 'actions'),
},
{
type: 'logical',
// translators: Label for a list of logical automation steps (if/else, etc.)
title: _x('Logical', 'automation steps', 'mailpoet'),
// translators: Label for a list of logical automation steps (if/else, etc.)
label: _x('Logical', 'automation steps', 'mailpoet'),
title: __('Logical', 'mailpoet'),
label: __('Logical', 'mailpoet'),
items: steps.filter(({ group }) => group === 'logical'),
},
],
@ -101,8 +96,8 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
setFilterValue(value);
}}
value={filterValue}
label={__('Search for automation steps', 'mailpoet')}
placeholder={__('Search', 'mailpoet')}
label={__('Search for blocks and patterns')}
placeholder={__('Search')}
ref={searchRef}
/>
@ -140,7 +135,7 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
className="block-editor-inserter__no-results-icon"
icon={blockDefault}
/>
<p>{__('No results found.', 'mailpoet')}</p>
<p>{__('No results found.')}</p>
</div>
)}
</InserterListbox>

View File

@ -25,7 +25,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({
name: 'mailpoet/automation-editor/toggle-fullscreen',
category: 'global',
description: __('Toggle fullscreen mode.', 'mailpoet'),
description: __('Toggle fullscreen mode.'),
keyCombination: {
modifier: 'secondary',
character: 'f',
@ -35,7 +35,7 @@ export function KeyboardShortcuts(): null {
void registerShortcut({
name: 'mailpoet/automation-editor/toggle-sidebar',
category: 'global',
description: __('Show or hide the settings sidebar.', 'mailpoet'),
description: __('Show or hide the settings sidebar.'),
keyCombination: {
modifier: 'primaryShift',
character: ',',

View File

@ -1,155 +0,0 @@
import { useState } from 'react';
import { Button, Modal } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { dispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
type DeactivateImmediatelyModalProps = {
onClose: () => void;
};
export function DeactivateImmediatelyModal({
onClose,
}: DeactivateImmediatelyModalProps): JSX.Element {
const [isBusy, setIsBusy] = useState<boolean>(false);
return (
<Modal
className="mailpoet-automatoin-deactivate-modal"
title={__('Stop automatoin for all subscribers?', 'mailpoet')}
onRequestClose={onClose}
>
<p>
{__(
'Are you sure you want to deactivate now? This would stop this automation for all subscribers immediately.',
'mailpoet',
)}
</p>
<Button
isBusy={isBusy}
variant="primary"
onClick={() => {
setIsBusy(true);
dispatch(storeName).deactivate(true);
}}
>
{__('Deactivate now', 'mailpoet')}
</Button>
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
{__('Cancel', 'mailpoet')}
</Button>
</Modal>
);
}
type DeactivateModalProps = {
onClose: () => void;
};
export function DeactivateModal({
onClose,
}: DeactivateModalProps): JSX.Element {
const { workflowName } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
}),
[],
);
const [selected, setSelected] = useState<
WorkflowStatus.DRAFT | WorkflowStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation.
const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'),
workflowName,
);
return (
<Modal
className="mailpoet-automatoin-deactivate-modal"
title={title}
onRequestClose={onClose}
>
{__(
"Some subscribers entered but have not finished the flow. Let's decide what to do in this case.",
'mailpoet',
)}
<ul className="mailpoet-automation-options">
<li>
<label
className={
selected === WorkflowStatus.DEACTIVATING
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
/>
</span>
<span>
<strong>
{__('Let entered subscribers finish the flow', 'mailpoet')}
</strong>
{__(
"New subscribers won't enter, but recently entered could proceed.",
'mailpoet',
)}
</span>
</label>
</li>
<li>
<label
className={
selected === WorkflowStatus.DRAFT
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DRAFT}
onChange={() => setSelected(WorkflowStatus.DRAFT)}
/>
</span>
<span>
<strong>
{__('Stop automation for all subscribers', 'mailpoet')}
</strong>
{__(
'Automation will be deactivated for all the subscribers immediately.',
'mailpoet',
)}
</span>
</label>
</li>
</ul>
<Button
isBusy={isBusy}
variant="primary"
onClick={() => {
setIsBusy(true);
dispatch(storeName).deactivate(
selected !== WorkflowStatus.DEACTIVATING,
);
}}
>
{__('Deactivate automation', 'mailpoet')}
</Button>
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
{__('Cancel', 'mailpoet')}
</Button>
</Modal>
);
}

View File

@ -1,128 +0,0 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelect } from '@wordpress/data';
import { Button, Spinner } from '@wordpress/components';
import { closeSmall } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
import { storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow';
import { MailPoet } from '../../../../mailpoet';
function PreStep({ onClose }): JSX.Element {
const [isActivating, setIsActivating] = useState(false);
const { activate } = useDispatch(storeName);
return (
<>
<div className="mailpoet-automation-activate-panel__header">
<div className="mailpoet-automation-activate-panel__header-activate-button">
<Button
variant="primary"
disabled={isActivating}
isBusy={isActivating}
autoFocus={!isActivating}
onClick={() => {
setIsActivating(true);
activate();
}}
>
{isActivating && __('Activating…', 'mailpoet')}
{!isActivating && __('Activate', 'mailpoet')}
</Button>
</div>
<div className="mailpoet-automation-activate-panel__header-cancel-button">
<Button variant="secondary" onClick={onClose} disabled={isActivating}>
{__('Cancel', 'mailpoet')}
</Button>
</div>
</div>
{isActivating && (
<div className="mailpoet-automation-activate-panel__body">
<Spinner />
</div>
)}
{!isActivating && (
<div className="mailpoet-automation-activate-panel__body">
<p>
<strong>{__('Are you ready to activate?', 'mailpoet')}</strong>
</p>
<p>
{__('Double-check your settings before activating.', 'mailpoet')}
</p>
</div>
)}
</>
);
}
function PostStep({ onClose }): JSX.Element {
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
const goToListings = () => {
window.location.href = MailPoet.urls.automationListing;
};
return (
<>
<div className="mailpoet-automation-activate-panel__header">
<Button
icon={closeSmall}
onClick={onClose}
label={__('Close', 'mailpoet')}
/>
</div>
<div className="mailpoet-automation-activate-panel__body">
<div className="mailpoet-automation-activate-panel__section">
{sprintf(__('"%s" is now live.', 'mailpoet'), workflow.name)}
</div>
<p>
<strong>{__('Whats next?', 'mailpoet')}</strong>
</p>
<p>
{__(
'View all your automations to track statistics and create new ones.',
'mailpoet',
)}
</p>
<Button variant="secondary" onClick={goToListings}>
{__('View all automations', 'mailpoet')}
</Button>
</div>
</>
);
}
export function ActivatePanel({ onClose }): JSX.Element {
const { workflow, errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(),
}),
[],
);
useEffect(() => {
if (errors) {
onClose();
}
}, [errors, onClose]);
if (errors) {
return null;
}
const isActive = workflow.status === WorkflowStatus.ACTIVE;
return (
<div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />}
{!isActive && <PreStep onClose={onClose} />}
</div>
);
}

View File

@ -1,24 +0,0 @@
import { PanelBody as WpPanelBody } from '@wordpress/components';
import { useEffect, useState } from 'react';
type Props = WpPanelBody.Props & {
hasErrors?: boolean;
};
export function PanelBody({ hasErrors = false, ...props }: Props): JSX.Element {
const [isOpened, setIsOpened] = useState(props.initialOpen);
useEffect(() => {
if (hasErrors) {
setIsOpened(true);
}
}, [hasErrors]);
return (
<WpPanelBody
opened={isOpened}
onToggle={() => setIsOpened((prevState) => !prevState)}
{...props}
/>
);
}

View File

@ -1,5 +1,4 @@
import { Dropdown, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { edit, Icon } from '@wordpress/icons';
import { PlainBodyTitle } from './plain-body-title';
import { TitleActionButton } from './title-action-button';
@ -26,7 +25,7 @@ export function StepName({
<TitleActionButton
onClick={onToggle}
aria-expanded={isOpen}
aria-label={__('Edit step name', 'mailpoet')}
aria-label="Edit step name"
>
<Icon icon={edit} size={16} />
</TitleActionButton>
@ -34,15 +33,13 @@ export function StepName({
)}
renderContent={() => (
<TextControl
label={__('Step name', 'mailpoet')}
label="Step name"
className="mailpoet-step-name-input"
placeholder={defaultName}
value={currentName}
onChange={update}
help={__(
'Give the automation step a name that indicates its purpose. E.g "Abandoned cart recovery". This name will be displayed only to you and not to the clients.',
'mailpoet',
)}
help="Give the automation step a name that indicates its purpose. E.g
Abandoned cart recovery. This name will be displayed only to you and not to the clients."
/>
)}
/>

View File

@ -1,6 +1,5 @@
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
// See:
@ -18,13 +17,13 @@ export function Header({ sidebarKey }: Props): JSX.Element {
const [workflowAriaLabel, workflowActiveClass] =
sidebarKey === workflowSidebarKey
? [__('Automation (selected)', 'mailpoet'), 'is-active']
: [__('Automation', 'mailpoet'), ''];
? ['Workflow (selected)', 'is-active']
: ['Workflow', ''];
const [stepAriaLabel, stepActiveClass] =
sidebarKey === stepSidebarKey
? [__('Step (selected)', 'mailpoet'), 'is-active']
: [__('Step', 'mailpoet'), ''];
? ['Step (selected)', 'is-active']
: ['Step', ''];
return (
<ul>
@ -33,9 +32,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
onClick={openWorkflowSettings}
className={`edit-site-sidebar__panel-tab ${workflowActiveClass}`}
aria-label={workflowAriaLabel}
data-label={__('Automation', 'mailpoet')}
data-label="Workflow"
>
{__('Automation', 'mailpoet')}
Workflow
</Button>
</li>
<li>
@ -43,9 +42,9 @@ export function Header({ sidebarKey }: Props): JSX.Element {
onClick={openStepSettings}
className={`edit-site-sidebar__panel-tab ${stepActiveClass}`}
aria-label={stepAriaLabel}
data-label={__('Step', 'mailpoet')}
data-label="Workflow"
>
{__('Step', 'mailpoet')}
Step
</Button>
</li>
</ul>

View File

@ -47,13 +47,13 @@ export function Sidebar(props: Props): JSX.Element {
<ComplementaryArea
identifier={sidebarKey}
header={<Header sidebarKey={sidebarKey} />}
closeLabel={__('Close settings', 'mailpoet')}
closeLabel={__('Close settings')}
headerClassName="edit-site-sidebar__panel-tabs"
title={__('Settings', 'mailpoet')}
title={__('Settings')}
icon={cog}
className="edit-site-sidebar mailpoet-automation-sidebar"
panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)', 'mailpoet')}
smallScreenTitle={workflowName || __('(no title)')}
scope={storeName}
toggleShortcut={keyboardShortcut}
isActiveByDefault={sidebarActiveByDefault}

View File

@ -30,11 +30,22 @@ export function StepSidebar(): JSX.Element {
icon={selectedStepType.icon}
/>
<Edit
// Force sidebar remount to avoid different steps mixing their data.
// This can happen e.g. when having "useState" or "useRef" internally.
key={selectedStep.id}
/>
<Edit />
<PanelBody title="Debug info" initialOpen={false}>
<div>
<strong>ID:</strong> {selectedStep.id}
</div>
<div>
<strong>Type:</strong> {selectedStep.type}
</div>
<div>
<strong>Key:</strong> {selectedStep.key}
</div>
<div>
<strong>Args:</strong> {JSON.stringify(selectedStep.args)}
</div>
</PanelBody>
</div>
);
}

View File

@ -1,6 +1,5 @@
import { PanelBody, PanelRow } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../../store';
import { TrashButton } from '../../actions/trash-button';
@ -19,7 +18,7 @@ export function WorkflowSidebar(): JSX.Element {
};
return (
<PanelBody title={__('Automation details', 'mailpoet')} initialOpen>
<PanelBody title="Automation details" initialOpen>
<PanelRow>
<strong>Date added</strong>{' '}
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString(

View File

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

View File

@ -16,10 +16,6 @@ import { InserterPopover } from '../inserter-popover';
import { storeName } from '../../store';
import { AddTrigger } from './add-trigger';
import { Statistics } from './statistics';
import {
RenderStepSeparatorType,
RenderStepType,
} from '../../../types/filters';
export function Workflow(): JSX.Element {
const { workflowData, selectedStep } = useSelect(
@ -54,7 +50,7 @@ export function Workflow(): JSX.Element {
}, [stepMap]);
const renderStep = useMemo(
(): RenderStepType =>
() =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step',
(stepData: StepData) =>
@ -71,7 +67,7 @@ export function Workflow(): JSX.Element {
);
const renderSeparator = useMemo(
(): RenderStepSeparatorType =>
() =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step_separator',
(previousStepData: StepData) => (
@ -90,7 +86,7 @@ export function Workflow(): JSX.Element {
<Composite
state={compositeState}
role="tree"
aria-label={__('Automation', 'mailpoet')}
aria-label={__('Workflow', 'mailpoet')}
aria-orientation="vertical"
className="mailpoet-automation-editor-workflow"
>

View File

@ -1,7 +1,6 @@
import { useSelect } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element {
const { workflow } = useSelect(
@ -12,29 +11,27 @@ export function Statistics(): JSX.Element {
);
return (
<div className="mailpoet-automation-editor-stats">
<BaseStatistics
items={[
{
key: 'entered',
// translators: Total number of subscribers who entered an automation
label: _x('Total Entered', 'automation stats', 'mailpoet'),
value: workflow.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,
},
{
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,
},
]}
/>
<div>
<ul className="mailpoet-automation-stats">
<li className="mailpoet-automation-stats-item">
<span className="mailpoet-automation-stats-label">
{__('Total Entered', 'mailpoet')}
</span>
{new Intl.NumberFormat().format(workflow.stats.totals.entered)}
</li>
<li className="mailpoet-automation-stats-item">
<span className="mailpoet-automation-stats-label">
{__('Total Processing', 'mailpoet')}
</span>
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)}
</li>
<li className="mailpoet-automation-stats-item">
<span className="mailpoet-automation-stats-label">
{__('Total Exited', 'mailpoet')}
</span>
{new Intl.NumberFormat().format(workflow.stats.totals.exited)}
</li>
</ul>
</div>
);
}

View File

@ -1,77 +1,59 @@
import { useState, Fragment } from 'react';
import { useCallback, useState } from 'react';
import { DropdownMenu } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { moreVertical, trash } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { PremiumModal } from 'common/premium_modal';
import { Step as StepData } from './types';
import { storeName } from '../../store';
import { StepMoreControlsType } from '../../../types/filters';
type Props = {
step: StepData;
};
export function StepMoreMenu({ step }: Props): JSX.Element {
const { stepType } = useSelect(
(select) => ({
stepType: select(storeName).getStepType(step.key),
}),
[step],
);
const [showModal, setShowModal] = useState(false);
const moreControls: StepMoreControlsType = Hooks.applyFilters(
'mailpoet.automation.workflow.step.more-controls',
{
delete: {
key: 'delete',
control: {
title: __('Delete step', 'mailpoet'),
icon: trash,
onClick: () => setShowModal(true),
},
slot: () => {
if (!showModal) {
return false;
}
return (
<PremiumModal
onRequestClose={() => {
setShowModal(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'remove_automation_step',
}}
>
{__('You cannot remove a step from the automation.', 'mailpoet')}
</PremiumModal>
);
},
const onDelete = useCallback((stepData: StepData) => {
const deleteStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.delete_step_callback',
() => {
setShowModal(true);
},
},
step,
stepType,
);
);
deleteStepCallback(stepData);
}, []);
const slots = Object.values(moreControls).filter(
(item) => item.slot !== undefined,
);
const controls = Object.values(moreControls).map((item) => item.control);
return (
<div className="mailpoet-automation-step-more-menu">
{slots.map(({ key, slot }) => (
<Fragment key={key}>{slot()}</Fragment>
))}
<DropdownMenu
label={__('More', 'mailpoet')}
icon={moreVertical}
popoverProps={{ position: 'bottom right' }}
toggleProps={{ isSmall: true }}
controls={Object.values(controls)}
/>
</div>
<>
<div className="mailpoet-automation-step-more-menu">
<DropdownMenu
label={__('More', 'mailpoet')}
icon={moreVertical}
controls={[
{
title: __('Delete step', 'mailpoet'),
icon: trash,
onClick: () => onDelete(step),
},
]}
popoverProps={{ position: 'bottom right' }}
toggleProps={{ isSmall: true }}
/>
</div>
{showModal && (
<PremiumModal
onRequestClose={() => {
setShowModal(false);
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'remove_automation_step',
}}
>
{__('You cannot remove a new step from the automation.', 'mailpoet')}
</PremiumModal>
)}
</>
);
}

View File

@ -3,7 +3,7 @@ import { useContext } from 'react';
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 { __ } from '@wordpress/i18n';
import { WorkflowCompositeContext } from './context';
import { StepMoreMenu } from './step-more-menu';
import { Step as StepData } from './types';
@ -51,7 +51,6 @@ export function Step({ step, isSelected }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext);
const { batch } = useRegistry();
const compositeItemId = `step-${step.id}`;
const stepTypeData = stepType ?? getUnknownStepType(step);
return (
<div className="mailpoet-automation-editor-step-wrapper">
@ -64,7 +63,6 @@ export function Step({ step, isSelected }: Props): JSX.Element {
'is-selected-step': isSelected,
'is-unknown-step': !stepType,
})}
id={compositeItemId}
key={step.id}
focusable
onClick={() =>
@ -84,14 +82,11 @@ export function Step({ step, isSelected }: Props): JSX.Element {
/>
</div>
<div>
<label
htmlFor={compositeItemId}
className="mailpoet-automation-editor-step-title"
>
<div className="mailpoet-automation-editor-step-title">
{step.type !== 'trigger'
? stepTypeData.title
: _x('Trigger', 'noun', 'mailpoet')}
</label>
: __('Trigger', 'mailpoet')}
</div>
<div className="mailpoet-automation-editor-step-subtitle">
{step.type !== 'trigger'
? stepTypeData.subtitle(step)

View File

@ -1,5 +1,3 @@
import { WorkflowStatus } from '../../../listing/workflow';
export type NextStep = {
id: string;
};
@ -15,7 +13,7 @@ export type Step = {
export type Workflow = {
id: number;
name: string;
status: WorkflowStatus;
status: 'active' | 'inactive' | 'draft' | 'trash';
created_at: string;
updated_at: string;
activated_at: string;

View File

@ -1,22 +1,14 @@
import classnames from 'classnames';
import ReactDOM from 'react-dom';
import { useEffect, useState } from 'react';
import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
import {
dispatch,
select as globalSelect,
StoreDescriptor,
useSelect,
} from '@wordpress/data';
import { useSelect } from '@wordpress/data';
import { wordpress } from '@wordpress/icons';
import {
ComplementaryArea,
FullscreenMode,
InterfaceSkeleton,
FullscreenMode,
} from '@wordpress/interface';
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { Header } from './components/header';
import { InserterSidebar } from './components/inserter-sidebar';
@ -31,9 +23,6 @@ import { initialize as initializeMailPoetIntegration } from '../integrations/mai
import { MailPoet } from '../../mailpoet';
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
import { registerApiErrorHandler } from './api-error-handler';
import { ActivatePanel } from './components/panel/activate-panel';
import { registerTranslations } from '../i18n';
import { WorkflowStatus } from '../listing/workflow';
// See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
@ -42,36 +31,6 @@ import { WorkflowStatus } from '../listing/workflow';
// disable inserter sidebar until we implement drag & drop
const showInserterSidebar = false;
/**
* Show temporary message that active workflows cant be updated
*
* see MAILPOET-4744
*/
function updatingActiveWorkflowNotPossible() {
const workflow = globalSelect(storeName).getWorkflowData();
if (
![WorkflowStatus.ACTIVE, WorkflowStatus.DEACTIVATING].includes(
workflow.status,
)
) {
return;
}
if (workflow.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.',
'mailpoet',
),
{
type: 'snackbar',
},
);
}
function Editor(): JSX.Element {
const {
isFullscreenActive,
@ -89,16 +48,7 @@ function Editor(): JSX.Element {
}),
[],
);
const [showActivatePanel, setShowActivatePanel] = useState(false);
const [isBooting, setIsBooting] = useState(true);
useEffect(() => {
if (!isBooting) {
return;
}
updatingActiveWorkflowNotPossible();
setIsBooting(false);
}, [isBooting]);
const className = classnames('interface-interface-skeleton', {
'is-sidebar-opened': isSidebarOpened,
'show-icon-labels': showIconLabels,
@ -110,11 +60,6 @@ function Editor(): JSX.Element {
});
return null;
}
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return (
<ShortcutProvider>
<SlotFillProvider>
@ -135,12 +80,7 @@ function Editor(): JSX.Element {
</div>
)
}
header={
<Header
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
header={<Header showInserterToggle={showInserterSidebar} />}
content={
<>
<EditorNotices />
@ -152,7 +92,6 @@ function Editor(): JSX.Element {
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
}
/>
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
<Popover.Slot />
</SlotFillProvider>
</ShortcutProvider>
@ -164,7 +103,6 @@ window.addEventListener('DOMContentLoaded', () => {
const root = document.getElementById('mailpoet_automation_editor');
if (root) {
registerTranslations();
registerApiErrorHandler();
initializeApi();
initializeCoreIntegration();

View File

@ -1,7 +1,5 @@
import { dispatch, select, StoreDescriptor } from '@wordpress/data';
import { select } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
import { store as noticesStore } from '@wordpress/notices';
import { __ } from '@wordpress/i18n';
import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences';
import { addQueryArgs } from '@wordpress/url';
@ -9,7 +7,6 @@ import { storeName } from './constants';
import { Feature, State } from './types';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices';
import { MailPoet } from '../../../mailpoet';
import { WorkflowStatus } from '../../listing/workflow';
export const openSidebar =
(key) =>
@ -65,17 +62,6 @@ export function* save() {
data: { ...workflow },
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data) {
void createNotice(
'success',
__('The automation has been saved.', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return {
type: 'SAVE',
workflow: data?.data ?? workflow,
@ -89,72 +75,16 @@ export function* activate() {
method: 'PUT',
data: {
...workflow,
status: WorkflowStatus.ACTIVE,
status: 'active',
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.ACTIVE) {
void createNotice(
'success',
__('Well done! Automation is now activated!', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return {
type: 'ACTIVATE',
workflow: data?.data ?? workflow,
} as const;
}
export function* deactivate(deactivateWorkflowRuns = true) {
const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
...workflow,
status: deactivateWorkflowRuns
? WorkflowStatus.DRAFT
: WorkflowStatus.DEACTIVATING,
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (deactivateWorkflowRuns && data?.data.status === WorkflowStatus.DRAFT) {
void createNotice(
'success',
__('Automation is now deactivated!', 'mailpoet'),
{
type: 'snackbar',
},
);
}
if (
!deactivateWorkflowRuns &&
data?.data.status === WorkflowStatus.DEACTIVATING
) {
void createNotice(
'success',
__(
'Automation is deactivated. But recent users are still going through the flow.',
'mailpoet',
),
{
type: 'snackbar',
},
);
}
return {
type: 'DEACTIVATE',
workflow: data?.data ?? workflow,
} as const;
}
export function* trash(onTrashed: () => void = undefined) {
const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({
@ -162,13 +92,13 @@ export function* trash(onTrashed: () => void = undefined) {
method: 'PUT',
data: {
...workflow,
status: WorkflowStatus.TRASH,
status: 'trash',
},
});
onTrashed?.();
if (data?.status === WorkflowStatus.TRASH) {
if (data?.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id,
});

View File

@ -39,12 +39,6 @@ export function reducer(state: State, action: Action): State {
workflowData: action.workflow,
workflowSaved: true,
};
case 'DEACTIVATE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
};
case 'TRASH':
return {
...state,
@ -67,12 +61,10 @@ export function reducer(state: State, action: Action): State {
? action.value(prevArgs[action.name] ?? undefined)
: action.value;
const args =
value === undefined
? Object.fromEntries(
Object.entries(prevArgs).filter(([name]) => name !== action.name),
)
: { ...prevArgs, [action.name]: value };
const args = {
...prevArgs,
[action.name]: value,
};
const step = { ...state.workflowData.steps[action.stepId], args };

View File

@ -2,7 +2,7 @@ import { createRegistrySelector } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences';
import { storeName } from './constants';
import { Context, Errors, Feature, State, StepErrors, StepType } from './types';
import { Context, Errors, Feature, State, StepError, StepType } from './types';
import { Item } from '../components/inserter/item';
import { Step, Workflow } from '../components/workflow/types';
@ -78,6 +78,6 @@ export function getErrors(state: State): Errors | undefined {
return state.errors;
}
export function getStepError(state: State, id: string): StepErrors | undefined {
export function getStepError(state: State, id: string): StepError | undefined {
return state.errors?.steps[id] ?? undefined;
}

View File

@ -1,4 +1,9 @@
import { createReduxStore, register, StoreDescriptor } from '@wordpress/data';
import {
createReduxStore,
register,
StoreConfig,
StoreDescriptor,
} from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { Hooks } from 'wp-js-hooks';
import * as actions from './actions';
@ -8,7 +13,6 @@ import { reducer } from './reducer';
import * as selectors from './selectors';
import { State } from './types';
import { OmitFirstArgs } from '../../../types';
import { EditorStoreConfigType } from '../../types/filters';
type StoreType = Omit<StoreDescriptor, 'name'> & {
name: typeof storeName;
@ -24,8 +28,8 @@ export const createStore = (): StoreType => {
selectors,
reducer,
initialState: getInitialState(),
} as EditorStoreConfigType,
) as EditorStoreConfigType;
} as StoreConfig<State>,
) as StoreConfig<State>;
const store = createReduxStore<State>(storeName, storeConfig) as StoreType;
register(store);

View File

@ -32,17 +32,15 @@ export type StepType = {
edit: ComponentType;
foreground: string;
background: string;
createStep?: (step: Step, state: State) => Step;
};
export type StepErrors = {
export type StepError = {
step_id: string;
message: string;
fields: Record<string, string>;
};
export type Errors = {
steps: Record<string, StepErrors>;
steps: Record<string, StepError>;
};
export type State = {

View File

@ -1,14 +0,0 @@
import { getLocaleData, setLocaleData } from '@wordpress/i18n';
declare global {
interface Window {
wp: {
i18n: { getLocaleData: typeof getLocaleData };
};
}
}
// We are using "@wordpress/i18n" from our bundle while WordPress initializes
// translation data on the core one — we need to pass the data to our code.
export const registerTranslations = () =>
setLocaleData(window.wp.i18n.getLocaleData('mailpoet'), 'mailpoet');

View File

@ -6,48 +6,27 @@ import {
FlexItem,
} from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { PlainBodyTitle } from '../../../../editor/components/panel';
import { storeName } from '../../../../editor/store';
import { DelayTypeOptions } from './types/delayTypes';
export function Edit(): JSX.Element {
const { selectedStep, errors } = useSelect(
const { selectedStep } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const errorFields = errors?.fields ?? {};
const delayErrorMessage = errorFields?.delay ?? '';
const delayTypeErrorMessage = errorFields?.delay_type ?? '';
const delayValueInputId = `delay-number-${selectedStep.id}`;
return (
<PanelBody opened>
<label htmlFor={delayValueInputId}>
<PlainBodyTitle
title={
// translators: A label for a wait delay time selection form field - time unit follows
__('Wait for', 'mailpoet')
}
/>
</label>
<PlainBodyTitle title="Wait for" />
<Flex align="top">
<FlexItem
style={{ flex: '1 1 0' }}
className={
delayErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<FlexItem style={{ flex: '1 1 0' }}>
<TextControl
id={delayValueInputId}
help={delayErrorMessage}
label=""
type="number"
placeholder={__('Number', 'mailpoet')}
placeholder="Number"
value={(selectedStep.args.delay as string) ?? ''}
onChange={(rawValue) => {
const value: number =
@ -62,15 +41,9 @@ export function Edit(): JSX.Element {
}}
/>
</FlexItem>
<FlexItem
style={{ flex: '1 1 0' }}
className={
delayTypeErrorMessage ? 'mailpoet-automation-field__error' : ''
}
>
<FlexItem style={{ flex: '1 1 0' }}>
<SelectControl
label=""
help={delayTypeErrorMessage}
value={(selectedStep.args.delay_type as string) ?? 'HOURS'}
options={DelayTypeOptions}
onChange={(value) =>

View File

@ -1,4 +1,3 @@
import { __, _x } from '@wordpress/i18n';
import { Icon } from './icon';
import { Edit } from './edit';
import { StepType } from '../../../../editor/store/types';
@ -15,16 +14,13 @@ const getDelayInformation = (delayTypeValue: string, value: number): string =>
export const step: StepType = {
key: 'core:delay',
group: 'actions',
title: _x('Delay', 'noun', 'mailpoet'),
title: 'Delay',
foreground: '#7F54B3',
background: '#f7edf7',
description: __(
'Wait some time before proceeding with the steps below',
'mailpoet',
),
description: 'Wait some time before proceeding with the steps below',
subtitle: (data): string => {
if (!data.args.delay || !data.args.delay_type) {
return __('Not set up yet.', 'mailpoet');
return 'Not set up yet.';
}
return getDelayInformation(

View File

@ -1,44 +1,25 @@
import { SelectControl } from '@wordpress/components';
import { __, _n, sprintf } from '@wordpress/i18n';
export type DelayTypes = SelectControl.Option & {
subtitle: (value: number) => string;
};
export const DelayTypeOptions: DelayTypes[] = [
{
label: __('Minutes', 'mailpoet'),
label: 'Hours',
subtitle: (value: number) =>
sprintf(
_n('Wait for %d minute', 'Wait for %d minutes', value, 'mailpoet'),
value,
),
value: 'MINUTES',
},
{
label: __('Hours', 'mailpoet'),
subtitle: (value: number) =>
sprintf(
_n('Wait for %d hour', 'Wait for %d hours', value, 'mailpoet'),
value,
),
`Wait for ${value} ${value === 1 ? 'hour' : 'hours'}`,
value: 'HOURS',
},
{
label: __('Days', 'mailpoet'),
label: 'Days',
subtitle: (value: number) =>
sprintf(
_n('Wait for %d day', 'Wait for %d days', value, 'mailpoet'),
value,
),
`Wait for ${value} ${value === 1 ? 'day' : 'days'}`,
value: 'DAYS',
},
{
label: __('Weeks', 'mailpoet'),
label: 'Weeks',
subtitle: (value: number) =>
sprintf(
_n('Wait for %d week', 'Wait for %d weeks', value, 'mailpoet'),
value,
),
`Wait for ${value} ${value === 1 ? 'week' : 'weeks'}`,
value: 'WEEKS',
},
];

View File

@ -2,11 +2,9 @@ import { registerStepType } from '../../editor/store';
import { step as SendEmailStep } from './steps/send_email';
import { step as SomeoneSubscribesTrigger } from './steps/someone-subscribes';
import { step as WpUserRegisteredTrigger } from './steps/wp-user-registered';
import { registerStepControls } from './step-controls';
export const initialize = (): void => {
registerStepType(SendEmailStep);
registerStepType(WpUserRegisteredTrigger);
registerStepType(SomeoneSubscribesTrigger);
registerStepControls();
};

View File

@ -1,47 +0,0 @@
import { __ } from '@wordpress/i18n';
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';
const emailStatisticsControl = (step: Step): MoreControlType => {
const hasEmail = step.args?.email_id > 0;
return {
key: 'statistics',
control: {
icon: chartBar,
title: __('Email statistics', 'mailpoet'),
isDisabled: !hasEmail,
onClick: () => {
window.open(
`admin.php?page=mailpoet-newsletters#/stats/${
step.args.email_id as string
}`,
'_blank',
);
},
},
slot: () => null,
};
};
export function registerStepControls() {
Hooks.addFilter(
'mailpoet.automation.workflow.step.more-controls',
'mailpoet',
(
controls: StepMoreControlsType,
step: Step,
stepType: StepType,
): StepMoreControlsType => {
if (stepType.key === 'mailpoet:send-email') {
return {
statistics: emailStatisticsControl(step),
...controls,
};
}
return controls;
},
);
}

View File

@ -1,52 +1,25 @@
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { plus } from '@wordpress/icons';
import { useCallback, useEffect, useState } from 'react';
import { Button } from '../../../components/button';
import { storeName } from '../../../../../editor/store';
import { MailPoet } from '../../../../../../mailpoet';
const emailPreviewLinkCache = {};
const retrievePreviewLink = async (emailId) => {
if (
emailPreviewLinkCache[emailId] &&
emailPreviewLinkCache[emailId].length > 0
) {
return emailPreviewLinkCache[emailId];
}
const response = await MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'get',
data: {
id: emailId,
},
});
emailPreviewLinkCache[emailId] = response?.meta?.preview_url ?? '';
return emailPreviewLinkCache[emailId];
};
export function EditNewsletter(): JSX.Element {
const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
useState(false);
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
const { selectedStep, workflowId, workflowSaved, errors } = useSelect(
const { selectedStep, workflowId, workflowSaved } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id,
workflowSaved: select(storeName).getWorkflowSaved(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id;
const errorFields = errors?.fields ?? {};
const emailIdError = errorFields?.email_id ?? '';
const createEmail = useCallback(async () => {
setRedirectToTemplateSelection(true);
@ -83,26 +56,16 @@ export function EditNewsletter(): JSX.Element {
if (!emailId || redirectToTemplateSelection) {
return (
<div className={emailIdError ? 'mailpoet-automation-field__error' : ''}>
<Button
variant="sidebar-primary"
centered
icon={plus}
onClick={createEmail}
isBusy={redirectToTemplateSelection}
disabled={redirectToTemplateSelection}
>
{__('Design email', 'mailpoet')}
</Button>
{emailIdError && (
<span className="mailpoet-automation-field-message">
{__(
'You need to design an email before you can activate the automation',
'mailpoet',
)}
</span>
)}
</div>
<Button
variant="sidebar-primary"
centered
icon={plus}
onClick={createEmail}
isBusy={redirectToTemplateSelection}
disabled={redirectToTemplateSelection}
>
Design email
</Button>
);
}
@ -115,21 +78,10 @@ export function EditNewsletter(): JSX.Element {
selectedStep.args.email_id as string
}`}
>
{__('Edit content', 'mailpoet')}
Edit content
</Button>
<Button
variant="secondary"
centered
isBusy={fetchingPreviewLink}
disabled={fetchingPreviewLink}
onClick={async () => {
setFetchingPreviewLink(true);
const link = await retrievePreviewLink(emailId);
window.open(link as string, '_blank');
setFetchingPreviewLink(false);
}}
>
{__('Preview', 'mailpoet')}
<Button variant="secondary" centered>
Preview
</Button>
</div>
);

View File

@ -1,7 +1,6 @@
import { ComponentProps } from 'react';
import { PanelBody, TextareaControl, TextControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { ShortcodeHelpText } from './shortcode_help_text';
import { PlainBodyTitle } from '../../../../../editor/components/panel';
import { storeName } from '../../../../../editor/store';
@ -32,21 +31,14 @@ function SingleLineTextareaControl(
}
export function EmailPanel(): JSX.Element {
const { selectedStep, selectedStepType, errors } = useSelect(
const { selectedStep, selectedStepType } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
selectedStepType: select(storeName).getSelectedStepType(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const errorFields = errors?.fields ?? {};
const senderNameErrorMessage = errorFields?.sender_name ?? '';
const senderAddressErrorMessage = errorFields?.sender_address ?? '';
const subjectErrorMessage = errorFields?.subject ?? '';
return (
<PanelBody opened>
<StepName
@ -57,15 +49,8 @@ export function EmailPanel(): JSX.Element {
}}
/>
<TextControl
className={
senderNameErrorMessage ? 'mailpoet-automation-field__error' : ''
}
help={senderNameErrorMessage}
label={__('"From" name', 'mailpoet')}
placeholder={
// translators: A placeholder for a person's name
__('John Doe', 'mailpoet')
}
label="“From” name"
placeholder="John Doe"
value={(selectedStep.args.sender_name as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -76,16 +61,9 @@ export function EmailPanel(): JSX.Element {
}
/>
<TextControl
className={
senderAddressErrorMessage ? 'mailpoet-automation-field__error' : ''
}
help={senderAddressErrorMessage}
type="email"
label={__('"From" email address', 'mailpoet')}
placeholder={
// translators: A placeholder for an email
__('you@domain.com', 'mailpoet')
}
label="“From email address"
placeholder="you@domain.com"
value={(selectedStep.args.sender_address as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -96,25 +74,17 @@ export function EmailPanel(): JSX.Element {
}
/>
<SingleLineTextareaControl
className={
subjectErrorMessage ? 'mailpoet-automation-field__error' : ''
}
label={__('Subject', 'mailpoet')}
placeholder={__('Type in subject…', 'mailpoet')}
label="Subject"
placeholder="Type in subject…"
value={(selectedStep.args.subject as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(selectedStep.id, 'subject', value)
}
help={
<>
{`${subjectErrorMessage} `}
<ShortcodeHelpText />
</>
}
help={<ShortcodeHelpText />}
/>
<SingleLineTextareaControl
label={__('Preheader', 'mailpoet')}
placeholder={__('Type in preheader…', 'mailpoet')}
label="Preheader"
placeholder="Type in preheader…"
value={(selectedStep.args.preheader as string) ?? ''}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
@ -127,7 +97,7 @@ export function EmailPanel(): JSX.Element {
/>
<div className="mailpoet-automation-email-content-separator" />
<PlainBodyTitle title={__('Email', 'mailpoet')} />
<PlainBodyTitle title="Email" />
<EditNewsletter />
</PanelBody>
);

View File

@ -1,36 +1,27 @@
import { useState } from 'react';
import { ToggleControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { PremiumModal } from 'common/premium_modal';
import { Hooks } from 'wp-js-hooks';
import { storeName } from '../../../../../editor/store';
import { GoogleAnalyticsPanelBodyType } from '../../../types/filters';
import { PanelBody } from '../../../../../editor/components/panel/panel-body';
export function GoogleAnalyticsPanel(): JSX.Element {
const { selectedStep, errors } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
)?.fields?.ga_campaign,
}),
const { selectedStep } = useSelect(
(select) => ({ selectedStep: select(storeName).getSelectedStep() }),
[],
);
const { updateStepArgs } = useDispatch(storeName);
const hasValue = typeof selectedStep.args?.ga_campaign !== 'undefined';
const [enabled, setEnabled] = useState(hasValue);
const panelBody: GoogleAnalyticsPanelBodyType = Hooks.applyFilters(
const enabled = typeof selectedStep.args?.ga_campaign !== 'undefined';
const panelBody = Hooks.applyFilters(
'mailpoet.automation.send_email.google_analytics_panel',
<PremiumModal
onRequestClose={() => {
setEnabled(false);
updateStepArgs(selectedStep.id, 'ga_campaign', undefined);
}}
onRequestClose={() =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
undefined,
)
}
>
{__(
'Google Analytics tracking is not available in the free version of the MailPoet plugin.',
@ -40,20 +31,17 @@ export function GoogleAnalyticsPanel(): JSX.Element {
);
return (
<PanelBody
title={__('Google Analytics', 'mailpoet')}
initialOpen={false}
hasErrors={!!errors}
>
<PanelBody title="Google analytics" initialOpen={false}>
<ToggleControl
label={__('Enable custom GA tracking', 'mailpoet')}
label="Enable custom GA tracking"
checked={enabled}
onChange={(value) => {
setEnabled(value);
if (!value) {
updateStepArgs(selectedStep.id, 'ga_campaign', undefined);
}
}}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
value ? '' : undefined,
)
}
/>
{enabled && panelBody}

View File

@ -1,113 +1,67 @@
import { useRef, useState } from 'react';
import { TextControl, ToggleControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../../../../editor/store';
import { PanelBody } from '../../../../../editor/components/panel/panel-body';
type ReplyToArgs = {
reply_to_name?: string;
reply_to_address?: string;
};
export function ReplyToPanel(): JSX.Element {
const { context, selectedStep, errors } = useSelect(
const { selectedStep } = useSelect(
(select) => ({
context: select(storeName).getContext(),
selectedStep: select(storeName).getSelectedStep(),
errors: select(storeName).getStepError(
select(storeName).getSelectedStep().id,
),
}),
[],
);
const { updateStepArgs } = useDispatch(storeName);
const replyToName = selectedStep.args.reply_to_name as string | undefined;
const replyToAddress = selectedStep.args.reply_to_address as
| string
| undefined;
const args = selectedStep.args as ReplyToArgs;
const hasValue = !!args.reply_to_name || !!args.reply_to_address;
const [expanded, setExpanded] = useState(hasValue);
const prevValue = useRef<{ name?: string; address?: string }>();
const enabled =
typeof replyToName !== 'undefined' || typeof replyToAddress !== 'undefined';
// defaults
const argsContext =
context.steps['mailpoet:send-email']?.args_schema?.properties ?? {};
const defaultName = argsContext.reply_to_name?.default;
const defaultAddress = argsContext.reply_to_address?.default;
const errorFields = errors?.fields ?? {};
const replyToNameError = errorFields?.reply_to_name ?? '';
const replyToAddressError = errorFields?.reply_to_address ?? '';
return (
<PanelBody
title={__('Reply to', 'mailpoet')}
initialOpen={false}
hasErrors={!!replyToNameError || !!replyToAddressError}
>
<PanelBody title="Reply to" initialOpen={false}>
<ToggleControl
label={__(
'Use different email address for getting replies to the email',
'mailpoet',
)}
checked={expanded}
label="Use different email address for getting replies to the email"
checked={enabled}
onChange={(value) => {
setExpanded(value);
const stepId = selectedStep.id;
if (value) {
const name = prevValue.current?.name ?? defaultName;
const address = prevValue.current?.address ?? defaultAddress;
updateStepArgs(stepId, 'reply_to_name', name);
updateStepArgs(stepId, 'reply_to_address', address);
} else {
prevValue.current = {
name: args.reply_to_name,
address: args.reply_to_address,
};
updateStepArgs(stepId, 'reply_to_name', undefined);
updateStepArgs(stepId, 'reply_to_address', undefined);
}
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_name',
value ? '' : undefined,
);
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_address',
value ? '' : undefined,
);
}}
/>
{expanded && (
{enabled && (
<>
<TextControl
className={
replyToNameError ? 'mailpoet-automation-field__error' : ''
}
help={replyToNameError}
label={__('"Reply to" name', 'mailpoet')}
placeholder={
// translators: A placeholder for a person's name
__('John Doe', 'mailpoet')
}
value={args.reply_to_name ?? ''}
label="“Reply to” name"
placeholder="John Doe"
value={replyToName ?? ''}
onChange={(value) =>
updateStepArgs(
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_name',
value || undefined,
value,
)
}
/>
<TextControl
className={
replyToAddressError ? 'mailpoet-automation-field__error' : ''
}
help={replyToAddressError}
type="email"
label={__('"Reply to" email address', 'mailpoet')}
placeholder={
// translators: A placeholder for an email
__('you@domain.com', 'mailpoet')
}
value={args.reply_to_address ?? ''}
label="“Reply to email address"
placeholder="you@domain.com"
value={replyToAddress ?? ''}
onChange={(value) =>
updateStepArgs(
dispatch(storeName).updateStepArgs(
selectedStep.id,
'reply_to_address',
value || undefined,
value,
)
}
/>

View File

@ -1,5 +1,3 @@
import { __ } from '@wordpress/i18n';
export function ShortcodeHelpText(): JSX.Element {
return (
<span className="mailpoet-shortcode-selector">
@ -10,7 +8,7 @@ export function ShortcodeHelpText(): JSX.Element {
rel="noopener noreferrer"
data-beacon-article="59d662ef042863379ddc6faa"
>
{__('MailPoet shortcodes', 'mailpoet')}
MailPoet shortcodes
</a>
</span>
);

View File

@ -1,6 +1,5 @@
import { ComponentProps, ComponentType, useEffect, useState } from 'react';
import { Spinner as WpSpinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { MailPoetAjax } from '../../../../../../ajax';
// @types/wordpress__components don't define "className", which is supported
@ -40,7 +39,7 @@ export function Thumbnail({ emailId }: Props): JSX.Element {
<img
className="mailpoet-automation-thumbnail-image"
src={thumbnailUrl}
alt={__('Email thumbnail', 'mailpoet')}
alt="Email thumbnail"
/>
</div>
) : (

View File

@ -1,25 +1,15 @@
import { __ } from '@wordpress/i18n';
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 { StepType } from '../../../../editor/store/types';
export const step: StepType = {
key: 'mailpoet:send-email',
group: 'actions',
title: __('Send email', 'mailpoet'),
description: __('An email will be sent to subscriber', 'mailpoet'),
subtitle: (data) =>
(data.args.name as string) ?? __('Send email', 'mailpoet'),
title: 'Send email',
description: 'An email will be sent to subscriber',
subtitle: (data) => (data.args.name as string) ?? 'Send email',
foreground: '#996800',
background: '#FCF9E8',
icon: Icon,
edit: Edit,
createStep: (stepData: Step, state: State) =>
Hooks.applyFilters(
'mailpoet.automation.send_email.create_step',
stepData,
state.workflowData.id,
),
} as const;

View File

@ -30,7 +30,7 @@ export function ListPanel(): JSX.Element {
<FormTokenField
label={__(
'When someone subscribes to the following lists:',
'When someone subscribers to the following list(s):',
'mailpoet',
)}
placeholder={__('Any list', 'mailpoet')}

View File

@ -1,4 +1,4 @@
import { __, _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { commentAuthorAvatar } from '@wordpress/icons';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
@ -13,7 +13,7 @@ export const step: StepType = {
'Starts the automation when a new subscriber is added to MailPoet.',
'mailpoet',
),
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
subtitle: () => __('Trigger', 'mailpoet'),
icon: () => (
<div style={{ width: '100%', height: '100%', scale: '1.4' }}>
{commentAuthorAvatar}

View File

@ -1,31 +1,11 @@
import { PanelBody } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import ReactStringReplace from 'react-string-replace';
import { storeName } from '../../../../../editor/store';
import { PlainBodyTitle } from '../../../../../editor/components/panel';
import { userRoles } from './role';
import { FormTokenField } from '../../../components/form-token-field';
function SettingsInfoText(): JSX.Element {
return (
<p>
{ReactStringReplace(
__(
'[link]Subscribe in registration form[/link] setting must be enabled.',
'mailpoet',
),
/\[link\](.*?)\[\/link\]/g,
(match) => (
<a href="admin.php?page=mailpoet-settings#/basics" target="_blank">
{match}
</a>
),
)}
</p>
);
}
export function RolePanel(): JSX.Element {
const { selectedStep } = useSelect(
(select) => ({
@ -40,11 +20,9 @@ export function RolePanel(): JSX.Element {
const selected = userRoles.filter((role): boolean =>
rawSelected.includes(role.id as string),
);
return (
<PanelBody opened>
<PlainBodyTitle title={__('Trigger settings', 'mailpoet')} />
<SettingsInfoText />
<FormTokenField
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@ -1,4 +1,4 @@
import { __, _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { wordpress } from '@wordpress/icons';
import { StepType } from '../../../../editor/store';
import { Edit } from './edit';
@ -13,7 +13,7 @@ export const step: StepType = {
'Starts the automation when a new user registered in WordPress.',
'mailpoet',
),
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
subtitle: () => __('Trigger', 'mailpoet'),
icon: () => (
<div style={{ width: '100%', height: '100%', scale: '1.12' }}>
{wordpress}

View File

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

View File

@ -1,36 +0,0 @@
import apiFetch, { APIFetchOptions } from '@wordpress/api-fetch';
import { dispatch, StoreDescriptor } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { ApiError } from '../api';
export const registerApiErrorHandler = (): void =>
apiFetch.use(
async (
options: APIFetchOptions,
next: (nextOptions: APIFetchOptions) => Promise<unknown>,
) => {
try {
const result = await next(options);
return result;
} catch (error) {
const errorObject = error as ApiError;
const status = errorObject.data?.status;
if (status && status >= 400 && status < 500) {
const message = errorObject.message;
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
message ?? __('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
return undefined;
}
void dispatch(noticesStore as StoreDescriptor).createErrorNotice(
__('An unknown error occurred.', 'mailpoet'),
{ explicitDismiss: true },
);
throw error;
}
},
);

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
type Props = {
workflow: Workflow;
label?: string;
};
export function Edit({ workflow, label }: Props): JSX.Element {
return (
<a href={`admin.php?page=mailpoet-automation-editor&id=${workflow.id}`}>
{label ?? __('Edit', 'mailpoet')}
</a>
);
}

View File

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

View File

@ -0,0 +1,25 @@
import { EllipsisMenu, MenuItem } from '@woocommerce/components/build';
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
type Props = {
workflow: Workflow;
};
export function More({ workflow }: Props): JSX.Element {
return (
<EllipsisMenu
label={`Actions for ${workflow.name}`}
renderContent={() => (
<div>
<MenuItem onInvoke={() => {}}>
<p>{__('Duplicate', 'mailpoet')}</p>
</MenuItem>
<MenuItem onInvoke={() => {}}>
<p>{__('Move to trash', 'mailpoet')}</p>
</MenuItem>
</div>
)}
/>
);
}

View File

@ -1,4 +1,4 @@
import { EditWorkflow } from '../actions';
import { Edit } from './edit';
import { Workflow } from '../../workflow';
type Props = {
@ -6,5 +6,5 @@ type Props = {
};
export function Name({ workflow }: Props): JSX.Element {
return <EditWorkflow workflow={workflow} label={workflow.name} />;
return <Edit workflow={workflow} label={workflow.name} />;
}

View File

@ -1,4 +1,6 @@
import { useState } from 'react';
import { __ } from '@wordpress/i18n';
import { ToggleControl } from '@wordpress/components';
import { Workflow, WorkflowStatus } from '../../workflow';
type Props = {
@ -6,8 +8,14 @@ type Props = {
};
export function Status({ workflow }: Props): JSX.Element {
const [isActive, setIsActive] = useState(workflow.status === 'active');
return (
<div className="mailpoet-automation-listing-cell-status">
<ToggleControl
checked={isActive}
onChange={(active) => setIsActive(active)}
/>
{workflow.status === WorkflowStatus.ACTIVE
? __('Active', 'mailpoet')
: __('Not active', 'mailpoet')}

View File

@ -1,6 +1,5 @@
import { _x } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { Workflow } from '../../workflow';
import { Statistics } from '../../../components/statistics';
type Props = {
workflow: Workflow;
@ -8,28 +7,25 @@ type Props = {
export function Subscribers({ workflow }: Props): JSX.Element {
return (
<Statistics
labelPosition="after"
items={[
{
key: 'entered',
// translators: Total number of subscribers who entered an automation
label: _x('Entered', 'automation stats', 'mailpoet'),
value: workflow.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,
},
{
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,
},
]}
/>
<ul className="mailpoet-automation-stats">
<li className="mailpoet-automation-stats-item">
{new Intl.NumberFormat().format(workflow.stats.totals.entered)}
<span className="mailpoet-automation-stats-label">
{__('Entered', 'mailpoet')}
</span>
</li>
<li className="mailpoet-automation-stats-item">
{new Intl.NumberFormat().format(workflow.stats.totals.in_progress)}
<span className="mailpoet-automation-stats-label">
{__('Processing', 'mailpoet')}
</span>
</li>
<li className="mailpoet-automation-stats-item">
{new Intl.NumberFormat().format(workflow.stats.totals.exited)}
<span className="mailpoet-automation-stats-label">
{__('Exited', 'mailpoet')}
</span>
</li>
</ul>
);
}

View File

@ -1,44 +0,0 @@
import { useState } from 'react';
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useDeleteButton = (workflow: Workflow): Item | undefined => {
const [showDialog, setShowDialog] = useState(false);
const { deleteWorkflow } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'delete',
control: {
title: __('Delete permanently', 'mailpoet'),
icon: null,
onClick: () => setShowDialog(true),
},
slot: (
<ConfirmDialog
isOpen={showDialog}
title={__('Permanently delete automation', 'mailpoet')}
confirmButtonText={__('Yes, permanently delete', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => deleteWorkflow(workflow)}
onCancel={() => setShowDialog(false)}
>
{sprintf(
// translators: %s is the automation name
__(
'Are you sure you want to permanently delete "%s" and all associated data? This cannot be undone!',
'mailpoet',
),
workflow.name,
)}
</ConfirmDialog>
),
};
};

View File

@ -1,22 +0,0 @@
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useDuplicateButton = (workflow: Workflow): Item | undefined => {
const { duplicateWorkflow } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'duplicate',
control: {
title: __('Duplicate', 'mailpoet'),
icon: null,
onClick: () => duplicateWorkflow(workflow),
},
};
};

View File

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

View File

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

View File

@ -1,22 +0,0 @@
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useRestoreButton = (workflow: Workflow): Item | undefined => {
const { restoreWorkflow } = useDispatch(storeName);
if (workflow.status !== WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'restore',
control: {
title: __('Restore', 'mailpoet'),
icon: null,
onClick: () => restoreWorkflow(workflow, WorkflowStatus.DRAFT),
},
};
};

View File

@ -1,44 +0,0 @@
import { useState } from 'react';
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { __, _x, sprintf } from '@wordpress/i18n';
import { Item } from './item';
import { storeName } from '../../store';
import { Workflow, WorkflowStatus } from '../../workflow';
export const useTrashButton = (workflow: Workflow): Item | undefined => {
const [showDialog, setShowDialog] = useState(false);
const { trashWorkflow } = useDispatch(storeName);
if (workflow.status === WorkflowStatus.TRASH) {
return undefined;
}
return {
key: 'trash',
control: {
title: _x('Trash', 'verb', 'mailpoet'),
icon: null,
onClick: () => setShowDialog(true),
},
slot: (
<ConfirmDialog
isOpen={showDialog}
title={__('Trash automation', 'mailpoet')}
confirmButtonText={__('Yes, move to trash', 'mailpoet')}
__experimentalHideHeader={false}
onConfirm={() => trashWorkflow(workflow)}
onCancel={() => setShowDialog(false)}
>
{sprintf(
// translators: %s is the automation name
__(
'Are you sure you want to move the automation "%s" to the Trash?',
'mailpoet',
),
workflow.name,
)}
</ConfirmDialog>
),
};
};

View File

@ -1,49 +0,0 @@
import { StoreDescriptor, useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { Notice } from '../../../../notices/notice';
export function Notices(): JSX.Element {
const { notices } = useSelect(
(select) => ({
notices: select(noticesStore as StoreDescriptor).getNotices(),
}),
[],
);
const { removeNotice } = useDispatch(noticesStore as StoreDescriptor);
const dismissibleNotices = notices.filter(
({ isDismissible, type }) => isDismissible && type === 'default',
);
const nonDismissibleNotices = notices.filter(
({ isDismissible, type }) => !isDismissible && type === 'default',
);
return (
<>
{nonDismissibleNotices
.reverse()
.map(({ id, status, content, __unstableHTML }) => (
<Notice key={id} renderInPlace type={status} timeout={false}>
{__unstableHTML ?? <p>{content}</p>}
</Notice>
))}
{dismissibleNotices
.reverse()
.map(({ id, status, content, __unstableHTML }) => (
<Notice
key={id}
type={status}
renderInPlace
timeout={false}
closable
onClose={removeNotice}
>
{__unstableHTML ?? <p>{content}</p>}
</Notice>
))}
</>
);
}

View File

@ -1,5 +1,5 @@
import { Workflow } from './workflow';
import { Actions, Name, Status, Subscribers } from './components/cells';
import { Edit, More, Name, Status, Subscribers } from './components/cells';
export function getRow(workflow: Workflow): object[] {
return [
@ -21,7 +21,12 @@ export function getRow(workflow: Workflow): object[] {
{
id: workflow.id,
value: null,
display: <Actions workflow={workflow} />,
display: <Edit workflow={workflow} />,
},
{
id: workflow.id,
value: null,
display: <More workflow={workflow} />,
},
];
}

View File

@ -1,66 +1,53 @@
import { TableCard } from '@woocommerce/components/build';
import { Button, Flex, TabPanel } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n';
import { useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
import { Search, TableCard } from '@woocommerce/components/build';
import { TabPanel } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useCallback, useMemo } from 'react';
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 { MailPoet } from '../../mailpoet';
const tabConfig = [
type Props = {
workflows: Workflow[];
loading: boolean;
};
const filterTabs = [
{
name: 'all',
title: __('All', 'mailpoet'),
title: 'All',
className: 'mailpoet-tab-all',
},
{
name: WorkflowStatus.ACTIVE,
title: __('Active', 'mailpoet'),
title: 'Active',
className: 'mailpoet-tab-active',
},
{
name: WorkflowStatus.INACTIVE,
title: 'Inactive',
className: 'mailpoet-tab-inactive',
},
{
name: WorkflowStatus.DRAFT,
title: _x('Draft', 'noun', 'mailpoet'),
title: 'Draft',
className: 'mailpoet-tab-draft',
},
{
name: WorkflowStatus.TRASH,
title: _x('Trash', 'noun', 'mailpoet'),
title: 'Trash',
className: 'mailpoet-tab-trash',
},
] as const;
const tableHeaders = [
{
key: 'name',
label: __('Name', 'mailpoet'),
cellClassName: 'mailpoet-automation-listing-cell-name',
},
{ key: 'name', label: __('Name', 'mailpoet') },
{ key: 'subscribers', label: __('Subscribers', 'mailpoet') },
{ key: 'status', label: __('Status', 'mailpoet') },
{ key: 'actions' },
{ key: 'edit' },
{ key: 'more' },
] as const;
export function AutomationListingHeader(): JSX.Element {
return (
<Flex className="mailpoet-automation-listing-heading">
<h1 className="wp-heading-inline">{__('Automations', 'mailpoet')}</h1>
<Button
href={MailPoet.urls.automationTemplates}
icon={plusIcon}
variant="primary"
className="mailpoet-add-new-button"
>
{__('New automation', 'mailpoet')}
</Button>
</Flex>
);
}
export function AutomationListing(): JSX.Element {
export function AutomationListing({ workflows, loading }: Props): JSX.Element {
const history = useHistory();
const location = useLocation();
const pageSearch = useMemo(
@ -68,22 +55,6 @@ export function AutomationListing(): JSX.Element {
[location],
);
const workflows = useSelect((select) => select(storeName).getWorkflows());
const { loadWorkflows } = useDispatch(storeName);
const status = pageSearch.get('status');
useEffect(() => {
loadWorkflows();
}, [loadWorkflows]);
// focus tab button on status change (needed due to the force re-mount below)
useLayoutEffect(() => {
if (status) {
document.querySelector<HTMLElement>(`.mailpoet-tab-${status}`)?.focus();
}
}, [status]);
const updateUrlSearchString = useCallback(
(search: Record<string, string>) => {
const newSearch = new URLSearchParams({
@ -104,8 +75,10 @@ export function AutomationListing(): JSX.Element {
);
const groupedWorkflows = useMemo<Record<string, Workflow[]>>(() => {
const grouped = { all: [] };
(workflows ?? []).forEach((workflow) => {
const grouped = {
all: [],
};
workflows.forEach((workflow) => {
if (!grouped[workflow.status]) {
grouped[workflow.status] = [];
}
@ -119,27 +92,30 @@ export function AutomationListing(): JSX.Element {
const tabs = useMemo(
() =>
tabConfig.map((tab) => {
const count = (groupedWorkflows[tab.name] ?? []).length;
filterTabs.map((filterTab) => {
const count = (groupedWorkflows[filterTab.name] || []).length;
return {
name: tab.name,
title: (
<>
<span>{tab.title}</span>
{count > 0 && <span className="count">{count}</span>}
</>
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- typed as string but supports JSX
className: tab.className,
name: filterTab.name,
title:
count > 0 ? (
<>
<span>{filterTab.title}</span>
<span className="count">{count}</span>
</>
) : (
<span>{filterTab.title}</span>
),
className: filterTab.className,
};
}),
[groupedWorkflows],
);
const renderTabs = useCallback(
const tabRenderer = useCallback(
(tab) => {
const filteredWorkflows: Workflow[] = groupedWorkflows[tab.name] ?? [];
const rowsPerPage = parseInt(pageSearch.get('per_page') ?? '25', 10);
const currentPage = parseInt(pageSearch.get('paged') ?? '1', 10);
const rowsPerPage = parseInt(pageSearch.get('per_page') || '25', 10);
const currentPage = parseInt(pageSearch.get('paged') || '1', 10);
const start = (currentPage - 1) * rowsPerPage;
const rows = filteredWorkflows
.map((workflow) => getRow(workflow))
@ -149,7 +125,7 @@ export function AutomationListing(): JSX.Element {
<TableCard
className="mailpoet-automation-listing"
title=""
isLoading={!workflows}
isLoading={loading}
headers={tableHeaders}
rows={rows}
rowKey={(_, i) => filteredWorkflows[i].id}
@ -159,26 +135,48 @@ export function AutomationListing(): JSX.Element {
}}
totalRows={filteredWorkflows.length}
query={Object.fromEntries(pageSearch)}
hasSearch
showMenu={false}
actions={[
<Search
className="mailpoet-automation-listing-search"
allowFreeTextSearch
inlineTags
key="search"
// onChange={ onSearchChange }
// placeholder={
// labels.placeholder ||
// __( 'Search by item name', 'woocommerce' )
// }
// selected={ searchedLabels }
type="custom"
disabled={loading || workflows.length === 0}
autocompleter={{}}
/>,
]}
/>
);
},
[workflows, groupedWorkflows, pageSearch, updateUrlSearchString],
[workflows, groupedWorkflows, pageSearch, loading, updateUrlSearchString],
);
return (
<TabPanel
className="mailpoet-filter-tab-panel"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the Tab type actually expects a string for titles but won't render HTML,
// making it very difficult to style the count badges. It seems to be compatible with JSX
// elements, however.
tabs={tabs}
onSelect={(tabName) => {
if (status !== tabName) {
if (pageSearch.get('status') !== tabName) {
updateUrlSearchString({ status: tabName });
}
}}
initialTabName={status ?? 'all'}
key={status} // force re-mount on history change to switch tab (via "initialTabName")
initialTabName={pageSearch.get('status') || 'all'}
key={pageSearch.get('status')} // Force re-render on browser forward/back
>
{renderTabs}
{tabRenderer}
</TabPanel>
);
}

View File

@ -1,115 +0,0 @@
import { dispatch, StoreDescriptor } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { Workflow, WorkflowStatus } from '../workflow';
import { EditWorkflow, UndoTrashButton } from '../components/actions';
const createSuccessNotice = (content: string, options?: unknown) =>
dispatch(noticesStore as StoreDescriptor).createSuccessNotice(
content,
options,
);
const removeNotice = (id: string) =>
dispatch(noticesStore as StoreDescriptor).removeNotice(id);
export function* loadWorkflows() {
const data = yield apiFetch({
path: `/workflows`,
});
return {
type: 'SET_WORKFLOWS',
workflows: data.data,
} as const;
}
export function* duplicateWorkflow(workflow: Workflow) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}/duplicate`,
method: 'POST',
});
void createSuccessNotice(
// translators: %s is the automation name
sprintf(__('Automation "%s" was duplicated.', 'mailpoet'), workflow.name),
);
return {
type: 'ADD_WORKFLOW',
workflow: data.data,
} as const;
}
export function* trashWorkflow(workflow: Workflow) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
status: WorkflowStatus.TRASH,
},
});
const message = __('1 automation moved to the Trash.', 'mailpoet');
void createSuccessNotice(message, {
id: `workflow-trashed-${workflow.id}`,
__unstableHTML: (
<p>
{message}{' '}
<UndoTrashButton workflow={workflow} previousStatus={workflow.status} />
</p>
),
});
return {
type: 'UPDATE_WORKFLOW',
workflow: data.data,
} as const;
}
export function* restoreWorkflow(workflow: Workflow, status: WorkflowStatus) {
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
status,
},
});
void removeNotice(`workflow-trashed-${workflow.id}`);
const message = __('1 automation restored from the Trash.', 'mailpoet');
void createSuccessNotice(message, {
__unstableHTML: (
<p>
{message}{' '}
<EditWorkflow
workflow={workflow}
label={__('Edit automation', 'mailpoet')}
/>
</p>
),
});
return {
type: 'UPDATE_WORKFLOW',
workflow: data.data,
} as const;
}
export function* deleteWorkflow(workflow: Workflow) {
yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'DELETE',
});
void createSuccessNotice(
__('1 automation and all associated data permanently deleted.', 'mailpoet'),
);
return {
type: 'DELETE_WORKFLOW',
workflow,
} as const;
}

View File

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

View File

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

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