Compare commits

..

1 Commits

Author SHA1 Message Date
b431d55eb7 Release 3.93.1 2022-08-02 13:10:38 +02:00
884 changed files with 13660 additions and 26788 deletions

View File

@ -71,13 +71,6 @@ anchors:
- trunk - trunk
- release - release
only_trunk_and_cot: &only_trunk_and_cot
filters:
branches:
only:
- trunk
- /^cot-.*/
multisite_acceptance_config: &multisite_acceptance_config multisite_acceptance_config: &multisite_acceptance_config
multisite: 1 multisite: 1
requires: requires:
@ -186,10 +179,10 @@ jobs:
- run: - run:
name: Download additional WP Plugins for tests name: Download additional WP Plugins for tests
command: | command: |
./do download:woo-commerce-zip 6.8.2 ./do download:woo-commerce-zip 6.2.0
./do download:woo-commerce-subscriptions-zip 4.5.1 ./do download:woo-commerce-subscriptions-zip 3.0.14
./do download:woo-commerce-memberships-zip 1.23.0 ./do download:woo-commerce-memberships-zip 1.22.9
./do download:woo-commerce-blocks-zip 8.4.0 ./do download:woo-commerce-blocks-zip 7.2.1
- run: - run:
name: Dump tests ENV variables for acceptance tests name: Dump tests ENV variables for acceptance tests
command: | command: |
@ -318,14 +311,11 @@ jobs:
parallelism: 20 parallelism: 20
working_directory: /home/circleci/mailpoet/mailpoet working_directory: /home/circleci/mailpoet/mailpoet
machine: machine:
image: ubuntu-2204:2022.07.1 image: ubuntu-2004:202111-01
parameters: parameters:
multisite: multisite:
type: integer type: integer
default: 0 default: 0
group:
type: string
default: ''
mysql_command: mysql_command:
type: string type: string
default: '' default: ''
@ -350,15 +340,6 @@ jobs:
woo_blocks_version: woo_blocks_version:
type: string type: string
default: '' default: ''
enable_cot:
type: integer
default: 0
enable_cot_sync:
type: integer
default: 0
allow_fail:
type: integer
default: 0
environment: environment:
MYSQL_COMMAND: << parameters.mysql_command >> MYSQL_COMMAND: << parameters.mysql_command >>
MYSQL_IMAGE_VERSION: << parameters.mysql_image_version >> MYSQL_IMAGE_VERSION: << parameters.mysql_image_version >>
@ -410,52 +391,23 @@ jobs:
name: Group acceptance tests name: Group acceptance tests
command: | command: |
# Convert test result filename values to be relative paths because the circleci CLI's split command requires exact matches # 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
sed -i.bak 's#/wp-core/wp-content/plugins/mailpoet/##g' $CIRCLE_INTERNAL_TASK_DATA/circle-test-results/results.json
fi
# `circleci tests split` returns different values based on the container it's run on # `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 circleci tests glob "tests/acceptance/**/*Cest.php" | circleci tests split --split-by=timings > tests/acceptance/_groups/circleci_split_group
if [[ -n '<< parameters.group >>' ]]; then
grep -rw 'tests/acceptance' -e '@group << parameters.group >>' | sed -e "s/:.*//" | circleci tests split --split-by=timings > tests/acceptance/_groups/circleci_split_group
else
circleci tests glob "tests/acceptance/**/*Cest.php" | circleci tests split --split-by=timings > tests/acceptance/_groups/circleci_split_group
fi
cat tests/acceptance/_groups/circleci_split_group cat tests/acceptance/_groups/circleci_split_group
- run: - run:
name: Run acceptance tests name: Run acceptance tests
command: | command: |
mkdir -m 777 -p tests/_output/exceptions mkdir -m 777 -p tests/_output/exceptions
cd tests/docker cd tests/docker
args=( docker-compose run -e SKIP_DEPS=1 -e CIRCLE_BRANCH=${CIRCLE_BRANCH} -e CIRCLE_JOB=${CIRCLE_JOB} -e MULTISITE=<< parameters.multisite >> codeception_acceptance -g circleci_split_group --steps --debug -vvv --html --xml
--steps - run:
--debug name: Check exceptions
-vvv command: |
--html if [ "$(ls tests/_output/exceptions/*.html)" ]; then
--xml echo "There were some exceptions during the tests run"
-g circleci_split_group exit 1
)
if [[ << parameters.allow_fail >> == 1 ]]; then
args+=(--no-exit)
fi fi
docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
-e MULTISITE=<< parameters.multisite >> \
-e ENABLE_COT=<< parameters.enable_cot >> \
-e ENABLE_COT_SYNC=<< parameters.enable_cot_sync >> \
codeception_acceptance "${args[@]}"
- 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: - store_artifacts:
path: tests/_output path: tests/_output
- store_test_results: - store_test_results:
@ -494,37 +446,38 @@ jobs:
integration_tests: integration_tests:
working_directory: /home/circleci/mailpoet/mailpoet working_directory: /home/circleci/mailpoet/mailpoet
machine: machine:
image: ubuntu-2204:2022.07.1 image: ubuntu-2004:202111-01
environment: environment:
CODECEPTION_IMAGE_VERSION: << parameters.codeception_image_version >> CODECEPTION_IMAGE_VERSION: << parameters.codeception_image_version >>
parameters: parameters:
codeception_image_version: codeception_image_version:
type: string type: string
default: '' default: ''
group:
type: string
default: ''
skip_group:
type: string
default: ''
skip_plugins:
type: integer
default: 0
enable_cot:
type: integer
default: 0
enable_cot_sync:
type: integer
default: 0
multisite: multisite:
type: integer type: integer
default: 0 default: 0
woo_core_version: run_command:
type: string type: string
default: '' default: |
allow_fail: mkdir -m 777 -p tests/_output/exceptions
type: integer cd tests/docker
default: 0 docker-compose run -e SKIP_DEPS=1 \
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
-e CIRCLE_JOB=${CIRCLE_JOB} \
-e WP_TEST_MAILER_ENABLE_SENDING=${WP_TEST_MAILER_ENABLE_SENDING} \
-e WP_TEST_ENABLE_NETWORK_TESTS=${WP_TEST_ENABLE_NETWORK_TESTS} \
-e WP_TEST_MAILER_MAILPOET_API=${WP_TEST_MAILER_MAILPOET_API} \
-e WP_TEST_MAILER_SENDGRID_API=${WP_TEST_MAILER_SENDGRID_API} \
-e WP_TEST_MAILER_AMAZON_ACCESS=${WP_TEST_MAILER_AMAZON_ACCESS} \
-e WP_TEST_MAILER_AMAZON_REGION=${WP_TEST_MAILER_AMAZON_REGION} \
-e WP_TEST_MAILER_AMAZON_SECRET=${WP_TEST_MAILER_AMAZON_SECRET} \
-e WP_TEST_IMPORT_MAILCHIMP_API=${WP_TEST_IMPORT_MAILCHIMP_API} \
-e WP_TEST_IMPORT_MAILCHIMP_LISTS=${WP_TEST_IMPORT_MAILCHIMP_LISTS} \
-e WP_TEST_MAILER_SMTP_HOST=${WP_TEST_MAILER_SMTP_HOST} \
-e WP_TEST_MAILER_SMTP_LOGIN=${WP_TEST_MAILER_SMTP_LOGIN} \
-e WP_TEST_MAILER_SMTP_PASSWORD=${WP_TEST_MAILER_SMTP_PASSWORD} \
-e MULTISITE=<< parameters.multisite >> \
codeception_integration --steps --debug -vvv --html --xml
steps: steps:
- attach_workspace: - attach_workspace:
at: /home/circleci at: /home/circleci
@ -532,55 +485,9 @@ jobs:
name: 'Pull test docker images' name: 'Pull test docker images'
# Pull docker images with 3 retries # Pull docker images with 3 retries
command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
- when:
condition: << parameters.woo_core_version >>
steps:
- run:
name: Download WooCommerce Core
command: |
cd tests/docker
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip << parameters.woo_core_version >>" --no-deps codeception_integration
- run: - run:
name: 'PHP Integration tests' name: 'PHP Integration tests'
command: | command: << parameters.run_command >>
mkdir -m 777 -p tests/_output/exceptions
cd tests/docker
args=(
--steps
--debug
-vvv
--html
--xml
)
if [[ -n '<< parameters.group >>' ]]; then
args+=(--group << parameters.group >>)
fi
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} \
-e SKIP_PLUGINS=<< parameters.skip_plugins >> \
-e WP_TEST_MAILER_ENABLE_SENDING=${WP_TEST_MAILER_ENABLE_SENDING} \
-e WP_TEST_ENABLE_NETWORK_TESTS=${WP_TEST_ENABLE_NETWORK_TESTS} \
-e WP_TEST_MAILER_MAILPOET_API=${WP_TEST_MAILER_MAILPOET_API} \
-e WP_TEST_MAILER_SENDGRID_API=${WP_TEST_MAILER_SENDGRID_API} \
-e WP_TEST_MAILER_AMAZON_ACCESS=${WP_TEST_MAILER_AMAZON_ACCESS} \
-e WP_TEST_MAILER_AMAZON_REGION=${WP_TEST_MAILER_AMAZON_REGION} \
-e WP_TEST_MAILER_AMAZON_SECRET=${WP_TEST_MAILER_AMAZON_SECRET} \
-e WP_TEST_IMPORT_MAILCHIMP_API=${WP_TEST_IMPORT_MAILCHIMP_API} \
-e WP_TEST_IMPORT_MAILCHIMP_LISTS=${WP_TEST_IMPORT_MAILCHIMP_LISTS} \
-e WP_TEST_MAILER_SMTP_HOST=${WP_TEST_MAILER_SMTP_HOST} \
-e WP_TEST_MAILER_SMTP_LOGIN=${WP_TEST_MAILER_SMTP_LOGIN} \
-e WP_TEST_MAILER_SMTP_PASSWORD=${WP_TEST_MAILER_SMTP_PASSWORD} \
-e MULTISITE=<< parameters.multisite >> \
-e ENABLE_COT=<< parameters.enable_cot >> \
-e ENABLE_COT_SYNC=<< parameters.enable_cot_sync >> \
codeception_integration "${args[@]}"
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
- store_artifacts: - store_artifacts:
@ -659,102 +566,12 @@ workflows:
- static_analysis_php8 - static_analysis_php8
- qa_js - qa_js
- qa_php - 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
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
- qa_js
- 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
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- acceptance_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
name: acceptance_tests_woo_cot_off
group: woo
woo_core_version: woo-cot-beta # Temporarily force COT beta version
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- js_tests: - js_tests:
<<: *slack-fail-post-step <<: *slack-fail-post-step
requires: requires:
- build - build
- integration_tests: - integration_tests:
<<: *slack-fail-post-step <<: *slack-fail-post-step
group: woo
name: integration_test_woocommerce
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 1
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_sync
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
enable_cot: 1
enable_cot_sync: 0
allow_fail: 1
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_no_sync
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- integration_tests:
<<: *slack-fail-post-step
<<: *only_trunk_and_cot
group: woo
woo_core_version: woo-cot-beta # Temporarily force COT beta version
name: integration_test_woo_cot_off
requires:
- unit_tests
- static_analysis_php8
- qa_js
- qa_php
- integration_tests:
<<: *slack-fail-post-step
skip_group: woo
skip_plugins: 1
name: integration_test_base
requires: requires:
- unit_tests - unit_tests
- static_analysis_php8 - static_analysis_php8
@ -781,8 +598,7 @@ workflows:
- build - build
- acceptance_tests - acceptance_tests
- js_tests - js_tests
- integration_test_woocommerce - integration_tests
- integration_test_base
nightly: nightly:
triggers: triggers:
@ -808,9 +624,9 @@ workflows:
<<: *slack-fail-post-step <<: *slack-fail-post-step
name: acceptance_oldest name: acceptance_oldest
woo_core_version: 6.2.2 woo_core_version: 6.2.2
woo_subscriptions_version: 4.3.0 woo_subscriptions_version: 3.0.10
woo_memberships_version: 1.21.0 woo_memberships_version: 1.22.9
woo_blocks_version: 5.3.2 woo_blocks_version: 5.5.1
mysql_command: --max_allowed_packet=100M mysql_command: --max_allowed_packet=100M
mysql_image_version: 5.7.36 mysql_image_version: 5.7.36
codeception_image_version: 7.4-cli_20210126.1 codeception_image_version: 7.4-cli_20210126.1

View File

@ -1,23 +0,0 @@
## Description
_N/A_
## Code review notes
_N/A_
## QA notes
_N/A_
## Linked PRs
_N/A_
## Linked tickets
_N/A_
## After-merge notes
_N/A_

View File

@ -1,11 +1,11 @@
# MailPoet Documentation for Integrators # MailPoet 3 - Documentation for Integrators
This is a place where we put documentation for developers who want to build an extension for MailPoet plugin. This is a place where we put documentation for developers who want to build an extension for MailPoet 3 plugin.
If you are a user looking for a user guide please visit our [knowledge base](https://kb.mailpoet.com/). If you are a user looking for a user guide please visit our [knowledge base](https://kb.mailpoet.com/).
## MailPoet API ## MailPoet API
MailPoet API is the officially supported way to integrate with the MailPoet plugin. It focuses on functionality for managing subscribers. MailPoet API is the officially supported way to integrate with the MailPoet 3 plugin. It focuses on functionality for managing subscribers.
Developers integrating MailPoet functionality in their own plugins or projects are strongly discouraged against using other functions and classes within MailPoet codebase! We are continually refactoring as part of our rapid development process, and backward compatibility is not guaranteed. Developers integrating MailPoet functionality in their own plugins or projects are strongly discouraged against using other functions and classes within MailPoet codebase! We are continually refactoring as part of our rapid development process, and backward compatibility is not guaranteed.
### Basics ### Basics
@ -30,12 +30,10 @@ Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by Wo
- [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md) - [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
- [Get Lists (getLists)](api_methods/GetLists.md) - [Get Lists (getLists)](api_methods/GetLists.md)
- [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md) - [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
- [Get Subscribers (getSubscribers)](api_methods/GetSubscribers.md)
- [Get Subscribers Count (getSubscribersCount)](api_methods/GetSubscribersCount.md)
- [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md) - [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md)
- [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md) - [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md)
- [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md) - [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md)
- [Subscribe to Lists (subscribeToLists)](api_methods/SubscribeToLists.md) - [Subscribe to List (subscribeToLists)](api_methods/SubscribeToLists.md)
- [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md) - [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
- [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md) - [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)

View File

@ -35,7 +35,6 @@ This method throws an `\Exception` in the event a subscriber with a given email
| source | string\|null | - | Possible values: `form`,`imported`,`administrator`,`api`,`wordpress_user`,`woocommerce_user`,`woocommerce_checkout`,`unknown`) | | source | string\|null | - | Possible values: `form`,`imported`,`administrator`,`api`,`wordpress_user`,`woocommerce_user`,`woocommerce_checkout`,`unknown`) |
| count_confirmations | string | 11 chars | Counter for confirmation emails | | count_confirmations | string | 11 chars | Counter for confirmation emails |
| subscriptions | array | - | List of subcriber subscriptions | | subscriptions | array | - | List of subcriber subscriptions |
| tags | array | - | List of subcriber tags |
| cf\_{custom_field['id']} | string | 65K chars | A custom subscriber field value (see [Get Subscriber Fields](GetSubscriberFields.md) | | cf\_{custom_field['id']} | string | 65K chars | A custom subscriber field value (see [Get Subscriber Fields](GetSubscriberFields.md) |
### Subscriber's subscription ### Subscriber's subscription
@ -49,17 +48,6 @@ This method throws an `\Exception` in the event a subscriber with a given email
| created_at | string | - | UTC time of creation in 'Y-m-d H:i:s' format | | created_at | string | - | UTC time of creation in 'Y-m-d H:i:s' format |
| updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format | | updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format |
### Subscriber's tag
| Property | Type | Limits | Description |
| ------------- | ------ | -------- | ----------------------------------------------- |
| id | string | 11 chars | Id of relation |
| subscriber_id | string | 11 chars | Id of subscriber |
| tag_id | string | 11 chars | Id of a list |
| name | string | - | Name of a tag |
| created_at | string | - | UTC time of creation in 'Y-m-d H:i:s' format |
| updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format |
### Response Example ### Response Example
```php ```php
@ -99,24 +87,6 @@ This method throws an `\Exception` in the event a subscriber with a given email
'updated_at' => '2019-05-14 08:43:08', 'updated_at' => '2019-05-14 08:43:08',
], ],
], ],
'tags' => [
0 => [
'id' => '2',
'subscriber_id' => '10',
'tag_id' => '1',
'name' => 'Alpha',
'created_at' => '2019-05-17 05:24:37',
'updated_at' => '2019-05-17 05:24:37',
],
1 => [
'id' => '4',
'subscriber_id' => '10',
'tag_id' => '5',
'name' => 'Beta',
'created_at' => '2020-03-07 15:21:37',
'updated_at' => '2020-03-07 15:21:37',
],
],
'cf_1' => 'US', 'cf_1' => 'US',
'cf_2' => 'New York', 'cf_2' => 'New York',
]; ];

View File

@ -1,25 +0,0 @@
[back to list](../Readme.md)
# Get Subscribers
## `array getSubscribers(array $filter = [], int $limit = 50, int $offset = 0)`
This method returns a list of subscribers. To see the subscriber data structure, please check [getSubscriber()](GetSubscriber.md) documentation.
## Arguments
| Argument | Type | Default | Description |
| ------------------ | ----- | ------- | --------------------------------------- |
| $filter (optional) | array | empty | Filters to retrieve subscribers |
| $limit (optional) | int | 50 | The number of results that are returned |
| $offset (optional) | int | 0 | From where to start returning data |
### Filter
Filter argument supports following array keys.
| 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,17 +0,0 @@
[back to list](../Readme.md)
# Get Subscribers Count
## `int getSubscribersCount(array $filter = [])`
This method returns the count of subscribers by a filter.
## Arguments
| Argument | Type | Default | Description |
| ------------------ | ----- | ------- | -------------------------------------------- |
| $filter (optional) | array | empty | Filters to retrieve the count of subscribers |
### Filter
To see supported filters, please check [getSubscribers()](GetSubscribers.md) documentation.

View File

@ -90,11 +90,9 @@ class RoboFile extends \Robo\Tasks {
// Clean up folder from previous files // Clean up folder from previous files
array_map('unlink', glob("assets/dist/css/*.*")); array_map('unlink', glob("assets/dist/css/*.*"));
$compilationResult = $this->taskExecStack() $this->_exec('pnpm run stylelint -- "assets/css/src/**/*.scss"');
->exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"') $this->_exec('pnpm run scss');
->exec('pnpm run scss') $compilationResult = $this->_exec('pnpm run autoprefixer');
->exec('pnpm run autoprefixer')
->run();
// Create manifest file // Create manifest file
$manifest = []; $manifest = [];
@ -184,20 +182,13 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec($command); return $this->_exec($command);
} }
public function testIntegration(array $opts = ['file' => null, 'group' => null, 'skip-group' => null, 'xml' => false, 'multisite' => false, 'debug' => false, 'skip-deps' => false, 'skip-plugins' => false, 'enable-cot' => false, 'enable-cot-sync' => false]) { public function testIntegration(array $opts = ['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false, 'skip-deps' => false]) {
return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration'])); return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration']));
} }
public function testMultisiteIntegration($opts = ['file' => null, 'group' => null, 'skip-group' => null, 'xml' => false, 'multisite' => true, 'skip-deps' => false, 'skip-plugins' => false, 'enable-cot' => false, 'enable-cot-sync' => false]) { public function testMultisiteIntegration($opts = ['file' => null, 'xml' => false, 'multisite' => true, 'skip-deps' => false]) {
return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration'])); return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration']));
}
public function testWooIntegration(array $opts = ['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false, 'enable-cot' => false, 'enable-cot-sync' => false]) {
return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration', 'group' => 'woo', 'skip-deps' => true, 'skip-plugins' => false]));
}
public function testBaseIntegration(array $opts = ['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
return $this->runTestsInContainer(array_merge($opts, ['test_type' => 'integration', 'skip-group' => 'woo', 'skip-deps' => true, 'skip-plugins' => true]));
} }
public function testCoverage($opts = ['file' => null, 'xml' => false]) { public function testCoverage($opts = ['file' => null, 'xml' => false]) {
@ -259,32 +250,20 @@ class RoboFile extends \Robo\Tasks {
return $this->testIntegration($opts); return $this->testIntegration($opts);
} }
public function testAcceptance($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, 'enable-cot' => false, 'enable-cot-sync' => false]) { public function testAcceptance($opts = ['file' => null, 'skip-deps' => false, 'timeout' => null]) {
return $this->runTestsInContainer($opts); return $this->runTestsInContainer($opts);
} }
public function testAcceptanceMultisite($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, 'enable-cot' => false, 'enable-cot-sync' => false]) { public function testAcceptanceMultisite($opts = ['file' => null, 'skip-deps' => false, 'timeout' => null]) {
return $this->runTestsInContainer(array_merge($opts, ['multisite' => true])); return $this->runTestsInContainer(array_merge($opts, ['multisite' => true]));
} }
/**
* Deletes docker stuff related to tests including docker images.
*/
public function deleteDocker() { public function deleteDocker() {
return $this->taskExec( return $this->taskExec(
'docker-compose down -v --remove-orphans --rmi all' 'docker-compose down -v --remove-orphans --rmi all'
)->dir(__DIR__ . '/tests/docker')->run(); )->dir(__DIR__ . '/tests/docker')->run();
} }
/**
* Deletes docker containers and volumes used in tests
*/
public function resetTestDocker() {
return $this->taskExec(
'docker-compose down -v --remove-orphans'
)->dir(__DIR__ . '/tests/docker')->run();
}
public function testFailedUnit() { public function testFailedUnit() {
$this->_exec('vendor/bin/codecept build'); $this->_exec('vendor/bin/codecept build');
return $this->_exec('vendor/bin/codecept run unit -g failed'); return $this->_exec('vendor/bin/codecept run unit -g failed');
@ -444,15 +423,13 @@ class RoboFile extends \Robo\Tasks {
public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) { public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) {
$severityFlag = $opts['severity'] === 'all' ? '-w' : '-n'; $severityFlag = $opts['severity'] === 'all' ? '-w' : '-n';
$task = implode(' ', [ $task = implode(' ', [
'php -d memory_limit=-1',
'./tasks/code_sniffer/vendor/bin/phpcs', './tasks/code_sniffer/vendor/bin/phpcs',
'--extensions=php', '--extensions=php',
$severityFlag, $severityFlag,
'--standard=tasks/code_sniffer/MailPoet', '--standard=tasks/code_sniffer/MailPoet',
'-s', '-s',
]); ]);
$foldersToIgnore = [
$ignorePaths = [
'.mp_svn', '.mp_svn',
'assets', 'assets',
'doc', 'doc',
@ -475,17 +452,11 @@ class RoboFile extends \Robo\Tasks {
'vendor-prefixed', 'vendor-prefixed',
'views', 'views',
]; ];
// the "--ignore" arg takes a list of regexes, we need to anchor and escape them
$ignorePatterns = array_map(function (string $path): string {
return '^' . preg_quote(__DIR__ . DIRECTORY_SEPARATOR . $path);
}, $ignorePaths);
$stringFilesToCheck = !empty($filesToCheck) ? implode(' ', $filesToCheck) : '.'; $stringFilesToCheck = !empty($filesToCheck) ? implode(' ', $filesToCheck) : '.';
return $this return $this
->taskExec($task) ->taskExec($task)
->arg('--ignore=' . implode(',', $ignorePatterns)) ->arg('--ignore=' . implode(',', $foldersToIgnore))
->rawArg($stringFilesToCheck) ->rawArg($stringFilesToCheck)
->run(); ->run();
} }
@ -923,41 +894,6 @@ class RoboFile extends \Robo\Tasks {
$printReviewers($logins, 'Full'); $printReviewers($logins, 'Full');
} }
public function displayCreatedPullRequests(ConsoleIO $io, int $months = 6) {
$projects = [
\MailPoetTasks\Release\GitHubController::PROJECT_SHOP,
\MailPoetTasks\Release\GitHubController::PROJECT_MAILPOET,
\MailPoetTasks\Release\GitHubController::PROJECT_PREMIUM,
];
$io->progressStart(count($projects));
$counts = [];
foreach ($projects as $project) {
$githubController = $this->createGitHubController($project);
$countsProject = $githubController->calculatePRcounts($months);
foreach ($countsProject as $login => $num) {
if (!isset($counts[$login])) {
$counts[$login] = 0;
}
$counts[$login] += $num;
}
$io->progressAdvance();
}
$io->progressFinish();
arsort($counts);
$io->title('Pull Request counts');
$outputList = [];
foreach ($counts as $login => $num) {
$outputList[] = [
$login,
$num,
round($num / $months, 2),
];
}
$io->table(['Login', 'Count', 'Per month'], $outputList);
}
public function releaseCheckPullRequest($version) { public function releaseCheckPullRequest($version) {
$this->createGitHubController() $this->createGitHubController()
->checkReleasePullRequestPassed($version); ->checkReleasePullRequestPassed($version);
@ -1087,20 +1023,10 @@ class RoboFile extends \Robo\Tasks {
} }
public function downloadWooCommerceZip($tag = null) { public function downloadWooCommerceZip($tag = null) {
if ($tag === 'woo-cot-beta') {
$this->downloadWooCommerceCotZip();
return;
}
$this->createWpOrgDownloader('woocommerce') $this->createWpOrgDownloader('woocommerce')
->downloadPluginZip('woocommerce.zip', __DIR__ . '/tests/plugins/', $tag); ->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) { public function generateData($generatorName = null, $threads = 1) {
require_once __DIR__ . '/tests/DataGenerator/_bootstrap.php'; require_once __DIR__ . '/tests/DataGenerator/_bootstrap.php';
$generator = new \MailPoet\Test\DataGenerator\DataGenerator(new \Codeception\Lib\Console\Output([])); $generator = new \MailPoet\Test\DataGenerator\DataGenerator(new \Codeception\Lib\Console\Output([]));
@ -1278,18 +1204,14 @@ class RoboFile extends \Robo\Tasks {
private function runTestsInContainer(array $opts) { private function runTestsInContainer(array $opts) {
$testType = $opts['test_type'] ?? 'acceptance'; $testType = $opts['test_type'] ?? 'acceptance';
$this->doctrineGenerateCache(); $this->doctrineGenerateCache();
return $this->taskExec( return $this->taskExec(
'COMPOSE_HTTP_TIMEOUT=200 docker-compose run ' . 'COMPOSE_HTTP_TIMEOUT=200 docker-compose run ' .
(isset($opts['skip-deps']) && $opts['skip-deps'] ? '-e SKIP_DEPS=1 ' : '') . (isset($opts['skip-deps']) && $opts['skip-deps'] ? '-e SKIP_DEPS=1 ' : '') .
(isset($opts['enable-cot']) && $opts['enable-cot'] ? '-e ENABLE_COT=1 ' : '') .
(isset($opts['enable-cot-sync']) && $opts['enable-cot-sync'] ? '-e ENABLE_COT_SYNC=1 ' : '') .
(isset($opts['skip-plugins']) && $opts['skip-plugins'] ? '-e SKIP_PLUGINS=1 ' : '') .
(isset($opts['timeout']) && $opts['timeout'] ? '-e WAIT_TIMEOUT=' . (int)$opts['timeout'] . ' ' : '') . (isset($opts['timeout']) && $opts['timeout'] ? '-e WAIT_TIMEOUT=' . (int)$opts['timeout'] . ' ' : '') .
(isset($opts['multisite']) && $opts['multisite'] ? '-e MULTISITE=1 ' : '-e MULTISITE=0 ') . (isset($opts['multisite']) && $opts['multisite'] ? '-e MULTISITE=1 ' : '-e MULTISITE=0 ') .
"codeception_{$testType} --steps --debug -vvv " . "codeception_{$testType} --steps --debug -vvv " .
(isset($opts['xml']) && $opts['xml'] ? '--xml ' : '') . (isset($opts['xml']) && $opts['xml'] ? '--xml ' : '') .
(isset($opts['group']) && $opts['group'] ? '--group ' . $opts['group'] . ' ' : '') .
(isset($opts['skip-group']) && $opts['skip-group'] ? '--skip-group ' . $opts['skip-group'] . ' ' : '') .
'-f ' . (isset($opts['file']) && $opts['file'] ? $opts['file'] : '') '-f ' . (isset($opts['file']) && $opts['file'] ? $opts['file'] : '')
)->dir(__DIR__ . '/tests/docker')->run(); )->dir(__DIR__ . '/tests/docker')->run();
} }

View File

@ -4,7 +4,6 @@
border: none; border: none;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 0 0 4px #fbfbfb; box-shadow: 0 0 0 4px #fbfbfb;
cursor: pointer;
fill: #2b3337; fill: #2b3337;
height: 16px; height: 16px;
margin: 0; margin: 0;

View File

@ -1,19 +0,0 @@
.mailpoet-automation-workflow-add-trigger {
align-items: center;
border: 1px dashed #c3c4c7;
border-radius: 4px;
color: #757575;
cursor: pointer;
display: flex;
fill: #757575;
height: 73px;
justify-content: center;
margin: 4px auto;
padding: 20px 32px;
&:focus {
box-shadow:
0 0 0 1px #fbfbfb, // space
0 0 0 calc(var(--wp-admin-border-width-focus) + 1px) var(--wp-admin-theme-color), // focus ring
}
}

View File

@ -1,27 +0,0 @@
.mailpoet-automation-chip {
align-items: center;
background: #fcf0f1;
border-radius: 9999px;
color: #8a2424;
cursor: pointer;
display: flex;
font-size: 13px;
height: 20px;
padding: 0 8px;
&.chip-small {
font-size: 13px;
height: 20px;
}
&.chip-medium {
font-size: 13px;
height: 24px;
}
&.chip-large {
font-size: 14px;
height: 32px;
padding: 0 12px;
}
}

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

@ -1,32 +0,0 @@
.mailpoet-automation-errors {
padding: 8px 0;
width: 280px;
}
.mailpoet-automation-errors-header {
font-weight: 600;
padding: 8px 12px;
}
.mailpoet-automation-step-error {
align-items: center;
appearance: none;
background: none;
border: none;
cursor: pointer;
display: grid;
gap: 12px;
grid-template-columns: auto 1fr;
padding: 9px 12px;
text-align: left;
width: 100%;
&:hover {
background: #f6f7f7;
}
&:focus-visible {
box-shadow: inset 0 0 0 1.5px #2271b1;
outline: none;
}
}

View File

@ -1,23 +0,0 @@
// See: https://github.com/WordPress/gutenberg/blob/659377eac576ac34f68dc9762187eb2f6aec2151/packages/edit-navigation/src/components/notices/style.scss
.mailpoet-automation-editor-notices__snackbar-list {
bottom: 0;
padding: 20px;
position: fixed;
}
.mailpoet-automation-editor-notices__notice-list {
// Notices have some unusual margin and padding by default, reset that.
.components-notice {
border-bottom: 1px solid rgba(0, 0, 0, .2);
box-sizing: border-box;
margin: 0;
min-height: 60px;
padding: 0 12px;
// Make sure the close button is centered.
.components-button {
align-self: initial;
}
}
}

View File

@ -1,7 +1,3 @@
.mailpoet-automation-sidebar {
padding-bottom: 100px;
}
.components-panel__body-title.mailpoet-automation-panel-plain-body-title { .components-panel__body-title.mailpoet-automation-panel-plain-body-title {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@ -12,10 +8,6 @@
font-weight: 500; font-weight: 500;
line-height: normal; line-height: normal;
padding: 16px 48px 16px 16px; padding: 16px 48px 16px 16px;
label & {
padding: 0;
}
} }
.mailpoet-automation-panel-plain-body-title-action { .mailpoet-automation-panel-plain-body-title-action {
@ -28,111 +20,3 @@
padding: 4px; padding: 4px;
} }
} }
.mailpoet-step-name-dropdown {
display: block;
h2 {
margin: 0;
}
.mailpoet-automation-panel-plain-body-title-text {
padding-left: 0;
padding-top: 0;
}
.mailpoet-automation-panel-plain-body-title-action {
margin-top: -10px;
padding-right: 0;
padding-top: 0;
}
}
.mailpoet-step-name-popover {
margin-top: -25px;
padding: 8px;
}
.mailpoet-step-name-input {
min-width: 208px;
}
.mailpoet-deactive {
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

@ -2,8 +2,9 @@
align-items: center; align-items: center;
background: #c3c4c7; background: #c3c4c7;
display: grid; display: grid;
height: 64px; height: 100%;
justify-content: center; justify-content: center;
margin: auto; margin: auto;
min-height: 100px;
width: 1px; width: 1px;
} }

View File

@ -1,22 +1,3 @@
.mailpoet-automation-editor-step-wrapper {
margin: auto;
position: relative;
width: 280px;
}
.mailpoet-automation-step-more-menu {
position: absolute;
right: 4px;
top: 14px;
button.components-button.is-small.has-icon {
height: 24px;
min-width: 24px;
padding: 3px;
width: 24px;
}
}
.mailpoet-automation-editor-step { .mailpoet-automation-editor-step {
appearance: none; appearance: none;
background: white; background: white;
@ -30,23 +11,15 @@
margin: 4px auto; margin: 4px auto;
padding: 12px; padding: 12px;
text-align: left; text-align: left;
width: 100%; width: 280px;
&.is-unknown-step {
background: #f0f0f1;
}
&:focus, &:focus,
&.is-selected-step { &.selected-step {
box-shadow: box-shadow:
0 0 0 1px #fbfbfb, // space 0 0 0 1px #fbfbfb, // space
0 0 0 calc(var(--wp-admin-border-width-focus) + 1px) var(--wp-admin-theme-color), // focus ring 0 0 0 calc(var(--wp-admin-border-width-focus) + 1px) var(--wp-admin-theme-color), // focus ring
0 1px 2px rgba(0, 0, 0, .05); // original shadow 0 1px 2px rgba(0, 0, 0, .05); // original shadow
} }
&:has(.mailpoet-automation-editor-step-footer) {
padding-bottom: 8px;
}
} }
.mailpoet-automation-editor-step-icon { .mailpoet-automation-editor-step-icon {
@ -59,23 +32,4 @@
.mailpoet-automation-editor-step-subtitle { .mailpoet-automation-editor-step-subtitle {
color: inherit; color: inherit;
font-weight: 600;
}
.mailpoet-automation-editor-step-footer {
display: flex;
grid-column: 1 / -1;
}
.mailpoet-automation-editor-step-error {
margin-left: auto;
}
.mailpoet-automation-colored-icon {
border-radius: 50%;
box-sizing: content-box;
display: flex;
justify-content: center;
padding: 12px;
position: relative;
} }

View File

@ -7,31 +7,3 @@
display: grid; display: grid;
padding: 50px 20px; padding: 50px 20px;
} }
.mailpoet-automation-editor-workflow-end {
background: #8c8f94;
border-radius: 999999px;
fill: white;
height: 18px;
margin: 4px auto;
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

@ -7,23 +7,6 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: #1d2327; background: #1d2327;
} }
&:disabled {
color: rgba(255, 255, 255, .4);
}
&.is-busy {
--background-color-1: #2c3236;
--background-color-2: #535659;
background-image:
linear-gradient(
-45deg,
var(--background-color-1) 33%,
var(--background-color-2) 33%,
var(--background-color-2) 70%,
var(--background-color-1) 70%
);
}
} }
.components-button.mailpoet-automation-button-centered, .components-button.mailpoet-automation-button-centered,

View File

@ -1,9 +1,3 @@
.mailpoet-automation-email-content-separator { .mailpoet-automation-email-content-separator {
height: 16px; height: 16px;
} }
.mailpoet-automation-email-buttons {
display: grid;
gap: 8px;
grid-template-columns: 1fr 1fr;
}

View File

@ -27,3 +27,9 @@
margin: auto; margin: auto;
max-width: 192px; max-width: 192px;
} }
.mailpoet-automation-thumbnail-buttons {
display: grid;
gap: 8px;
grid-template-columns: 1fr 1fr;
}

View File

@ -1,64 +1,3 @@
#mailpoet_automation * { #mailpoet_automation * {
box-sizing: border-box; box-sizing: border-box;
.mailpoet-add-new-button {
padding-right: 12px;
}
}
.mailpoet-automation-listing-heading {
margin-bottom: 16px;
}
.mailpoet-automation-listing {
box-shadow: none;
margin-bottom: 0;
}
.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);
}
.count {
background-color: #f0f0f1;
border-radius: 12px;
font-size: 11px;
font-weight: 400;
margin-left: 6px;
padding: 2px 5px;
}
}
.mailpoet-automation-listing-more-button button.components-button {
height: 36px;
padding: 0;
width: 36px;
svg {
height: 28px;
width: 28px;
}
} }

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 { .mailpoet-automation-listing-cell-status {
align-items: center; align-items: center;
display: grid; display: flex;
grid-auto-flow: column;
white-space: nowrap;
> div.components-base-control > div.components-base-control__field { > div.components-base-control > div.components-base-control__field {
margin-bottom: 0; margin-bottom: 0;

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

@ -83,14 +83,13 @@ h2 {
} }
} }
// Adjustments for family-font-select in popover
.mailpoet_toolbar_item { .mailpoet_toolbar_item {
.mailpoet-font-family-select { align-items: center;
height: 48px; background-color: white;
display: flex;
.components-input-control__container .components-custom-select-control__button { .mailpoet-font-family-select {
width: 200px; width: $grid-column-small;
}
} }
// Force rendering of select arrow on the right // Force rendering of select arrow on the right

View File

@ -22,31 +22,18 @@
min-width: 5em; min-width: 5em;
} }
.block-editor-panel-color-gradient-settings {
border: none;
padding: 10px 0;
h2 {
font-weight: normal;
}
}
hr { hr {
margin: 0; margin: 0;
} }
}
.mailpoet-font-family-select { .mailpoet-font-family-select {
.components-flex { button {
height: auto; width: 100%;
} }
.components-input-control__container { .components-custom-select-control__label {
width: 100%; font-weight: bold;
}
.components-custom-select-control__label {
font-weight: bold;
}
} }
} }

View File

@ -44,7 +44,7 @@ p.sender_email_address_warning.sender_email_address_warning a {
} }
p.sender_email_address_warning:first-child { p.sender_email_address_warning:first-child {
margin-top: 0; // unify spacing with parsley errors margin-top: 1em;
} }
.button.mailpoet-button-bigger { .button.mailpoet-button-bigger {
@ -88,37 +88,3 @@ p.sender_email_address_warning:first-child {
margin-left: 10px; margin-left: 10px;
} }
} }
.mailpoet_manage_sender_domain {
.mailpoet_table_header {
font-weight: 700 !important;
text-align: center !important; // to prevent being overwritten by widefat table classes
}
.dns_record_type_column {
font-weight: 550 !important;
text-align: center !important; // to prevent being overwritten by widefat table classes
}
}
body .components-modal__screen-overlay {
z-index: 9999999; // increased to overlay Gutenberg block inserter
}
.mailpoet-premium-modal.components-modal__frame {
max-width: 500px;
}
.mailpoet-premium-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 12px;
}
.mailpoet-premium-modal-error {
display: flex;
justify-content: flex-end;
margin-top: $grid-gap-half;
}

View File

@ -35,10 +35,6 @@ textarea.regular-text {
width: 25em !important; width: 25em !important;
} }
.regular-text-full-width {
width: 100%;
}
@include respond-to(small-screen) { @include respond-to(small-screen) {
.select2-container { .select2-container {
width: 100% !important; width: 100% !important;
@ -221,18 +217,9 @@ progress::-moz-progress-bar {
.mailpoet-form-notice-message { .mailpoet-form-notice-message {
font-style: italic; font-style: italic;
} }
.parsley-errors-list {
left: 0;
margin-top: 2px;
}
} }
// We need to hide the label because it doesn't fit to our form design, and it's added automatically by Gutenberg component // We need to hide the label because it doesn't fit to our form design, and it's added automatically by Gutenberg component
.mailpoet-form-field-tags label.components-form-token-field__label { .mailpoet-form-field-tags label.components-form-token-field__label {
display: none; display: none;
} }
.mailpoet-form-field-disabled {
cursor: not-allowed;
}

View File

@ -92,12 +92,17 @@ h1.title.mailpoet-newsletter-listing-heading {
margin-bottom: $grid-gap; margin-bottom: $grid-gap;
} }
#mailpoet_editor_steps_heading { .mailpoet-newsletter-listing-heading-wrapper {
.mailpoet-top-bar { .mailpoet-top-bar-logo {
left: 0; cursor: pointer;
left: 17px;
position: absolute;
top: 25px;
z-index: 1;
.mailpoet-top-bar-beamer { svg {
top: 4px; max-height: 100%;
max-width: 100%;
} }
} }
} }

View File

@ -0,0 +1,38 @@
#logger {
background-color: transparent;
border: 0;
border-top: 1px #aba9a9 solid;
font-size: .85em;
height: 300px;
margin-top: 20px;
overflow: scroll;
padding: 2px;
resize: both;
width: 100%;
}
#progressbar {
background-color: #d8d8d8;
border-radius: 5px;
width: 50%;
}
$progressbar_color: #fecf23;
$progressbar_gradient_to_color: #fd9215;
.ui-progressbar .ui-progressbar-value {
background-color: $progressbar_color;
background-image: linear-gradient(to bottom, $progressbar_color, $progressbar_gradient_to_color);
border: 0;
border-radius: 3px;
box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset;
height: 100%;
}
.mailpoet_progress_label {
font-size: 15px;
}
.error_msg {
color: #f00;
}

View File

@ -100,12 +100,6 @@ ul.sending-method-benefits {
.mailpoet-form-input:not(:first-child) { .mailpoet-form-input:not(:first-child) {
margin-top: $grid-gap-half; margin-top: $grid-gap-half;
} }
&.mailpoet-import-tags {
label.components-form-token-field__label {
display: none;
}
}
} }
.mailpoet-settings-inputs-row { .mailpoet-settings-inputs-row {

View File

@ -65,18 +65,6 @@ $form-line-height: 1.4;
} }
} }
/* Reset fieldset styles in form for backward compatibility. */
.mailpoet_paragraph {
fieldset,
legend {
background: transparent;
border: 0;
color: inherit;
margin: 0;
padding: 0;
}
}
.mailpoet_textarea { .mailpoet_textarea {
height: auto; height: auto;
} }

View File

@ -11,13 +11,13 @@ select.parsley-error,
textarea.parsley-error { textarea.parsley-error {
background-color: #f2dede; background-color: #f2dede;
border-color: #eed3d7; border-color: #eed3d7;
color: #900; color: #b94a48;
} }
.parsley-errors-list { .parsley-errors-list {
color: #900; color: #b94a48;
font-size: 13px; font-size: .9em;
line-height: 1em; line-height: .9em;
list-style-type: none; list-style-type: none;
margin: 8px 0 3px; margin: 8px 0 3px;
opacity: 0; opacity: 0;
@ -38,7 +38,7 @@ textarea.parsley-error {
.parsley-required, .parsley-required,
.parsley-custom-error-message { .parsley-custom-error-message {
color: #900; color: #b94a48;
} }
.mailpoet-form-errors { .mailpoet-form-errors {

View File

@ -32,7 +32,6 @@ $beamer-dot-size: 8px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
top: 2px; top: 2px;
z-index: 1;
svg { svg {
max-height: 100%; max-height: 100%;
@ -86,8 +85,6 @@ $beamer-dot-size: 8px;
border: none; border: none;
color: $color-wordpress-grey-dark; color: $color-wordpress-grey-dark;
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: column;
height: 60px; height: 60px;
justify-content: center; justify-content: center;
position: relative; position: relative;

View File

@ -61,7 +61,7 @@
margin-top: $grid-gap; margin-top: $grid-gap;
} }
&:not(.mailpoet-full-width) + .mailpoet-form-input:not(.mailpoet-full-width):not(.mailpoet-form-select), &:not(.mailpoet-full-width) + .mailpoet-form-input:not(.mailpoet-full-width),
&:not(.mailpoet-full-width) + .mailpoet-button:not(.mailpoet-full-width) { &:not(.mailpoet-full-width) + .mailpoet-button:not(.mailpoet-full-width) {
margin-left: $grid-gap; margin-left: $grid-gap;
} }

View File

@ -3,6 +3,7 @@
border: 1px solid $color-input-border !important; border: 1px solid $color-input-border !important;
border-radius: $form-control-border-radius !important; border-radius: $form-control-border-radius !important;
box-sizing: border-box; box-sizing: border-box;
display: inline-flex;
max-width: 100%; max-width: 100%;
min-height: $form-control-height; min-height: $form-control-height;
min-width: 0; min-width: 0;
@ -10,7 +11,6 @@
// To align the left padding with the other inputs // To align the left padding with the other inputs
input[type=text].components-form-token-field__input { input[type=text].components-form-token-field__input {
margin-left: 0; margin-left: 0;
min-height: 30px;
padding-left: 8px; padding-left: 8px;
} }
// For better fit when the last item is active // For better fit when the last item is active

View File

@ -1,29 +1,18 @@
@import '../../../node_modules/@wordpress/components/build-style/style';
@import '../../../node_modules/@wordpress/interface/build-style/style'; @import '../../../node_modules/@wordpress/interface/build-style/style';
@import '../../../node_modules/@wordpress/edit-site/build-style/style'; @import '../../../node_modules/@wordpress/edit-site/build-style/style';
@import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles @import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles
@import 'settings/colors'; @import 'settings/colors';
// automation components
@import './components-automation/statistics';
// automation editor
@import './components-automation-editor/add-step-button'; @import './components-automation-editor/add-step-button';
@import './components-automation-editor/add-trigger';
@import './components-automation-editor/block-icon'; @import './components-automation-editor/block-icon';
@import './components-automation-editor/chip';
@import './components-automation-editor/dropdown'; @import './components-automation-editor/dropdown';
@import './components-automation-editor/empty-workflow'; @import './components-automation-editor/empty-workflow';
@import './components-automation-editor/errors';
@import './components-automation-editor/panel'; @import './components-automation-editor/panel';
@import './components-automation-editor/separator'; @import './components-automation-editor/separator';
@import './components-automation-editor/status'; @import './components-automation-editor/status';
@import './components-automation-editor/step'; @import './components-automation-editor/step';
@import './components-automation-editor/step-card'; @import './components-automation-editor/step-card';
@import './components-automation-editor/workflow'; @import './components-automation-editor/workflow';
@import './components-automation-editor/notices';
@import './components-automation-editor/deactivate-modal';
// integrations // integrations

View File

@ -1,74 +0,0 @@
.mailpoet-automation-templates-heading {
margin: auto;
max-width: 982px;
}
ul.mailpoet-automation-templates {
clear: both;
cursor: pointer;
display: grid;
grid-gap: 32px;
grid-template-columns: repeat(auto-fill, 306px);
justify-content: center;
margin: auto;
max-width: 982px;
padding: 48px 0;
button.components-button {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
display: block;
height: 100%;
padding: 24px 24px 26px;
text-align: left;
width: 100%;
&:hover {
background: #fff;
border: 1px solid #dcdcde;
box-shadow: 0 3px 6px rgba(0, 0, 0, .15);
color: inherit;
}
&:focus {
border-color: #2271b1;
box-shadow: 0 3px 6px rgba(0, 0, 0, .15);
color: inherit;
}
}
h2 {
background: transparent;
border: none;
color: #2271b1;
font-size: 14px;
font-weight: 600;
line-height: 21px;
margin: 0;
padding: 0;
}
p {
margin: 8px 0 0;
}
.mailpoet-automation-from-scratch {
button {
align-content: center;
border: 2px dashed #dcdcde;
display: grid;
grid-gap: 24px;
justify-items: center;
&:hover {
border: 2px dashed #dcdcde;
}
}
svg {
fill: #dcdcde;
}
}
}

View File

@ -1,14 +1,6 @@
@import '../../../node_modules/@woocommerce/components/build-style/style'; @import '../../../node_modules/@woocommerce/components/build-style/style';
@import 'settings/colors'; @import 'settings/colors';
// automation components
@import './components-automation/statistics';
// automation listing
@import './components-automation-listing/listing'; @import './components-automation-listing/listing';
@import './components-automation-listing/header'; @import './components-automation-listing/header';
@import './components-automation-listing/search'; @import './components-automation-listing/search';
@import './components-automation-listing/cells/actions';
@import './components-automation-listing/cells/status'; @import './components-automation-listing/cells/status';

View File

@ -74,6 +74,7 @@
@import 'components-plugin/pages-custom'; @import 'components-plugin/pages-custom';
@import 'components-plugin/premium-page'; @import 'components-plugin/premium-page';
@import 'components-plugin/menu'; @import 'components-plugin/menu';
@import 'components-plugin/mp2-migrator';
@import 'components-plugin/newsletter'; @import 'components-plugin/newsletter';
@import 'components-plugin/newsletter-templates'; @import 'components-plugin/newsletter-templates';
@import 'components-plugin/newsletter-types'; @import 'components-plugin/newsletter-types';

View File

@ -12,74 +12,3 @@ export function getTrackingData() {
} }
return trackingDataLoading; return trackingDataLoading;
} }
export function mapFilterType(filter) {
const action = filter.action;
const filterType = filter.type;
// Email
if (filterType === 'email') {
switch (action) {
case 'machineOpensAbsoluteCount':
return '# of machine-opens';
case 'opensAbsoluteCount':
return '# of opens';
case 'numberOfOrders':
return '# of orders';
case 'clicked':
return 'clicked';
case 'clickedAny':
return 'clicked any email';
case 'opened':
return 'opened';
case 'machineOpened':
return 'machine-opened';
default:
return '';
}
}
// User Role
if (filterType === 'userRole') {
switch (action) {
case 'subscriberTag':
return 'subscriber tags';
case 'subscribedToList':
return 'subscribed to list';
case 'subscriberScore':
return 'score';
case 'wordpressRole':
return 'WordPress user role';
case 'mailpoetCustomField':
return 'MailPoet custom field';
default:
return '';
}
}
// WooCommerce
if (filterType === 'woocommerce')
switch (action) {
case 'customerInCountry':
return 'is in country';
case 'purchasedCategory':
return 'purchased in category';
case 'purchasedProduct':
return 'purchased product';
case 'subscribedDate':
return 'subscribed date';
case 'totalSpent':
return 'total spent';
default:
return '';
}
// WooCommerce Subscription
if (
filterType === 'woocommerceSubscription' &&
action === 'hasActiveSubscription'
)
return 'has an active subscription';
// WooCommerce Membership
if (filterType === 'woocommerceMembership' && action === 'isMemberOf')
return 'is active member of';
return '';
}

View File

@ -5,17 +5,6 @@ export * from './hooks';
const apiUrl = `${api.root}/mailpoet/v1/automation/`; const apiUrl = `${api.root}/mailpoet/v1/automation/`;
export type ApiError = {
code?: string;
message?: string;
data?: {
status?: number;
details?: Error;
params?: Record<string, string>;
errors?: unknown[];
};
};
export const initializeApi = () => { export const initializeApi = () => {
apiFetch.use(apiFetch.createRootURLMiddleware(apiUrl)); apiFetch.use(apiFetch.createRootURLMiddleware(apiUrl));
apiFetch.use(apiFetch.createNonceMiddleware(api.nonce)); apiFetch.use(apiFetch.createNonceMiddleware(api.nonce));

View File

@ -1,45 +1,29 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { Workflow } from './listing/workflow';
import { TopBarWithBeamer } from 'common/top_bar/top_bar';
import { plusIcon } from 'common/button/icon/plus';
import { Button, Flex, Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { initializeApi, useMutation } from './api';
import { createStore, storeName } from './listing/store';
import { AutomationListing } from './listing'; import { AutomationListing } from './listing';
import { registerApiErrorHandler } from './listing/api-error-handler';
import { Notices } from './listing/components/notices';
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
import { Onboarding } from './onboarding'; import { Onboarding } from './onboarding';
import { import {
CreateEmptyWorkflowButton, CreateTestingWorkflowButton,
CreateWorkflowFromTemplateButton, CreateWorkflowFromTemplateButton,
} from './testing'; } from './testing';
import { MailPoet } from '../mailpoet'; import { useMutation, useQuery } from './api';
function Content(): JSX.Element {
const count = useSelect((select) => select(storeName).getWorkflowCount());
return count > 0 ? <AutomationListing /> : <Onboarding />;
}
function Workflows(): JSX.Element { function Workflows(): JSX.Element {
return ( const { data, loading, error } = useQuery<{ data: Workflow[] }>('workflows');
<>
<TopBarWithBeamer /> if (error) {
<Flex className="mailpoet-automation-listing-heading"> return <div>Error: {error}</div>;
<h1 className="wp-heading-inline">Automations</h1> }
<Button
href={MailPoet.urls.automationTemplates} if (loading) {
icon={plusIcon} return <div>Loading workflows...</div>;
variant="primary" }
className="mailpoet-add-new-button"
> const workflows = data?.data ?? [];
New automation return workflows.length === 0 ? (
</Button> <Onboarding />
</Flex> ) : (
<Notices /> <AutomationListing workflows={workflows} loading={loading} />
<Content />
</>
); );
} }
@ -50,7 +34,6 @@ function RecreateSchemaButton(): JSX.Element {
return ( return (
<div> <div>
<WorkflowListingNotices />
<button <button
className="button button-link-delete" className="button button-link-delete"
type="button" type="button"
@ -94,40 +77,26 @@ function DeleteSchemaButton(): JSX.Element {
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<SlotFillProvider> <div>
<BrowserRouter> <Workflows />
<div> <div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
<Workflows /> <CreateTestingWorkflowButton />
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}> <CreateWorkflowFromTemplateButton template="delayed-email-after-signup">
<CreateEmptyWorkflowButton /> Create testing workflow from template (welcome email)
<CreateWorkflowFromTemplateButton slug="simple-welcome-email"> </CreateWorkflowFromTemplateButton>
Create testing workflow from template (welcome email) <CreateWorkflowFromTemplateButton template="welcome-email-sequence">
</CreateWorkflowFromTemplateButton> Create testing workflow from template (welcome sequence)
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence"> </CreateWorkflowFromTemplateButton>
Create testing workflow from template (welcome sequence, only <RecreateSchemaButton />
premium) <DeleteSchemaButton />
</CreateWorkflowFromTemplateButton> </div>
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence"> </div>
Create testing workflow from template (advanced welcome sequence,
only premium)
</CreateWorkflowFromTemplateButton>
<RecreateSchemaButton />
<DeleteSchemaButton />
</div>
<Popover.Slot />
</div>
</BrowserRouter>
</SlotFillProvider>
); );
} }
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
createStore();
const root = document.getElementById('mailpoet_automation'); const root = document.getElementById('mailpoet_automation');
if (root) { if (root) {
registerApiErrorHandler();
initializeApi();
ReactDOM.render(<App />, root); ReactDOM.render(<App />, root);
} }
}); });

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; root: string;
nonce: string; nonce: string;
}; };
mailpoet_workflow_count: number;
} }
} }
export const api = window.mailpoet_automation_api; export const api = window.mailpoet_automation_api;
export const workflowCount = window.mailpoet_workflow_count;

View File

@ -1,43 +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 { storeName } from './store';
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;
const code = errorObject.code;
if (code === 'mailpoet_automation_workflow_not_valid') {
dispatch(storeName).setErrors({ steps: errorObject.data.errors });
return undefined;
}
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,45 +0,0 @@
import { useState } from 'react';
import {
__experimentalConfirmDialog as ConfirmDialog,
Button,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { storeName } from '../../store';
export function TrashButton(): JSX.Element {
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
const { trash } = useDispatch(storeName);
return (
<>
<ConfirmDialog
isOpen={showConfirmDialog}
title="Delete workflow"
confirmButtonText="Yes, delete"
onConfirm={async () => {
trash(() => {
setShowConfirmDialog(false);
});
}}
onCancel={() => setShowConfirmDialog(false)}
__experimentalHideHeader={false}
>
You are about to delete the {workflow.name} workflow.
</ConfirmDialog>
<Button
variant="secondary"
isDestructive
onClick={() => setShowConfirmDialog(true)}
>
Move to Trash
</Button>
</>
);
}

View File

@ -1,22 +0,0 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
type Props = {
size?: 'small' | 'medium' | 'large';
children?: ReactNode;
};
export function Chip({ size = 'medium', children }: Props): JSX.Element {
return (
<div
className={classNames({
'mailpoet-automation-chip': true,
'chip-small': size === 'small',
'chip-medium': size === 'medium',
'chip-large': size === 'large',
})}
>
{children}
</div>
);
}

View File

@ -9,7 +9,7 @@ import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element'; import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { chevronDown } from '@wordpress/icons'; import { chevronDown } from '@wordpress/icons';
import { storeName } from '../../store'; import { store } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { WorkflowStatus } from '../../../listing/workflow';
// See: https://github.com/WordPress/gutenberg/blob/eff0cab2b3181c004dbd15398e570ecec28a3726/packages/edit-site/src/components/header/document-actions/index.js // See: https://github.com/WordPress/gutenberg/blob/eff0cab2b3181c004dbd15398e570ecec28a3726/packages/edit-site/src/components/header/document-actions/index.js
@ -24,9 +24,9 @@ const Dropdown: ComponentType<
export function DocumentActions({ children }): JSX.Element { export function DocumentActions({ children }): JSX.Element {
const { workflowName, workflowStatus, showIconLabels } = useSelect( const { workflowName, workflowStatus, showIconLabels } = useSelect(
(select) => ({ (select) => ({
workflowName: select(storeName).getWorkflowData().name, workflowName: select(store).getWorkflowData().name,
workflowStatus: select(storeName).getWorkflowData().status, workflowStatus: select(store).getWorkflowData().status,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(store).isFeatureActive('showIconLabels'),
}), }),
[], [],
); );

View File

@ -1,169 +0,0 @@
import { ComponentType, useContext, useEffect, useMemo, useState } from 'react';
import {
__unstableComposite as Composite,
__unstableCompositeItem as CompositeItem,
__unstableUseCompositeState as useCompositeState,
Button,
Popover as WpPopover,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { createContext } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Chip } from '../chip';
import { ColoredIcon } from '../icons';
import {
StepError as StepErrorType,
stepSidebarKey,
storeName,
} from '../../store';
// properties "offset" and "placement" are missing in WpPopover type definition
const Popover: ComponentType<
WpPopover.Props & {
offset?: number;
placement?: string;
}
> = WpPopover;
export const ErrorsCompositeContext =
createContext<ReturnType<typeof useCompositeState>>(undefined);
type StepErrorProps = {
stepId: string;
};
function StepError({ stepId }: StepErrorProps): JSX.Element {
const compositeState = useContext(ErrorsCompositeContext);
const { steps, workflowData } = useSelect(
(select) => ({
steps: select(storeName).getSteps(),
workflowData: select(storeName).getWorkflowData(),
}),
[],
);
const { openSidebar, selectStep } = useDispatch(storeName);
const stepData = workflowData.steps[stepId];
const step = steps.find(({ key }) => key === stepData.key);
return (
<CompositeItem
className="mailpoet-automation-step-error"
role="listitem"
state={compositeState}
onClick={() => {
openSidebar(stepSidebarKey);
selectStep(stepData);
}}
>
<ColoredIcon
icon={step.icon}
foreground={step.foreground}
background={step.background}
width="23px"
height="23px"
/>
{step.title}
</CompositeItem>
);
}
export function Errors(): JSX.Element | null {
const [showPopover, setShowPopover] = useState(false);
const compositeState = useCompositeState({
orientation: 'vertical',
shift: true,
});
const { errors, workflowData } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflowData: select(storeName).getWorkflowData(),
}),
[],
);
// walk the steps tree (breadth first) to produce stable error order
const stepErrors = useMemo(() => {
if (!errors) {
return [];
}
const visited = new Map<string, StepErrorType | undefined>();
const ids = workflowData.steps.root.next_steps.map(({ id }) => id);
while (ids.length > 0) {
const id = ids.shift();
if (!visited.has(id)) {
visited.set(id, errors.steps[id]);
workflowData.steps[id]?.next_steps?.forEach((step) =>
ids.push(step.id),
);
}
}
return [...visited.values()].filter((error) => !!error);
}, [errors, workflowData]);
// automatically open the popover when errors appear
const hasErrors = stepErrors.length > 0;
useEffect(() => {
if (hasErrors) {
setShowPopover(true);
}
}, [hasErrors]);
if (stepErrors.length === 0) {
return null;
}
return (
<div>
<Button
variant="link"
onClick={() =>
setShowPopover((prevState) =>
prevState === undefined ? false : !prevState,
)
}
onMouseDown={() =>
// Catch and mark a mouse down event from an open popover with "undefined" to avoid closing it
// (automatically via click outside) and reopening it right after (via the onClick handler).
// The "onClose" method of the popover doesn't pass any events so we can't filter them.
setShowPopover((prevState) => (prevState ? undefined : prevState))
}
style={{ textDecoration: 'none', borderRadius: 99999 }}
>
<Chip>{stepErrors.length} issues</Chip>
</Button>
{showPopover && (
<Popover
offset={10}
placement="bottom-end"
onClose={() =>
setShowPopover((prevState) =>
prevState === undefined ? undefined : false,
)
}
>
<ErrorsCompositeContext.Provider value={compositeState}>
<Composite
state={compositeState}
role="list"
aria-label={__('Workflow errors', 'mailpoet')}
className="mailpoet-automation-errors"
>
<div className="mailpoet-automation-errors-header">
{__('The following steps are not fully set:', 'mailpoet')}
</div>
{stepErrors.map((error) => (
<StepError key={error.step_id} stepId={error.step_id} />
))}
</Composite>
</ErrorsCompositeContext.Provider>
</Popover>
)}
</div>
);
}

View File

@ -1,118 +1,53 @@
import { useState } from 'react';
import { Button, NavigableMenu, TextControl } from '@wordpress/components'; import { Button, NavigableMenu, TextControl } from '@wordpress/components';
import { dispatch, useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { PinnedItems } from '@wordpress/interface'; import { PinnedItems } from '@wordpress/interface';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { DocumentActions } from './document_actions'; import { DocumentActions } from './document_actions';
import { Errors } from './errors';
import { InserterToggle } from './inserter_toggle'; import { InserterToggle } from './inserter_toggle';
import { MoreMenu } from './more_menu'; import { MoreMenu } from './more_menu';
import { storeName } from '../../store'; import { store, storeName } from '../../store';
import { WorkflowStatus } from '../../../listing/workflow'; import { WorkflowStatus } from '../../../listing/workflow';
import { DeactivateModal } from '../modals/deactivate-modal';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/index.js
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js // https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
function ActivateButton({ onClick }): JSX.Element { function ActivateButton(): JSX.Element {
const { errors } = useSelect( const { activate } = useDispatch(store);
(select) => ({
errors: select(storeName).getErrors(),
}),
[],
);
return ( return (
<Button <Button isPrimary className="editor-post-publish-button" onClick={activate}>
variant="primary" Activate
className="editor-post-publish-button"
onClick={onClick}
disabled={!!errors}
>
{__('Activate', 'mailpoet')}
</Button> </Button>
); );
} }
function UpdateButton(): JSX.Element { function UpdateButton(): JSX.Element {
const { save } = useDispatch(storeName); const { activate } = useDispatch(store);
return ( return (
<Button <Button isPrimary className="editor-post-publish-button" onClick={activate}>
variant="primary" Update
className="editor-post-publish-button"
onClick={save}
>
{__('Update', 'mailpoet')}
</Button> </Button>
); );
} }
function SaveDraftButton(): JSX.Element { function SaveDraftButton(): JSX.Element {
const { save } = useDispatch(storeName); const { save } = useDispatch(store);
return ( return (
<Button variant="tertiary" onClick={save}> <Button isTertiary onClick={save}>
{__('Save draft', 'mailpoet')} {__('Save Draft')}
</Button> </Button>
); );
} }
function DeactivateButton(): JSX.Element { export function Header(): JSX.Element {
const [showDeactivateModal, setShowDeactivateModal] = useState(false); const { setWorkflowName } = useDispatch(store);
const [isBusy, setIsBusy] = useState(false);
const { hasUsersInProgress } = useSelect(
(select) => ({
hasUsersInProgress:
select(storeName).getWorkflowData().stats.totals.in_progress > 0,
}),
[],
);
const deactivateOrShowModal = () => {
if (hasUsersInProgress) {
setShowDeactivateModal(true);
return;
}
setIsBusy(true);
void dispatch(storeName).deactivate();
};
return (
<>
{showDeactivateModal && (
<DeactivateModal
onClose={() => {
setShowDeactivateModal(false);
}}
/>
)}
<Button
isBusy={isBusy}
variant="tertiary"
onClick={deactivateOrShowModal}
>
{__('Deactivate', 'mailpoet')}
</Button>
</>
);
}
type Props = {
showInserterToggle: boolean;
toggleActivatePanel: () => void;
};
export function Header({
showInserterToggle,
toggleActivatePanel,
}: Props): JSX.Element {
const { setWorkflowName } = useDispatch(storeName);
const { workflowName, workflowStatus } = useSelect( const { workflowName, workflowStatus } = useSelect(
(select) => ({ (select) => ({
workflowName: select(storeName).getWorkflowData().name, workflowName: select(store).getWorkflowData().name,
workflowStatus: select(storeName).getWorkflowData().status, workflowStatus: select(store).getWorkflowData().status,
}), }),
[], [],
); );
@ -125,7 +60,7 @@ export function Header({
orientation="horizontal" orientation="horizontal"
role="toolbar" role="toolbar"
> >
{showInserterToggle && <InserterToggle />} <InserterToggle />
</NavigableMenu> </NavigableMenu>
</div> </div>
@ -134,14 +69,13 @@ export function Header({
{() => ( {() => (
<div className="mailpoet-automation-editor-dropdown-name-edit"> <div className="mailpoet-automation-editor-dropdown-name-edit">
<div className="mailpoet-automation-editor-dropdown-name-edit-title"> <div className="mailpoet-automation-editor-dropdown-name-edit-title">
{__('Automation name', 'mailpoet')} {__('Automation name')}
</div> </div>
<TextControl <TextControl
value={workflowName} value={workflowName}
onChange={(newName) => setWorkflowName(newName)} onChange={(newName) => setWorkflowName(newName)}
help={__( help={__(
`Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`, `Give the automation a name that indicates its purpose. E.g. "Abandoned cart recovery"`,
'mailpoet',
)} )}
/> />
</div> </div>
@ -151,19 +85,9 @@ export function Header({
<div className="edit-site-header_end"> <div className="edit-site-header_end">
<div className="edit-site-header__actions"> <div className="edit-site-header__actions">
<Errors /> <SaveDraftButton />
{workflowStatus !== WorkflowStatus.ACTIVE && ( {workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />}
<> {workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />}
<SaveDraftButton />
<ActivateButton onClick={toggleActivatePanel} />
</>
)}
{workflowStatus === WorkflowStatus.ACTIVE && (
<>
<DeactivateButton />
<UpdateButton />
</>
)}
<PinnedItems.Slot scope={storeName} /> <PinnedItems.Slot scope={storeName} />
<MoreMenu /> <MoreMenu />
</div> </div>

View File

@ -2,7 +2,7 @@ import { Button, ToolbarItem } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n'; import { __, _x } from '@wordpress/i18n';
import { plus } from '@wordpress/icons'; import { plus } from '@wordpress/icons';
import { storeName } from '../../store'; import { store } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/header/header-toolbar/index.js // https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/header/header-toolbar/index.js
@ -11,13 +11,13 @@ import { storeName } from '../../store';
export function InserterToggle(): JSX.Element { export function InserterToggle(): JSX.Element {
const { isInserterOpened, showIconLabels } = useSelect( const { isInserterOpened, showIconLabels } = useSelect(
(select) => ({ (select) => ({
isInserterOpened: select(storeName).isInserterSidebarOpened(), isInserterOpened: select(store).isInserterSidebarOpened(),
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(store).isFeatureActive('showIconLabels'),
}), }),
[], [],
); );
const { toggleInserterSidebar } = useDispatch(storeName); const { toggleInserterSidebar } = useDispatch(store);
return ( return (
<ToolbarItem <ToolbarItem

View File

@ -1,10 +1,9 @@
import { MenuGroup, MenuItem } from '@wordpress/components'; import { MenuGroup } from '@wordpress/components';
import { displayShortcut } from '@wordpress/keycodes'; import { displayShortcut } from '@wordpress/keycodes';
import { __, _x } from '@wordpress/i18n'; import { __, _x } from '@wordpress/i18n';
import { MoreMenuDropdown } from '@wordpress/interface'; import { MoreMenuDropdown } from '@wordpress/interface';
import { PreferenceToggleMenuItem } from '@wordpress/preferences'; import { PreferenceToggleMenuItem } from '@wordpress/preferences';
import { storeName } from '../../store'; import { storeName } from '../../store';
import { MailPoet } from '../../../../mailpoet';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/more-menu/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/more-menu/index.js
@ -19,28 +18,17 @@ export function MoreMenu(): JSX.Element {
}} }}
> >
{() => ( {() => (
<> <MenuGroup label={_x('View', 'noun')}>
<MenuGroup label={_x('View', 'noun')}> <PreferenceToggleMenuItem
<PreferenceToggleMenuItem scope={storeName}
scope={storeName} name="fullscreenMode"
name="fullscreenMode" label={__('Fullscreen mode')}
label={__('Fullscreen mode')} info={__('Work without distraction')}
info={__('Work without distraction')} messageActivated={__('Fullscreen mode activated')}
messageActivated={__('Fullscreen mode activated')} messageDeactivated={__('Fullscreen mode deactivated')}
messageDeactivated={__('Fullscreen mode deactivated')} shortcut={displayShortcut.secondary('f')}
shortcut={displayShortcut.secondary('f')} />
/> </MenuGroup>
</MenuGroup>
<MenuGroup>
<MenuItem
onClick={() => {
window.location.href = MailPoet.urls.automationListing;
}}
>
{__('View all automations', 'mailpoet')}
</MenuItem>
</MenuGroup>
</>
)} )}
</MoreMenuDropdown> </MoreMenuDropdown>
); );

View File

@ -1,30 +0,0 @@
import { Icon } from '@wordpress/components';
import { ComponentType } from 'react';
export type ColoredIconProps = {
width: string;
height: string;
background: string;
foreground: string;
icon: ComponentType;
};
export function ColoredIcon({
foreground,
background,
...iconProps
}: ColoredIconProps): JSX.Element {
return (
<div
className="mailpoet-automation-colored-icon"
style={{
width: iconProps.width,
height: iconProps.height,
backgroundColor: background,
fill: foreground,
}}
>
<Icon {...iconProps} />
</div>
);
}

View File

@ -0,0 +1,20 @@
export function DelayIcon(): JSX.Element {
return (
<svg
width="41"
height="41"
viewBox="0 0 41 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.796875 20.5C0.796875 9.45431 9.75118 0.5 20.7969 0.5C31.8426 0.5 40.7969 9.45431 40.7969 20.5C40.7969 31.5457 31.8426 40.5 20.7969 40.5C9.75118 40.5 0.796875 31.5457 0.796875 20.5Z"
fill="#F7EDF7"
/>
<path
d="M20.7972 11.1665C15.6639 11.1665 11.4639 15.3665 11.4639 20.4998C11.4639 25.6332 15.6639 29.8332 20.7972 29.8332C25.9305 29.8332 30.1305 25.6332 30.1305 20.4998C30.1305 15.3665 25.9305 11.1665 20.7972 11.1665ZM20.7972 27.9665C16.6812 27.9665 13.3305 24.6158 13.3305 20.4998C13.3305 16.3838 16.6812 13.0332 20.7972 13.0332C24.9132 13.0332 28.2639 16.3838 28.2639 20.4998C28.2639 24.6158 24.9132 27.9665 20.7972 27.9665ZM21.2639 15.8332H19.8639V21.4332L24.7172 24.4198L25.4639 23.2065L21.2639 20.6865V15.8332Z"
fill="#7F54B3"
/>
</svg>
);
}

View File

@ -0,0 +1,26 @@
export function EmailIcon(): JSX.Element {
return (
<svg
width="41"
height="41"
viewBox="0 0 41 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.5 20.5C0.5 9.45431 9.45431 0.5 20.5 0.5C31.5457 0.5 40.5 9.45431 40.5 20.5C40.5 31.5457 31.5457 40.5 20.5 40.5C9.45431 40.5 0.5 31.5457 0.5 20.5Z"
fill="#FCF9E8"
/>
<path
d="M27.4998 14.375H13.4998C12.6944 14.375 12.0415 15.0279 12.0415 15.8333V25.1667C12.0415 25.9721 12.6944 26.625 13.4998 26.625H27.4998C28.3053 26.625 28.9582 25.9721 28.9582 25.1667V15.8333C28.9582 15.0279 28.3053 14.375 27.4998 14.375Z"
stroke="#996800"
strokeWidth="1.5"
/>
<path
d="M12.3335 14.6665L20.5002 21.6665L28.6668 14.6665"
stroke="#996800"
strokeWidth="1.5"
/>
</svg>
);
}

View File

@ -1,2 +1,3 @@
export { DelayIcon } from './delay';
export { EmailIcon } from './email';
export { TriggerIcon } from './trigger'; export { TriggerIcon } from './trigger';
export { ColoredIcon } from './colored-icon';

View File

@ -1,8 +1,24 @@
export function TriggerIcon(): JSX.Element { export function TriggerIcon(): JSX.Element {
return ( return (
<svg viewBox="0 0 24 18" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M16.1135 1.06667L22.6469 7.6C23.4635 8.3 23.4635 9.7 22.5302 10.5167L15.9969 17.05C15.6469 17.4 15.0635 17.6333 14.5968 17.6333C14.1302 17.6333 13.5468 17.4 13.1968 17.05L6.66352 10.5167C5.84685 9.7 5.84685 8.41667 6.66352 7.6L13.1968 1.06667C14.0135 0.25 15.2968 0.25 16.1135 1.06667Z" /> width="40"
<path d="M9.46352 1.41667L2.11352 8.76667C1.99685 8.88333 1.99685 9.11667 2.23018 9.11667L9.58018 16.4667L8.29685 17.75L0.946851 10.5167C0.130184 9.7 0.130184 8.41667 0.946851 7.6L8.29685 0.25L9.46352 1.41667Z" /> height="41"
viewBox="0 0 40 41"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 20.5C0 9.45431 8.95431 0.5 20 0.5C31.0457 0.5 40 9.45431 40 20.5C40 31.5457 31.0457 40.5 20 40.5C8.95431 40.5 0 31.5457 0 20.5Z"
fill="#F0F6FC"
/>
<path
d="M24.3163 12.5667L30.8496 19.1C31.6663 19.8 31.6663 21.2 30.7329 22.0167L24.1996 28.55C23.8496 28.9 23.2663 29.1333 22.7996 29.1333C22.3329 29.1333 21.7496 28.9 21.3996 28.55L14.8663 22.0167C14.0496 21.2 14.0496 19.9167 14.8663 19.1L21.3996 12.5667C22.2163 11.75 23.4996 11.75 24.3163 12.5667Z"
fill="#2271B1"
/>
<path
d="M17.6663 12.9167L10.3163 20.2667C10.1996 20.3833 10.1996 20.6167 10.4329 20.6167L17.7829 27.9667L16.4996 29.25L9.14961 22.0167C8.33294 21.2 8.33294 19.9167 9.14961 19.1L16.4996 11.75L17.6663 12.9167Z"
fill="#2271B1"
/>
</svg> </svg>
); );
} }

View File

@ -1,66 +1,28 @@
import { useCallback, useRef, useState } from 'react';
import { Popover } from '@wordpress/components'; import { Popover } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Hooks } from 'wp-js-hooks';
import { PremiumModal } from 'common/premium_modal';
import { Inserter } from '../inserter'; import { Inserter } from '../inserter';
import { Item } from '../inserter/item'; import { store } from '../../store';
import { storeName } from '../../store';
export function InserterPopover(): JSX.Element | null { export function InserterPopover(): JSX.Element | null {
const popoverRef = useRef<HTMLDivElement>(); const { inserterPopoverAnchor } = useSelect(
const [showModal, setShowModal] = useState(false);
const { inserterPopover } = useSelect(
(select) => ({ (select) => ({
inserterPopover: select(storeName).getInserterPopover(), inserterPopoverAnchor: select(store).getInserterPopoverAnchor(),
}), }),
[], [],
); );
const { setInserterPopover } = useDispatch(storeName);
const onInsert = useCallback((item: Item) => { const { setInserterPopoverAnchor } = dispatch(store);
const addStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.add_step_callback',
() => {
setShowModal(true);
},
);
addStepCallback(item);
}, []);
if (!inserterPopover) { if (!inserterPopoverAnchor) {
return null; return null;
} }
return ( return (
<> <Popover
<Popover anchorRect={inserterPopoverAnchor.getBoundingClientRect()}
ref={popoverRef} onClose={() => setInserterPopoverAnchor(undefined)}
anchorRect={inserterPopover.anchor.getBoundingClientRect()} >
onClose={() => { <Inserter />
if (!showModal) { </Popover>
setInserterPopover(undefined);
}
}}
>
<Inserter onInsert={onInsert} />
</Popover>
{showModal && (
<PremiumModal
onRequestClose={() => {
setShowModal(false);
popoverRef.current?.focus();
}}
tracking={{
utm_medium: 'upsell_modal',
utm_campaign: 'add_automation_step',
}}
>
{__('You cannot add a new step to the automation.', 'mailpoet')}
</PremiumModal>
)}
</>
); );
} }

View File

@ -1,8 +0,0 @@
import { Item } from './item';
export type Group = {
type: string;
title?: string;
label: string;
items: Item[];
};

View File

@ -1,15 +1,14 @@
import { forwardRef, Fragment, useCallback, useMemo } from 'react'; import { forwardRef, useCallback, useMemo } from 'react';
import { SearchControl } from '@wordpress/components'; import { SearchControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { useRef, useImperativeHandle, useState } from '@wordpress/element'; import { useRef, useImperativeHandle, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { blockDefault, Icon } from '@wordpress/icons'; import { blockDefault, Icon } from '@wordpress/icons';
import { Group } from './group';
import { Item } from './item'; import { Item } from './item';
import { StepInfoPanel } from './step_info_panel'; import { StepInfoPanel } from './step_info_panel';
import { StepList } from './step_list'; import { StepList } from './step_list';
import { InserterListbox } from '../inserter-listbox'; import { InserterListbox } from '../inserter-listbox';
import { storeName } from '../../store'; import { store } from '../../store';
// See: https://github.com/WordPress/gutenberg/blob/628ae68152f572d0b395bb15c0f71b8821e7f130/packages/block-editor/src/components/inserter/menu.js // See: https://github.com/WordPress/gutenberg/blob/628ae68152f572d0b395bb15c0f71b8821e7f130/packages/block-editor/src/components/inserter/menu.js
@ -18,50 +17,18 @@ const filterItems = (value: string, item: Item[]): Item[] =>
step.title.toLowerCase().includes(value.trim().toLowerCase()), step.title.toLowerCase().includes(value.trim().toLowerCase()),
); );
type Props = { export const Inserter = forwardRef((_, ref): JSX.Element => {
onInsert?: (item: Item) => void;
};
export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
const [filterValue, setFilterValue] = useState(''); const [filterValue, setFilterValue] = useState('');
const [hoveredItem, setHoveredItem] = useState(null); const [hoveredItem, setHoveredItem] = useState(null);
const { steps, type } = useSelect( const { actionSteps, logicalSteps } = useSelect(
(select) => ({ (select) => ({
steps: select(storeName).getSteps(), actionSteps: select(store).getInserterActionSteps(),
type: select(storeName).getInserterPopover().type, logicalSteps: select(store).getInserterLogicalSteps(),
}), }),
[], [],
); );
const groups: Group[] = useMemo(
() =>
type === 'triggers'
? [
{
type: 'triggers',
title: undefined,
label: __('Triggers', 'mailpoet'),
items: steps.filter(({ group }) => group === 'triggers'),
},
]
: [
{
type: 'actions',
title: __('Actions', 'mailpoet'),
label: __('Actions', 'mailpoet'),
items: steps.filter(({ group }) => group === 'actions'),
},
{
type: 'logical',
title: __('Logical', 'mailpoet'),
label: __('Logical', 'mailpoet'),
items: steps.filter(({ group }) => group === 'logical'),
},
],
[steps, type],
);
const onHover = useCallback( const onHover = useCallback(
(item) => { (item) => {
setHoveredItem(item); setHoveredItem(item);
@ -76,13 +43,13 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
}, },
})); }));
const filteredGroups = useMemo( const filteredActionSteps = useMemo(
() => () => filterItems(filterValue, actionSteps),
groups.map((group) => ({ [actionSteps, filterValue],
...group, );
items: filterItems(filterValue, group.items), const filteredLogicalSteps = useMemo(
})), () => filterItems(filterValue, logicalSteps),
[filterValue, groups], [filterValue, logicalSteps],
); );
return ( return (
@ -103,41 +70,52 @@ export const Inserter = forwardRef(({ onInsert }: Props, ref): JSX.Element => {
<div className="block-editor-inserter__block-list"> <div className="block-editor-inserter__block-list">
<InserterListbox> <InserterListbox>
{filteredGroups.map( {filteredActionSteps.length > 0 && (
(group) => <>
group.items.length > 0 && ( <div className="block-editor-inserter__panel-header">
<Fragment key={group.type}> <h2 className="block-editor-inserter__panel-title">
{group.title && ( <div>Actions</div>
<div className="block-editor-inserter__panel-header"> </h2>
<h2 className="block-editor-inserter__panel-title"> </div>
<div>{group.title}</div> <div className="block-editor-inserter__panel-content">
</h2> <StepList
</div> items={filteredActionSteps}
)} onHover={onHover}
<div className="block-editor-inserter__panel-content"> onSelect={() => {}}
<StepList label="A"
items={group.items} />
onHover={onHover} </div>
onSelect={(item: Item) => onInsert(item)} </>
label={group.label}
/>
</div>
</Fragment>
),
)} )}
{filteredGroups.reduce( {filteredLogicalSteps.length > 0 && (
(sum, { items }) => sum + items.length, <>
0, <div className="block-editor-inserter__panel-header">
) === 0 && ( <h2 className="block-editor-inserter__panel-title">
<div className="block-editor-inserter__no-results"> <div>Logical</div>
<Icon </h2>
className="block-editor-inserter__no-results-icon" </div>
icon={blockDefault} <div className="block-editor-inserter__panel-content">
/> <StepList
<p>{__('No results found.')}</p> items={filteredLogicalSteps}
</div> onHover={onHover}
onSelect={() => {}}
label="B"
/>
</div>
</>
)} )}
{filteredActionSteps.length === 0 &&
filteredLogicalSteps.length === 0 && (
<div className="block-editor-inserter__no-results">
<Icon
className="block-editor-inserter__no-results-icon"
icon={blockDefault}
/>
<p>{__('No results found.')}</p>
</div>
)}
</InserterListbox> </InserterListbox>
</div> </div>
</div> </div>

View File

@ -15,9 +15,7 @@ const isAppleOS = (): boolean => {
); );
}; };
type ListboxItemProps = ComponentProps<typeof InserterListboxItem>; type Props = ComponentProps<typeof InserterListboxItem> & {
type Props = Omit<ListboxItemProps, 'onSelect' | 'onHover'> & {
item: Item; item: Item;
onSelect: (item: Item, isModifierKey: boolean) => void; onSelect: (item: Item, isModifierKey: boolean) => void;
onHover: (item: Item) => void; onHover: (item: Item) => void;

View File

@ -5,7 +5,7 @@ import {
store as keyboardShortcutsStore, store as keyboardShortcutsStore,
} from '@wordpress/keyboard-shortcuts'; } from '@wordpress/keyboard-shortcuts';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, store, workflowSidebarKey } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js
@ -13,16 +13,16 @@ import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store';
export function KeyboardShortcuts(): null { export function KeyboardShortcuts(): null {
const { isSidebarOpened, selectedStep } = useSelect((select) => ({ const { isSidebarOpened, selectedStep } = useSelect((select) => ({
isSidebarOpened: select(storeName).isSidebarOpened, isSidebarOpened: select(store).isSidebarOpened,
selectedStep: select(storeName).getSelectedStep, selectedStep: select(store).getSelectedStep,
})); }));
const { openSidebar, closeSidebar, toggleFeature } = useDispatch(storeName); const { openSidebar, closeSidebar, toggleFeature } = useDispatch(store);
const { registerShortcut } = useDispatch(keyboardShortcutsStore); const { registerShortcut } = useDispatch(keyboardShortcutsStore);
useEffect(() => { useEffect(() => {
void registerShortcut({ registerShortcut({
name: 'mailpoet/automation-editor/toggle-fullscreen', name: 'mailpoet/automation-editor/toggle-fullscreen',
category: 'global', category: 'global',
description: __('Toggle fullscreen mode.'), description: __('Toggle fullscreen mode.'),
@ -32,7 +32,7 @@ export function KeyboardShortcuts(): null {
}, },
}); });
void registerShortcut({ registerShortcut({
name: 'mailpoet/automation-editor/toggle-sidebar', name: 'mailpoet/automation-editor/toggle-sidebar',
category: 'global', category: 'global',
description: __('Show or hide the settings sidebar.'), description: __('Show or hide the settings sidebar.'),

View File

@ -1,115 +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';
export function DeactivateModal({ onClose }): JSX.Element {
const { workflowName } = useSelect(
(select) => ({
workflowName: select(storeName).getWorkflowData().name,
}),
[],
);
const [selected, setSelected] = useState<
WorkflowStatus.INACTIVE | WorkflowStatus.DEACTIVATING
>(WorkflowStatus.DEACTIVATING);
const [isBusy, setIsBusy] = useState<boolean>(false);
// translators: %s is the name of the automation.
const title = sprintf(
__('Deactivate the "%s" automation?', 'mailpoet'),
workflowName,
);
return (
<Modal
className="mailpoet-automatoin-deactivate-modal"
title={title}
onRequestClose={onClose}
>
{__(
"Some subscribers entered but have not finished the flow. Let's decide what to do in this case.",
'mailpoet',
)}
<ul className="mailpoet-automation-options">
<li>
<label
className={
selected === WorkflowStatus.DEACTIVATING
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.DEACTIVATING}
onChange={() => setSelected(WorkflowStatus.DEACTIVATING)}
/>
</span>
<span>
<strong>
{__('Let entered subscribers finish the flow', 'mailpoet')}
</strong>
{__(
"New subscribers won't enter, but recently entered could proceed.",
'mailpoet',
)}
</span>
</label>
</li>
<li>
<label
className={
selected === WorkflowStatus.INACTIVE
? 'mailpoet-automation-option active'
: 'mailpoet-automation-option'
}
>
<span>
<input
type="radio"
disabled={isBusy}
name="deactivation-method"
checked={selected === WorkflowStatus.INACTIVE}
onChange={() => setSelected(WorkflowStatus.INACTIVE)}
/>
</span>
<span>
<strong>
{__('Stop automation for all subscribers', 'mailpoet')}
</strong>
{__(
'Automation will be deactivated for all the subscribers immediately.',
'mailpoet',
)}
</span>
</label>
</li>
</ul>
<Button
isBusy={isBusy}
variant="primary"
onClick={() => {
setIsBusy(true);
if (selected === WorkflowStatus.DEACTIVATING) {
// @ToDo Use the correct method provided in MAILPOET-4731
dispatch(storeName).deactivate();
return;
}
dispatch(storeName).deactivate();
}}
>
{__('Deactivate automation', 'mailpoet')}
</Button>
<Button disabled={isBusy} variant="tertiary" onClick={onClose}>
{__('Cancel', 'mailpoet')}
</Button>
</Modal>
);
}

View File

@ -1,45 +0,0 @@
import { NoticeList, SnackbarList } from '@wordpress/components';
import { StoreDescriptor, useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
// See: https://github.com/WordPress/gutenberg/blob/5be0ec4153c3adf9f0f2513239f4f7a358ba7948/packages/editor/src/components/editor-notices/index.js
export function EditorNotices(): 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',
);
const snackbarNotices = notices.filter(({ type }) => type === 'snackbar');
return (
<>
<NoticeList
notices={nonDismissibleNotices}
className="mailpoet-automation-editor-notices__notice-list"
/>
<NoticeList
notices={dismissibleNotices}
className="mailpoet-automation-editor-notices__notice-list"
onRemove={removeNotice}
/>
<SnackbarList
notices={snackbarNotices}
className="mailpoet-automation-editor-notices__snackbar-list"
onRemove={removeNotice}
/>
</>
);
}

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>{__("What's next?", 'mailpoet')}</strong>
</p>
<p>
{__(
'View all your automations to track statistics and create new ones.',
'mailpoet',
)}
</p>
<Button variant="secondary" onClick={goToListings}>
{__('View all automations', 'mailpoet')}
</Button>
</div>
</>
);
}
export function ActivatePanel({ onClose }): JSX.Element {
const { workflow, errors } = useSelect(
(select) => ({
errors: select(storeName).getErrors(),
workflow: select(storeName).getWorkflowData(),
}),
[],
);
useEffect(() => {
if (errors) {
onClose();
}
}, [errors, onClose]);
if (errors) {
return null;
}
const isActive = workflow.status === WorkflowStatus.ACTIVE;
return (
<div className="mailpoet-automation-activate-panel">
{isActive && <PostStep onClose={onClose} />}
{!isActive && <PreStep onClose={onClose} />}
</div>
);
}

View File

@ -1,47 +0,0 @@
import { Dropdown, TextControl } from '@wordpress/components';
import { edit, Icon } from '@wordpress/icons';
import { PlainBodyTitle } from './plain-body-title';
import { TitleActionButton } from './title-action-button';
type Props = {
currentName: string;
defaultName: string;
update: (value: string) => void;
};
export function StepName({
currentName,
defaultName,
update,
}: Props): JSX.Element {
return (
<Dropdown
className="mailpoet-step-name-dropdown"
contentClassName="mailpoet-step-name-popover"
position="bottom left"
renderToggle={({ isOpen, onToggle }) => (
<PlainBodyTitle
title={currentName.length > 0 ? currentName : defaultName}
>
<TitleActionButton
onClick={onToggle}
aria-expanded={isOpen}
aria-label="Edit step name"
>
<Icon icon={edit} size={16} />
</TitleActionButton>
</PlainBodyTitle>
)}
renderContent={() => (
<TextControl
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."
/>
)}
/>
);
}

View File

@ -1,6 +1,6 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data'; import { useDispatch } from '@wordpress/data';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import { stepSidebarKey, store, workflowSidebarKey } from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/sidebar/settings-header/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/sidebar/settings-header/index.js
@ -11,7 +11,7 @@ type Props = {
}; };
export function Header({ sidebarKey }: Props): JSX.Element { export function Header({ sidebarKey }: Props): JSX.Element {
const { openSidebar } = useDispatch(storeName); const { openSidebar } = useDispatch(store);
const openWorkflowSettings = () => openSidebar(workflowSidebarKey); const openWorkflowSettings = () => openSidebar(workflowSidebarKey);
const openStepSettings = () => openSidebar(stepSidebarKey); const openStepSettings = () => openSidebar(stepSidebarKey);

View File

@ -11,7 +11,12 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { Header } from './header'; import { Header } from './header';
import { StepSidebar } from './step'; import { StepSidebar } from './step';
import { WorkflowSidebar } from './workflow'; import { WorkflowSidebar } from './workflow';
import { stepSidebarKey, storeName, workflowSidebarKey } from '../../store'; import {
stepSidebarKey,
store,
storeName,
workflowSidebarKey,
} from '../../store';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js // https://github.com/WordPress/gutenberg/blob/5caeae34b3fb303761e3b9432311b26f4e5ea3a6/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js
@ -37,8 +42,8 @@ export function Sidebar(props: Props): JSX.Element {
sidebarKey: sidebarKey:
select(interfaceStore).getActiveComplementaryArea(storeName) ?? select(interfaceStore).getActiveComplementaryArea(storeName) ??
workflowSidebarKey, workflowSidebarKey,
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(store).isFeatureActive('showIconLabels'),
workflowName: select(storeName).getWorkflowData().name, workflowName: select(store).getWorkflowData().name,
}), }),
[], [],
); );
@ -51,7 +56,7 @@ export function Sidebar(props: Props): JSX.Element {
headerClassName="edit-site-sidebar__panel-tabs" headerClassName="edit-site-sidebar__panel-tabs"
title={__('Settings')} title={__('Settings')}
icon={cog} icon={cog}
className="edit-site-sidebar mailpoet-automation-sidebar" className="edit-site-sidebar"
panelClassName="edit-site-sidebar" panelClassName="edit-site-sidebar"
smallScreenTitle={workflowName || __('(no title)')} smallScreenTitle={workflowName || __('(no title)')}
scope={storeName} scope={storeName}

View File

@ -1,13 +1,13 @@
import { PanelBody } from '@wordpress/components'; import { PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { storeName } from '../../../store'; import { store } from '../../../store';
import { StepCard } from '../../step-card'; import { StepCard } from '../../step-card';
export function StepSidebar(): JSX.Element { export function StepSidebar(): JSX.Element {
const { selectedStep, selectedStepType } = useSelect( const { selectedStep, selectedStepType } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(store).getSelectedStep(),
selectedStepType: select(storeName).getSelectedStepType(), selectedStepType: select(store).getSelectedStepType(),
}), }),
[], [],
); );

View File

@ -1,54 +1,35 @@
import { PanelBody, PanelRow } from '@wordpress/components'; import { PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { storeName } from '../../../store'; import { store } from '../../../store';
import { TrashButton } from '../../actions/trash-button';
export function WorkflowSidebar(): JSX.Element { export function WorkflowSidebar(): JSX.Element {
const { workflowData } = useSelect( const { workflowData } = useSelect(
(select) => ({ (select) => ({
workflowData: select(storeName).getWorkflowData(), workflowData: select(store).getWorkflowData(),
}), }),
[], [],
); );
const dateOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
};
return ( return (
<PanelBody title="Automation details" initialOpen> <PanelBody>
<PanelRow> <div>
<strong>Date added</strong>{' '} <strong>{workflowData.name}</strong>
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString( </div>
undefined, <br />
dateOptions, <div>
)} <strong>ID:</strong> {workflowData.id}
</PanelRow> </div>
<PanelRow> <div>
<strong>Activated</strong>{' '} <strong>Status:</strong> {workflowData.status}
{workflowData.status === 'active' && </div>
new Date(Date.parse(workflowData.updated_at)).toLocaleDateString( <div>
undefined, <strong>Created:</strong>{' '}
dateOptions, {new Date(Date.parse(workflowData.created_at)).toLocaleString()}
)} </div>
{workflowData.status !== 'active' && <div>
workflowData.activated_at && <strong>Updated:</strong>{' '}
new Date(Date.parse(workflowData.activated_at)).toLocaleDateString( {new Date(Date.parse(workflowData.updated_at)).toLocaleString()}
undefined, </div>
dateOptions,
)}
{workflowData.status !== 'active' && !workflowData.activated_at && (
<span className="mailpoet-deactive">Not activated yet.</span>
)}
</PanelRow>
<PanelRow>
<strong>Author</strong> {workflowData.author.name}
</PanelRow>
<PanelRow>
<TrashButton />
</PanelRow>
</PanelBody> </PanelBody>
); );
} }

View File

@ -6,7 +6,7 @@ import { StepIcon } from '../step-icon';
type Props = { type Props = {
title: string; title: string;
description: string; description: string;
icon: JSX.Element | ComponentType; icon: ComponentType;
}; };
export function StepCard({ title, description, icon }: Props): JSX.Element { export function StepCard({ title, description, icon }: Props): JSX.Element {

View File

@ -5,10 +5,9 @@ import { WorkflowCompositeContext } from './context';
type Props = { type Props = {
onClick?: (element: HTMLButtonElement) => void; onClick?: (element: HTMLButtonElement) => void;
previousStepId: string;
}; };
export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element { export function AddStepButton({ onClick }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(WorkflowCompositeContext);
return ( return (
<CompositeItem <CompositeItem
@ -16,7 +15,6 @@ export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element {
role="treeitem" role="treeitem"
className="mailpoet-automation-editor-add-step-button" className="mailpoet-automation-editor-add-step-button"
focusable focusable
data-previous-step-id={previousStepId}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
const button = (event.target as HTMLElement).closest('button'); const button = (event.target as HTMLElement).closest('button');

View File

@ -1,37 +0,0 @@
import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { Icon, plus } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { WorkflowCompositeContext } from './context';
import { Step } from './types';
import { storeName } from '../../store';
type Props = {
step: Step;
};
export function AddTrigger({ step }: Props): JSX.Element {
const compositeState = useContext(WorkflowCompositeContext);
const { setInserterPopover } = useDispatch(storeName);
return (
<CompositeItem
state={compositeState}
role="treeitem"
className="mailpoet-automation-workflow-add-trigger"
data-previous-step-id={step.id}
focusable
onClick={(event) => {
event.stopPropagation();
setInserterPopover({
anchor: (event.target as HTMLElement).closest('button'),
type: 'triggers',
});
}}
>
<Icon icon={plus} size={16} />
{__('Add trigger', 'mailpoet')}
</CompositeItem>
);
}

View File

@ -4,24 +4,18 @@ import {
__unstableUseCompositeState as useCompositeState, __unstableUseCompositeState as useCompositeState,
} from '@wordpress/components'; } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Icon, check } from '@wordpress/icons';
import { Hooks } from 'wp-js-hooks';
import { WorkflowCompositeContext } from './context'; import { WorkflowCompositeContext } from './context';
import { EmptyWorkflow } from './empty-workflow'; import { EmptyWorkflow } from './empty-workflow';
import { Separator } from './separator'; import { Separator } from './separator';
import { Step } from './step'; import { Step } from './step';
import { Step as StepData } from './types';
import { InserterPopover } from '../inserter-popover'; import { InserterPopover } from '../inserter-popover';
import { storeName } from '../../store'; import { store } from '../../store';
import { AddTrigger } from './add-trigger';
import { Statistics } from './statistics';
export function Workflow(): JSX.Element { export function Workflow(): JSX.Element {
const { workflowData, selectedStep } = useSelect( const { workflowData, selectedStep } = useSelect(
(select) => ({ (select) => ({
workflowData: select(storeName).getWorkflowData(), workflowData: select(store).getWorkflowData(),
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(store).getSelectedStep(),
}), }),
[], [],
); );
@ -34,48 +28,33 @@ export function Workflow(): JSX.Element {
const stepMap = workflowData?.steps ?? undefined; const stepMap = workflowData?.steps ?? undefined;
const triggers = useMemo(
() => Object.values(stepMap ?? {}).filter(({ type }) => type === 'trigger'),
[stepMap],
);
// serialize steps (for now, we support only one trigger and linear workflows) // serialize steps (for now, we support only one trigger and linear workflows)
const steps = useMemo(() => { const steps = useMemo(() => {
const stepArray = [stepMap.root]; if (!stepMap || triggers.length < 1) {
return [];
}
const stepArray = [triggers[0]];
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const lastStep = stepArray[stepArray.length - 1]; const lastStep = stepArray[stepArray.length - 1];
if (!lastStep || lastStep.next_steps.length === 0) { if (
!('next_step_id' in lastStep) ||
!lastStep.next_step_id ||
!(stepMap[lastStep.next_step_id] ?? false)
) {
break; break;
} }
stepArray.push(stepMap[lastStep.next_steps[0].id]); stepArray.push(stepMap[lastStep.next_step_id]);
} }
return stepArray.slice(1); return stepArray;
}, [stepMap]); }, [triggers, stepMap]);
const renderStep = useMemo(
() =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step',
(stepData: StepData) =>
stepData.type === 'root' ? (
<AddTrigger step={stepData} />
) : (
<Step
step={stepData}
isSelected={selectedStep && stepData.id === selectedStep.id}
/>
),
),
[selectedStep],
);
const renderSeparator = useMemo(
() =>
Hooks.applyFilters(
'mailpoet.automation.workflow.render_step_separator',
(previousStepData: StepData) => (
<Separator previousStepId={previousStepData.id} />
),
),
[],
);
if (!workflowData) { if (!workflowData) {
return <EmptyWorkflow />; return <EmptyWorkflow />;
@ -86,38 +65,20 @@ export function Workflow(): JSX.Element {
<Composite <Composite
state={compositeState} state={compositeState}
role="tree" role="tree"
aria-label={__('Workflow', 'mailpoet')}
aria-orientation="vertical" aria-orientation="vertical"
className="mailpoet-automation-editor-workflow" className="mailpoet-automation-editor-workflow"
> >
<div className="mailpoet-automation-editor-workflow-wrapper"> <div className="mailpoet-automation-editor-workflow-wrapper">
<Statistics /> <div />
{stepMap.root.next_steps.length === 0 ? ( {steps.map((step, i) => (
<>
{renderStep(stepMap.root)}
{renderSeparator(stepMap.root)}
</>
) : (
stepMap.root.next_steps.map(
({ id }) =>
stepMap[id]?.type !== 'trigger' && (
<Fragment key={`root-${id}`}>
{renderStep(stepMap.root)}
{renderSeparator(stepMap.root)}
</Fragment>
),
)
)}
{steps.map((step) => (
<Fragment key={step.id}> <Fragment key={step.id}>
{renderStep(step)} {i > 0 && <Separator />}
{renderSeparator(step)} <Step
step={step}
isSelected={selectedStep && step.id === selectedStep.id}
/>
</Fragment> </Fragment>
))} ))}
<Icon
className="mailpoet-automation-editor-workflow-end"
icon={check}
/>
<div /> <div />
</div> </div>
<InserterPopover /> <InserterPopover />

View File

@ -1,22 +1,13 @@
import { dispatch } from '@wordpress/data'; import { dispatch } from '@wordpress/data';
import { AddStepButton } from './add-step-button'; import { AddStepButton } from './add-step-button';
import { storeName } from '../../store'; import { store } from '../../store';
type Props = { export function Separator(): JSX.Element {
previousStepId: string; const { setInserterPopoverAnchor } = dispatch(store);
};
export function Separator({ previousStepId }: Props): JSX.Element {
const { setInserterPopover } = dispatch(storeName);
return ( return (
<div className="mailpoet-automation-editor-separator"> <div className="mailpoet-automation-editor-separator">
<AddStepButton <AddStepButton onClick={(button) => setInserterPopoverAnchor(button)} />
onClick={(button) =>
setInserterPopover({ anchor: button, type: 'steps' })
}
previousStepId={previousStepId}
/>
</div> </div>
); );
} }

View File

@ -1,37 +0,0 @@
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { storeName } from '../../store';
import { Statistics as BaseStatistics } from '../../../components/statistics';
export function Statistics(): JSX.Element {
const { workflow } = useSelect(
(select) => ({
workflow: select(storeName).getWorkflowData(),
}),
[],
);
return (
<div className="mailpoet-automation-editor-stats">
<BaseStatistics
items={[
{
key: 'entered',
label: __('Total Entered', 'mailpoet'),
value: workflow.stats.totals.entered,
},
{
key: 'processing',
label: __('Total Processing', 'mailpoet'),
value: workflow.stats.totals.in_progress,
},
{
key: 'exited',
label: __('Total Exited', 'mailpoet'),
value: workflow.stats.totals.exited,
},
]}
/>
</div>
);
}

View File

@ -1,59 +0,0 @@
import { useCallback, useState } from 'react';
import { DropdownMenu } from '@wordpress/components';
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';
type Props = {
step: StepData;
};
export function StepMoreMenu({ step }: Props): JSX.Element {
const [showModal, setShowModal] = useState(false);
const onDelete = useCallback((stepData: StepData) => {
const deleteStepCallback = Hooks.applyFilters(
'mailpoet.automation.workflow.delete_step_callback',
() => {
setShowModal(true);
},
);
deleteStepCallback(stepData);
}, []);
return (
<>
<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

@ -1,111 +1,96 @@
import classNames from 'classnames';
import { useContext } from 'react'; import { useContext } from 'react';
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components'; import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useDispatch, useRegistry } from '@wordpress/data';
import { blockMeta } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { WorkflowCompositeContext } from './context'; import { WorkflowCompositeContext } from './context';
import { StepMoreMenu } from './step-more-menu'; import { Step as StepType } from './types';
import { Step as StepData } from './types'; import { DelayIcon, EmailIcon, TriggerIcon } from '../icons';
import { Chip } from '../chip'; import { stepSidebarKey, store } from '../../store';
import { ColoredIcon } from '../icons';
import { stepSidebarKey, storeName } from '../../store';
import { StepType } from '../../store/types';
const getUnknownStepType = (step: StepData): StepType => { // mocked data
const isTrigger = step.type === 'trigger'; function getIcon(step: StepType): JSX.Element | null {
return { if (step.type === 'trigger') {
title: isTrigger return <TriggerIcon />;
? __('Unknown trigger', 'mailpoet') }
: __('Unknown step', 'mailpoet'),
subtitle: () => if (step.key === 'core:delay') {
isTrigger return <DelayIcon />;
? __('Trigger type not registered', 'mailpoet') }
: __('Step type not registered', 'mailpoet'),
description: isTrigger if (step.key === 'mailpoet:send-email') {
? __('Unknown trigger', 'mailpoet') return <EmailIcon />;
: __('Unknown step', 'mailpoet'), }
group: step.type === 'trigger' ? 'triggers' : 'actions',
key: step.key, return null;
foreground: '#8c8f94', }
background: '#dcdcde',
edit: () => null, // mocked data
icon: () => blockMeta, function getTitle(step: StepType): string {
}; if (step.type === 'trigger') {
}; return 'Trigger';
}
if (step.key === 'core:delay') {
return 'Delay';
}
if (step.key === 'mailpoet:send-email') {
return 'Send email';
}
return '';
}
// mocked data
function getSubtitle(step: StepType): string {
if (step.key === 'mailpoet:segment:subscribed') {
return 'Subscribed to segment';
}
if (step.key === 'core:delay') {
return `${step.args.seconds as number} seconds`;
}
if (step.key === 'mailpoet:send-email') {
return `Email ID: ${step.args.email_id as number}`;
}
return step.key;
}
type Props = { type Props = {
step: StepData; step: StepType;
isSelected: boolean; isSelected: boolean;
}; };
export function Step({ step, isSelected }: Props): JSX.Element { export function Step({ step, isSelected }: Props): JSX.Element {
const { stepType, error } = useSelect( const { openSidebar, selectStep } = useDispatch(store);
(select) => ({
stepType: select(storeName).getStepType(step.key),
error: select(storeName).getStepError(step.id),
}),
[step],
);
const { openSidebar, selectStep } = useDispatch(storeName);
const compositeState = useContext(WorkflowCompositeContext); const compositeState = useContext(WorkflowCompositeContext);
const { batch } = useRegistry(); const { batch } = useRegistry();
const compositeItemId = `step-${step.id}`;
const stepTypeData = stepType ?? getUnknownStepType(step);
return ( return (
<div className="mailpoet-automation-editor-step-wrapper"> <CompositeItem
<StepMoreMenu step={step} /> state={compositeState}
<CompositeItem role="treeitem"
state={compositeState} className={`mailpoet-automation-editor-step ${
role="treeitem" isSelected ? 'selected-step' : ''
className={classNames({ }`}
'mailpoet-automation-editor-step': true, key={step.id}
'is-selected-step': isSelected, focusable
'is-unknown-step': !stepType, onClick={() =>
})} batch(() => {
id={compositeItemId} openSidebar(stepSidebarKey);
key={step.id} selectStep(step);
focusable })
onClick={() => }
batch(() => { >
openSidebar(stepSidebarKey); <div className="mailpoet-automation-editor-step-icon">
selectStep(step); {getIcon(step)}
}) </div>
} <div>
> <div className="mailpoet-automation-editor-step-title">
<div className="mailpoet-automation-editor-step-icon"> {getTitle(step)}
<ColoredIcon
icon={stepTypeData.icon}
foreground={stepTypeData.foreground}
background={stepTypeData.background}
width="23px"
height="23px"
/>
</div> </div>
<div> <div className="mailpoet-automation-editor-step-subtitle">
<label {getSubtitle(step)}
htmlFor={compositeItemId}
className="mailpoet-automation-editor-step-title"
>
{step.type !== 'trigger'
? stepTypeData.title
: __('Trigger', 'mailpoet')}
</label>
<div className="mailpoet-automation-editor-step-subtitle">
{step.type !== 'trigger'
? stepTypeData.subtitle(step)
: stepTypeData.title}
</div>
</div> </div>
{error && ( </div>
<div className="mailpoet-automation-editor-step-footer"> </CompositeItem>
<div className="mailpoet-automation-editor-step-error">
<Chip size="small">{__('Not set', 'mailpoet')}</Chip>
</div>
</div>
)}
</CompositeItem>
</div>
); );
} }

View File

@ -1,33 +1,16 @@
export type NextStep = {
id: string;
};
export type Step = { export type Step = {
id: string; id: string;
type: 'root' | 'trigger' | 'action'; type: 'trigger' | 'action';
key: string; key: string;
next_step_id?: string;
args: Record<string, unknown>; args: Record<string, unknown>;
next_steps: NextStep[];
}; };
export type Workflow = { export type Workflow = {
id: number; id?: number;
name: string; name: string;
status: 'active' | 'inactive' | 'draft' | 'trash'; status: 'active' | 'inactive' | 'draft';
created_at: string; created_at: string;
updated_at: string; updated_at: string;
activated_at: string; steps: Record<string, Step>;
author: {
id: number;
name: string;
};
stats: {
has_values: boolean;
totals: {
entered: number;
in_progress: number;
exited: number;
};
};
steps: Record<string, Step> & { root: Step };
}; };

View File

@ -1,6 +1,5 @@
import classnames from 'classnames'; import classnames from 'classnames';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useState } from 'react';
import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components'; import { Button, Icon, Popover, SlotFillProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { wordpress } from '@wordpress/icons'; import { wordpress } from '@wordpress/icons';
@ -10,64 +9,40 @@ import {
FullscreenMode, FullscreenMode,
} from '@wordpress/interface'; } from '@wordpress/interface';
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { addQueryArgs } from '@wordpress/url';
import { Header } from './components/header'; import { Header } from './components/header';
import { InserterSidebar } from './components/inserter-sidebar'; import { InserterSidebar } from './components/inserter-sidebar';
import { KeyboardShortcuts } from './components/keyboard-shortcuts'; import { KeyboardShortcuts } from './components/keyboard-shortcuts';
import { EditorNotices } from './components/notices';
import { Sidebar } from './components/sidebar'; import { Sidebar } from './components/sidebar';
import { Workflow } from './components/workflow'; import { Workflow } from './components/workflow';
import { createStore, storeName } from './store'; import { store, storeName } from './store';
import { initializeApi } from '../api'; import { initializeApi } from '../api';
import { initialize as initializeCoreIntegration } from '../integrations/core';
import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet'; import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet';
import { MailPoet } from '../../mailpoet';
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
import { registerApiErrorHandler } from './api-error-handler';
import { ActivatePanel } from './components/panel/activate-panel';
// See: // See:
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js // https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/editor/index.js // https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/editor/index.js
// disable inserter sidebar until we implement drag & drop
const showInserterSidebar = false;
function Editor(): JSX.Element { function Editor(): JSX.Element {
const { const {
isFullscreenActive, isFullscreenActive,
isInserterOpened, isInserterOpened,
isSidebarOpened, isSidebarOpened,
showIconLabels, showIconLabels,
workflow,
} = useSelect( } = useSelect(
(select) => ({ (select) => ({
isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'), isFullscreenActive: select(store).isFeatureActive('fullscreenMode'),
isInserterOpened: select(storeName).isInserterSidebarOpened(), isInserterOpened: select(store).isInserterSidebarOpened(),
isSidebarOpened: select(storeName).isSidebarOpened(), isSidebarOpened: select(store).isSidebarOpened(),
showIconLabels: select(storeName).isFeatureActive('showIconLabels'), showIconLabels: select(store).isFeatureActive('showIconLabels'),
workflow: select(storeName).getWorkflowData(),
}), }),
[], [],
); );
const [showActivatePanel, setShowActivatePanel] = useState(false);
const className = classnames('interface-interface-skeleton', { const className = classnames('interface-interface-skeleton', {
'is-sidebar-opened': isSidebarOpened, 'is-sidebar-opened': isSidebarOpened,
'show-icon-labels': showIconLabels, 'show-icon-labels': showIconLabels,
}); });
if (workflow.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted]: workflow.id,
});
return null;
}
const toggleActivatePanel = () => {
setShowActivatePanel(!showActivatePanel);
};
return ( return (
<ShortcutProvider> <ShortcutProvider>
<SlotFillProvider> <SlotFillProvider>
@ -88,24 +63,11 @@ function Editor(): JSX.Element {
</div> </div>
) )
} }
header={ header={<Header />}
<Header content={<Workflow />}
showInserterToggle={showInserterSidebar}
toggleActivatePanel={toggleActivatePanel}
/>
}
content={
<>
<EditorNotices />
<Workflow />
</>
}
sidebar={<ComplementaryArea.Slot scope={storeName} />} sidebar={<ComplementaryArea.Slot scope={storeName} />}
secondarySidebar={ secondarySidebar={isInserterOpened ? <InserterSidebar /> : null}
showInserterSidebar && isInserterOpened ? <InserterSidebar /> : null
}
/> />
{showActivatePanel && <ActivatePanel onClose={toggleActivatePanel} />}
<Popover.Slot /> <Popover.Slot />
</SlotFillProvider> </SlotFillProvider>
</ShortcutProvider> </ShortcutProvider>
@ -113,13 +75,9 @@ function Editor(): JSX.Element {
} }
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
createStore();
const root = document.getElementById('mailpoet_automation_editor'); const root = document.getElementById('mailpoet_automation_editor');
if (root) { if (root) {
registerApiErrorHandler();
initializeApi(); initializeApi();
initializeCoreIntegration();
initializeMailPoetIntegration(); initializeMailPoetIntegration();
ReactDOM.render(<Editor />, root); ReactDOM.render(<Editor />, root);
} }

View File

@ -1,15 +1,9 @@
import { dispatch, select, StoreDescriptor } from '@wordpress/data'; import { select } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls'; 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 interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences'; import { store as preferencesStore } from '@wordpress/preferences';
import { addQueryArgs } from '@wordpress/url';
import { storeName } from './constants'; import { storeName } from './constants';
import { Feature, State } from './types'; import { Feature } from './types';
import { LISTING_NOTICE_PARAMETERS } from '../../listing/workflow-listing-notices';
import { MailPoet } from '../../../mailpoet';
import { WorkflowStatus } from '../../listing/workflow';
export const openSidebar = export const openSidebar =
(key) => (key) =>
@ -32,10 +26,10 @@ export function toggleInserterSidebar() {
} as const; } as const;
} }
export function setInserterPopover(data?: State['inserterPopover']) { export function setInserterPopoverAnchor(anchor?: HTMLElement) {
return { return {
type: 'SET_INSERTER_POPOVER', type: 'SET_INSERTER_POPOVER_ANCHOR',
data, anchor,
} as const; } as const;
} }
@ -48,12 +42,10 @@ export function selectStep(value) {
export function setWorkflowName(name) { export function setWorkflowName(name) {
const workflow = select(storeName).getWorkflowData(); const workflow = select(storeName).getWorkflowData();
workflow.name = name;
return { return {
type: 'UPDATE_WORKFLOW', type: 'UPDATE_WORKFLOW',
workflow: { workflow,
...workflow,
name,
},
} as const; } as const;
} }
@ -62,12 +54,12 @@ export function* save() {
const data = yield apiFetch({ const data = yield apiFetch({
path: `/workflows/${workflow.id}`, path: `/workflows/${workflow.id}`,
method: 'PUT', method: 'PUT',
data: { ...workflow }, data: workflow,
}); });
return { return {
type: 'SAVE', type: 'SAVE',
workflow: data?.data ?? workflow, workflow: data.data,
} as const; } as const;
} }
@ -77,79 +69,14 @@ export function* activate() {
path: `/workflows/${workflow.id}`, path: `/workflows/${workflow.id}`,
method: 'PUT', method: 'PUT',
data: { data: {
...workflow, name: workflow.name,
status: '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 { return {
type: 'ACTIVATE', type: 'ACTIVATE',
workflow: data?.data ?? workflow, workflow: data.data,
} as const;
}
// @ToDo: Decide on best naming once MAILPOET-4731 decides about the "deactivating" status name
export function* deactivate() {
const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
...workflow,
status: 'inactive',
},
});
const { createNotice } = dispatch(noticesStore as StoreDescriptor);
if (data?.data.status === WorkflowStatus.INACTIVE) {
void createNotice(
'success',
__('Automation is now deactivated!', 'mailpoet'),
{
type: 'snackbar',
},
);
}
return {
type: 'DEACTIVATE',
workflow: data?.data ?? workflow,
} as const;
}
export function* trash(onTrashed: () => void = undefined) {
const workflow = select(storeName).getWorkflowData();
const data = yield apiFetch({
path: `/workflows/${workflow.id}`,
method: 'PUT',
data: {
...workflow,
status: 'trash',
},
});
onTrashed?.();
if (data?.status === 'trash') {
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id,
});
}
return {
type: 'TRASH',
workflow: data?.data ?? workflow,
} as const; } as const;
} }
@ -168,10 +95,3 @@ export function updateStepArgs(stepId, name, value) {
value, value,
}; };
} }
export function setErrors(errors) {
return {
type: 'SET_ERRORS',
errors,
};
}

View File

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

View File

@ -2,15 +2,14 @@ import { AutomationEditorWindow, State } from './types';
declare let window: AutomationEditorWindow; declare let window: AutomationEditorWindow;
export const getInitialState = (): State => ({ export const initialState: State = {
context: { ...window.mailpoet_automation_context },
stepTypes: {}, stepTypes: {},
workflowData: { ...window.mailpoet_automation_workflow }, workflowData: { ...window.mailpoet_automation_workflow },
workflowSaved: true,
selectedStep: undefined, selectedStep: undefined,
inserterSidebar: { inserterSidebar: {
isOpened: false, isOpened: false,
}, },
inserterPopover: undefined, inserterPopover: {
errors: undefined, anchor: undefined,
}); },
};

View File

@ -11,10 +11,13 @@ export function reducer(state: State, action: Action): State {
isOpened: !state.inserterSidebar.isOpened, isOpened: !state.inserterSidebar.isOpened,
}, },
}; };
case 'SET_INSERTER_POPOVER': case 'SET_INSERTER_POPOVER_ANCHOR':
return { return {
...state, ...state,
inserterPopover: action.data, inserterPopover: {
...state.inserterPopover,
anchor: action.anchor,
},
}; };
case 'SET_SELECTED_STEP': case 'SET_SELECTED_STEP':
return { return {
@ -25,31 +28,16 @@ export function reducer(state: State, action: Action): State {
return { return {
...state, ...state,
workflowData: action.workflow, workflowData: action.workflow,
workflowSaved: false,
}; };
case 'SAVE': case 'SAVE':
return { return {
...state, ...state,
workflowData: action.workflow, workflowData: action.workflow,
workflowSaved: true,
}; };
case 'ACTIVATE': case 'ACTIVATE':
return { return {
...state, ...state,
workflowData: action.workflow, workflowData: action.workflow,
workflowSaved: true,
};
case 'DEACTIVATE':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
};
case 'TRASH':
return {
...state,
workflowData: action.workflow,
workflowSaved: true,
}; };
case 'REGISTER_STEP_TYPE': case 'REGISTER_STEP_TYPE':
return { return {
@ -60,24 +48,13 @@ export function reducer(state: State, action: Action): State {
}, },
}; };
case 'UPDATE_STEP_ARGS': { case 'UPDATE_STEP_ARGS': {
const prevArgs = state.workflowData.steps[action.stepId].args ?? {};
const value =
typeof action.value === 'function'
? action.value(prevArgs[action.name] ?? undefined)
: action.value;
const args = { const args = {
...prevArgs, ...(state.workflowData.steps[action.stepId].args ?? {}),
[action.name]: value, [action.name]: action.value,
}; };
const step = { ...state.workflowData.steps[action.stepId], args }; const step = { ...state.workflowData.steps[action.stepId], args };
const stepErrors = Object.values(state.errors?.steps ?? {}).filter(
({ step_id }) => step_id !== action.stepId,
);
return { return {
...state, ...state,
workflowData: { workflowData: {
@ -87,24 +64,9 @@ export function reducer(state: State, action: Action): State {
[action.stepId]: step, [action.stepId]: step,
}, },
}, },
workflowSaved: false,
selectedStep: step, selectedStep: step,
errors:
stepErrors.length > 0
? {
...state.errors,
steps: Object.fromEntries(
stepErrors.map((error) => [error.step_id, error]),
),
}
: undefined,
}; };
} }
case 'SET_ERRORS':
return {
...state,
errors: action.errors,
};
default: default:
return state; return state;
} }

View File

@ -1,7 +1,7 @@
import { dispatch } from '@wordpress/data'; import { dispatch } from '@wordpress/data';
import { storeName } from './constants'; import { store } from './store';
import { StepType } from './types'; import { StepType } from './types';
export const registerStepType = (stepType: StepType): void => { export const registerStepType = (stepType: StepType): void => {
dispatch(storeName).registerStepType(stepType); dispatch(store).registerStepType(stepType);
}; };

View File

@ -2,7 +2,7 @@ import { createRegistrySelector } from '@wordpress/data';
import { store as interfaceStore } from '@wordpress/interface'; import { store as interfaceStore } from '@wordpress/interface';
import { store as preferencesStore } from '@wordpress/preferences'; import { store as preferencesStore } from '@wordpress/preferences';
import { storeName } from './constants'; import { storeName } from './constants';
import { Context, Errors, Feature, State, StepError, StepType } from './types'; import { Feature, State, StepType } from './types';
import { Item } from '../components/inserter/item'; import { Item } from '../components/inserter/item';
import { Step, Workflow } from '../components/workflow/types'; import { Step, Workflow } from '../components/workflow/types';
@ -21,21 +21,6 @@ export function isInserterSidebarOpened(state: State): boolean {
return state.inserterSidebar.isOpened; return state.inserterSidebar.isOpened;
} }
export function getContext(state: State): Context {
return state.context;
}
export function getContextStep(
state: State,
key: string,
): Context['steps'][number] | undefined {
return state.context.steps[key];
}
export function getSteps(state: State): StepType[] {
return Object.values(state.stepTypes);
}
export function getInserterActionSteps(state: State): Item[] { export function getInserterActionSteps(state: State): Item[] {
return Object.values(state.stepTypes).filter( return Object.values(state.stepTypes).filter(
({ group }) => group === 'actions', ({ group }) => group === 'actions',
@ -48,36 +33,20 @@ export function getInserterLogicalSteps(state: State): Item[] {
); );
} }
export function getInserterPopover( export function getInserterPopoverAnchor(
state: State, state: State,
): State['inserterPopover'] | undefined { ): HTMLElement | undefined {
return state.inserterPopover; return state.inserterPopover.anchor;
} }
export function getWorkflowData(state: State): Workflow { export function getWorkflowData(state: State): Workflow {
return state.workflowData; return state.workflowData;
} }
export function getWorkflowSaved(state: State): boolean {
return state.workflowSaved;
}
export function getSelectedStep(state: State): Step | undefined { export function getSelectedStep(state: State): Step | undefined {
return state.selectedStep; return state.selectedStep;
} }
export function getStepType(state: State, key: string): StepType | undefined {
return state.stepTypes[key] ?? undefined;
}
export function getSelectedStepType(state: State): StepType | undefined { export function getSelectedStepType(state: State): StepType | undefined {
return getStepType(state, state.selectedStep?.key); return state.stepTypes[state.selectedStep?.key] ?? undefined;
}
export function getErrors(state: State): Errors | undefined {
return state.errors;
}
export function getStepError(state: State, id: string): StepError | undefined {
return state.errors?.steps[id] ?? undefined;
} }

View File

@ -1,14 +1,8 @@
import { import { createReduxStore, register, StoreDescriptor } from '@wordpress/data';
createReduxStore,
register,
StoreConfig,
StoreDescriptor,
} from '@wordpress/data';
import { controls } from '@wordpress/data-controls'; import { controls } from '@wordpress/data-controls';
import { Hooks } from 'wp-js-hooks';
import * as actions from './actions'; import * as actions from './actions';
import { storeName } from './constants'; import { storeName } from './constants';
import { getInitialState } from './initial_state'; import { initialState } from './initial_state';
import { reducer } from './reducer'; import { reducer } from './reducer';
import * as selectors from './selectors'; import * as selectors from './selectors';
import { State } from './types'; import { State } from './types';
@ -18,29 +12,20 @@ type StoreType = Omit<StoreDescriptor, 'name'> & {
name: typeof storeName; name: typeof storeName;
}; };
export const createStore = (): StoreType => { export const store = createReduxStore<State>(storeName, {
const storeConfig = Hooks.applyFilters( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the "Action" type is missing thunks with "dispatch"
'mailpoet.automation.editor.create_store', actions: actions as any,
{ controls,
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the "Action" type is missing thunks with "dispatch" selectors,
actions: actions as any, reducer,
controls, initialState,
selectors, }) as StoreType;
reducer,
initialState: getInitialState(),
} as StoreConfig<State>,
) as StoreConfig<State>;
const store = createReduxStore<State>(storeName, storeConfig) as StoreType; type StoreKey = typeof storeName | StoreType;
register(store);
return store;
};
export type StoreKey = typeof storeName | StoreType;
declare module '@wordpress/data' { declare module '@wordpress/data' {
function select(key: StoreKey): OmitFirstArgs<typeof selectors>; function select(key: StoreKey): OmitFirstArgs<typeof selectors>;
function dispatch(key: StoreKey): typeof actions; function dispatch(key: StoreKey): typeof actions;
} }
export { actions, selectors }; register(store);

View File

@ -2,61 +2,30 @@ import { ComponentType } from 'react';
import { Step, Workflow } from '../components/workflow/types'; import { Step, Workflow } from '../components/workflow/types';
export interface AutomationEditorWindow extends Window { export interface AutomationEditorWindow extends Window {
mailpoet_automation_context: Context;
mailpoet_automation_workflow: Workflow; mailpoet_automation_workflow: Workflow;
} }
export type Context = { export type StepGroup = 'actions' | 'logical';
steps: Record<
string,
{
key: string;
name: string;
args_schema: {
type: 'object';
properties?: Record<string, { type: string; default?: unknown }>;
};
}
>;
};
export type StepGroup = 'actions' | 'logical' | 'triggers';
export type StepType = { export type StepType = {
key: string; key: string;
group: StepGroup; group: StepGroup;
title: string; title: string;
description: string; description: string;
subtitle: (step: Step) => JSX.Element | string;
icon: ComponentType; icon: ComponentType;
edit: ComponentType; edit: ComponentType;
foreground: string;
background: string;
};
export type StepError = {
step_id: string;
message: string;
};
export type Errors = {
steps: Record<string, StepError>;
}; };
export type State = { export type State = {
context: Context;
stepTypes: Record<string, StepType>; stepTypes: Record<string, StepType>;
workflowData: Workflow; workflowData: Workflow;
workflowSaved: boolean;
selectedStep: Step | undefined; selectedStep: Step | undefined;
inserterSidebar: { inserterSidebar: {
isOpened: boolean; isOpened: boolean;
}; };
inserterPopover?: { inserterPopover: {
anchor: HTMLElement; anchor?: HTMLElement;
type: 'steps' | 'triggers';
}; };
errors?: Errors;
}; };
export type Feature = 'fullscreenMode' | 'showIconLabels'; export type Feature = 'fullscreenMode' | 'showIconLabels';

View File

@ -1,3 +0,0 @@
// exports for extensibility
export { id } from './id';
export * as EditorStore from './editor/store';

View File

@ -1,6 +0,0 @@
import { registerStepType } from '../../editor/store';
import { step as DelayStep } from './steps/delay';
export const initialize = (): void => {
registerStepType(DelayStep);
};

View File

@ -1,65 +0,0 @@
import {
PanelBody,
TextControl,
SelectControl,
Flex,
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 } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
}),
[],
);
const delayValueInputId = `delay-number-${selectedStep.id}`;
return (
<PanelBody opened>
<label htmlFor={delayValueInputId}>
<PlainBodyTitle title={__('Wait for', 'mailpoet')} />
</label>
<Flex align="top">
<FlexItem style={{ flex: '1 1 0' }}>
<TextControl
id={delayValueInputId}
type="number"
placeholder="Number"
value={(selectedStep.args.delay as string) ?? ''}
onChange={(rawValue) => {
const value: number =
rawValue.length === 0 || parseInt(rawValue, 10) < 1
? 1
: parseInt(rawValue, 10);
dispatch(storeName).updateStepArgs(
selectedStep.id,
'delay',
value,
);
}}
/>
</FlexItem>
<FlexItem style={{ flex: '1 1 0' }}>
<SelectControl
label=""
value={(selectedStep.args.delay_type as string) ?? 'HOURS'}
options={DelayTypeOptions}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'delay_type',
value,
)
}
/>
</FlexItem>
</Flex>
</PanelBody>
);
}

View File

@ -1,7 +0,0 @@
export function Icon(): JSX.Element {
return (
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M9.79683 0.666656C4.6635 0.666656 0.463501 4.86666 0.463501 9.99999C0.463501 15.1333 4.6635 19.3333 9.79683 19.3333C14.9302 19.3333 19.1302 15.1333 19.1302 9.99999C19.1302 4.86666 14.9302 0.666656 9.79683 0.666656ZM9.79683 17.4667C5.68083 17.4667 2.33017 14.116 2.33017 9.99999C2.33017 5.88399 5.68083 2.53332 9.79683 2.53332C13.9128 2.53332 17.2635 5.88399 17.2635 9.99999C17.2635 14.116 13.9128 17.4667 9.79683 17.4667ZM10.2635 5.33332H8.8635V10.9333L13.7168 13.92L14.4635 12.7067L10.2635 10.1867V5.33332Z" />
</svg>
);
}

View File

@ -1,33 +0,0 @@
import { Icon } from './icon';
import { Edit } from './edit';
import { StepType } from '../../../../editor/store/types';
import { DelayTypeOptions } from './types/delayTypes';
const getDelayInformation = (delayTypeValue: string, value: number): string =>
DelayTypeOptions.reduce((previousValue, current): string => {
if (current.value !== delayTypeValue) {
return previousValue;
}
return current.subtitle(value);
}, '');
export const step: StepType = {
key: 'core:delay',
group: 'actions',
title: 'Delay',
foreground: '#7F54B3',
background: '#f7edf7',
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.';
}
return getDelayInformation(
data.args.delay_type as string,
data.args.delay as number,
);
},
icon: Icon,
edit: Edit,
} as const;

View File

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

View File

@ -1,53 +0,0 @@
import { FormTokenField as WpFormTokenField } from '@wordpress/components';
export type FormTokenItem = {
id: number | string;
name: string;
};
export type FormTokenFieldProps = Omit<
WpFormTokenField.Props,
'value' | 'suggestions' | 'onChange'
> & {
value: FormTokenItem[];
suggestions: FormTokenItem[];
onChange: (values: FormTokenItem[]) => void;
placeholder: string;
label: string;
};
export function FormTokenField({
label,
value,
suggestions,
placeholder,
onChange,
...props
}: FormTokenFieldProps): JSX.Element {
return (
<WpFormTokenField
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// The following error seems to be a mismatch. It claims the 'label' prop does not exist, but it does.
label={label}
value={value.map((item) => item.name)}
suggestions={suggestions.map((item) => item.name)}
__experimentalExpandOnFocus
__experimentalAutoSelectFirstMatch
placeholder={placeholder}
onChange={(raw: string[]) => {
const allSelected: FormTokenItem[] = raw
.map((item) => {
const match = suggestions.find(
(suggestion) =>
suggestion.name.toLowerCase() === item.toLowerCase(),
);
return match ?? null;
})
.filter((item) => item !== null);
onChange(allSelected);
}}
{...props}
/>
);
}

View File

@ -1,10 +1,6 @@
import { registerStepType } from '../../editor/store'; import { registerStepType } from '../../editor/store';
import { step as SendEmailStep } from './steps/send_email'; 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';
export const initialize = (): void => { export const initialize = (): void => {
registerStepType(SendEmailStep); registerStepType(SendEmailStep);
registerStepType(WpUserRegisteredTrigger);
registerStepType(SomeoneSubscribesTrigger);
}; };

View File

@ -1,11 +1,14 @@
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
import { PanelBody, TextareaControl, TextControl } from '@wordpress/components'; import { PanelBody, TextareaControl, TextControl } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data'; import { dispatch, useSelect } from '@wordpress/data';
import { ShortcodeHelpText } from './shortcode_help_text'; import { plus, edit, Icon } from '@wordpress/icons';
import { PlainBodyTitle } from '../../../../../editor/components/panel'; import { Thumbnail } from './thumbnail';
import { storeName } from '../../../../../editor/store'; import { Button } from '../../components/button';
import { StepName } from '../../../../../editor/components/panel/step-name'; import {
import { EditNewsletter } from './edit_newsletter'; PlainBodyTitle,
TitleActionButton,
} from '../../../../editor/components/panel';
import { store } from '../../../../editor/store';
function SingleLineTextareaControl( function SingleLineTextareaControl(
props: ComponentProps<typeof TextareaControl>, props: ComponentProps<typeof TextareaControl>,
@ -30,47 +33,36 @@ function SingleLineTextareaControl(
); );
} }
export function EmailPanel(): JSX.Element { export function Edit(): JSX.Element {
const { selectedStep, selectedStepType } = useSelect( const { selectedStep } = useSelect(
(select) => ({ (select) => ({
selectedStep: select(storeName).getSelectedStep(), selectedStep: select(store).getSelectedStep(),
selectedStepType: select(storeName).getSelectedStepType(),
}), }),
[], [],
); );
return ( return (
<PanelBody opened> <PanelBody opened>
<StepName <PlainBodyTitle title="Email">
currentName={(selectedStep.args.name as string) ?? ''} <TitleActionButton>
defaultName={selectedStepType.title} <Icon icon={edit} size={16} />
update={(value) => { </TitleActionButton>
dispatch(storeName).updateStepArgs(selectedStep.id, 'name', value); </PlainBodyTitle>
}}
/>
<TextControl <TextControl
label="“From” name" label="“From” name"
placeholder="John Doe" placeholder="John Doe"
value={(selectedStep.args.sender_name as string) ?? ''} value={(selectedStep.args.from_name as string) ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(store).updateStepArgs(selectedStep.id, 'from_name', value)
selectedStep.id,
'sender_name',
value,
)
} }
/> />
<TextControl <TextControl
type="email" type="email"
label="“From” email address" label="“From” email address"
placeholder="you@domain.com" placeholder="you@domain.com"
value={(selectedStep.args.sender_address as string) ?? ''} value={(selectedStep.args.email as string) ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(store).updateStepArgs(selectedStep.id, 'email', value)
selectedStep.id,
'sender_address',
value,
)
} }
/> />
<SingleLineTextareaControl <SingleLineTextareaControl
@ -78,27 +70,27 @@ export function EmailPanel(): JSX.Element {
placeholder="Type in subject…" placeholder="Type in subject…"
value={(selectedStep.args.subject as string) ?? ''} value={(selectedStep.args.subject as string) ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs(selectedStep.id, 'subject', value) dispatch(store).updateStepArgs(selectedStep.id, 'subject', value)
} }
help={<ShortcodeHelpText />}
/> />
<SingleLineTextareaControl <SingleLineTextareaControl
label="Preheader" label="Preheader"
placeholder="Type in preheader…" placeholder="Type in preheader…"
value={(selectedStep.args.preheader as string) ?? ''} value={(selectedStep.args.preheader as string) ?? ''}
onChange={(value) => onChange={(value) =>
dispatch(storeName).updateStepArgs( dispatch(store).updateStepArgs(selectedStep.id, 'preheader', value)
selectedStep.id,
'preheader',
value,
)
} }
help={<ShortcodeHelpText />}
/> />
<div className="mailpoet-automation-email-content-separator" /> <div className="mailpoet-automation-email-content-separator" />
<PlainBodyTitle title="Email" /> <PlainBodyTitle title="Email content" />
<EditNewsletter /> {selectedStep.args.email_id ? (
<Thumbnail emailId={selectedStep.args.email_id as number} />
) : (
<Button variant="sidebar-primary" centered icon={plus}>
Design email
</Button>
)}
</PanelBody> </PanelBody>
); );
} }

View File

@ -1,88 +0,0 @@
import { dispatch, useSelect } from '@wordpress/data';
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';
export function EditNewsletter(): JSX.Element {
const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
useState(false);
const { selectedStep, workflowId, workflowSaved } = useSelect(
(select) => ({
selectedStep: select(storeName).getSelectedStep(),
workflowId: select(storeName).getWorkflowData().id,
workflowSaved: select(storeName).getWorkflowSaved(),
}),
[],
);
const emailId = selectedStep?.args?.email_id as number | undefined;
const workflowStepId = selectedStep.id;
const createEmail = useCallback(async () => {
setRedirectToTemplateSelection(true);
const response = await MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: {
type: 'automation',
subject: '',
options: {
workflowId,
workflowStepId,
},
},
});
dispatch(storeName).updateStepArgs(
workflowStepId,
'email_id',
parseInt(response.data.id as string, 10),
);
dispatch(storeName).save();
}, [workflowId, workflowStepId]);
// This component is rendered only when no email ID is set. Once we have the ID
// and the workflow is saved, we can safely redirect to the email design flow.
useEffect(() => {
if (redirectToTemplateSelection && emailId && workflowSaved) {
window.location.href = `admin.php?page=mailpoet-newsletters#/template/${emailId}`;
}
}, [emailId, workflowSaved, redirectToTemplateSelection]);
if (!emailId || redirectToTemplateSelection) {
return (
<Button
variant="sidebar-primary"
centered
icon={plus}
onClick={createEmail}
isBusy={redirectToTemplateSelection}
disabled={redirectToTemplateSelection}
>
Design email
</Button>
);
}
return (
<div className="mailpoet-automation-email-buttons">
<Button
variant="sidebar-primary"
centered
href={`?page=mailpoet-newsletter-editor&id=${
selectedStep.args.email_id as string
}`}
>
Edit content
</Button>
<Button variant="secondary" centered>
Preview
</Button>
</div>
);
}

View File

@ -1,50 +0,0 @@
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';
export function GoogleAnalyticsPanel(): JSX.Element {
const { selectedStep } = useSelect(
(select) => ({ selectedStep: select(storeName).getSelectedStep() }),
[],
);
const enabled = typeof selectedStep.args?.ga_campaign !== 'undefined';
const panelBody = Hooks.applyFilters(
'mailpoet.automation.send_email.google_analytics_panel',
<PremiumModal
onRequestClose={() =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
undefined,
)
}
>
{__(
'Google Analytics tracking is not available in the free version of the MailPoet plugin.',
'mailpoet',
)}
</PremiumModal>,
);
return (
<PanelBody title="Google analytics" initialOpen={false}>
<ToggleControl
label="Enable custom GA tracking"
checked={enabled}
onChange={(value) =>
dispatch(storeName).updateStepArgs(
selectedStep.id,
'ga_campaign',
value ? '' : undefined,
)
}
/>
{enabled && panelBody}
</PanelBody>
);
}

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