Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe6fa9480a | |||
6cb51a3841 | |||
122f73c3c2 |
@ -99,12 +99,12 @@ executors:
|
||||
wpcli_php_max_wporg:
|
||||
<<: *default_job_config
|
||||
docker:
|
||||
- image: mailpoet/wordpress:8.1_20230307.1 # We need to use 8.1 to emulate the WP.org environment
|
||||
- image: mailpoet/wordpress:8.1_20230307.1
|
||||
|
||||
wpcli_php_latest:
|
||||
<<: *default_job_config
|
||||
docker:
|
||||
- image: mailpoet/wordpress:8.2_20241126.1
|
||||
- image: mailpoet/wordpress:8.1_20230307.1
|
||||
|
||||
wpcli_php_mysql_oldest:
|
||||
<<: *default_job_config
|
||||
@ -115,7 +115,7 @@ executors:
|
||||
wpcli_php_mysql_latest:
|
||||
<<: *default_job_config
|
||||
docker:
|
||||
- image: mailpoet/wordpress:8.2_20241126.1
|
||||
- image: mailpoet/wordpress:8.1_20230307.1
|
||||
- image: cimg/mysql:8.0
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_520_ci
|
||||
|
||||
@ -141,8 +141,6 @@ jobs:
|
||||
key: composer-{{ checksum "tasks/code_sniffer/composer.json" }}-{{ checksum "tasks/code_sniffer/composer.lock" }}
|
||||
- restore_cache:
|
||||
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
||||
- restore_cache:
|
||||
key: composer-{{ checksum "../tests_env/composer.json" }}-{{ checksum "../tests_env/composer.lock" }}
|
||||
- restore_cache:
|
||||
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
||||
- restore_cache:
|
||||
@ -159,7 +157,7 @@ jobs:
|
||||
./do install
|
||||
./do compile:all --env production --skip-tests
|
||||
./do doctrine:generate-cache
|
||||
../tests_env/vendor/bin/codecept build
|
||||
vendor/bin/codecept build
|
||||
./do twig:generate-cache
|
||||
- run:
|
||||
name: 'Check Prettier formatting'
|
||||
@ -180,10 +178,6 @@ jobs:
|
||||
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
||||
paths:
|
||||
- vendor
|
||||
- save_cache:
|
||||
key: composer-{{ checksum "../tests_env/composer.json" }}-{{ checksum "../tests_env/composer.lock" }}
|
||||
paths:
|
||||
- ../tests_env/vendor
|
||||
- save_cache:
|
||||
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
||||
paths:
|
||||
@ -197,10 +191,10 @@ jobs:
|
||||
- run:
|
||||
name: Download additional WP Plugins for tests
|
||||
command: |
|
||||
./do download:woo-commerce-zip 9.4.3
|
||||
./do download:woo-commerce-subscriptions-zip 6.9.1
|
||||
./do download:woo-commerce-zip 9.1.4
|
||||
./do download:woo-commerce-subscriptions-zip 6.5.0
|
||||
./do download:woo-commerce-memberships-zip 1.26.5
|
||||
./do download:automate-woo-zip 6.1.3
|
||||
./do download:automate-woo-zip 6.0.31
|
||||
- run:
|
||||
name: Dump tests ENV variables for acceptance tests
|
||||
command: |
|
||||
@ -451,19 +445,19 @@ jobs:
|
||||
- run:
|
||||
name: 'Pull test docker images'
|
||||
# Pull docker images with 3 retries
|
||||
command: i='0';while ! docker compose -f ../tests_env/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
command: i='0';while ! docker-compose -f ../tests_env/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
- run:
|
||||
name: Create docker containers for test
|
||||
# We experienced some failures when creating containers so we do it explicitly with one retry
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose create || docker compose create
|
||||
docker-compose create || docker-compose create
|
||||
- run:
|
||||
# Some tools we use may need different version based on PHP version used in docker
|
||||
name: Ensure correct versions of tools
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project -e COMPOSER_DEV_MODE=1 --entrypoint "php tools/install.php" codeception_acceptance
|
||||
docker-compose run --rm -w /project -e COMPOSER_DEV_MODE=1 --entrypoint "php tools/install.php" codeception_acceptance
|
||||
- when:
|
||||
condition: ${WOOCOMMERCE_VERSION}
|
||||
steps:
|
||||
@ -471,7 +465,7 @@ jobs:
|
||||
name: Download WooCommerce Core
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
- when:
|
||||
condition: << parameters.woo_subscriptions_version >>
|
||||
steps:
|
||||
@ -479,7 +473,7 @@ jobs:
|
||||
name: Download WooCommerce Subscriptions
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
- when:
|
||||
condition: << parameters.woo_memberships_version >>
|
||||
steps:
|
||||
@ -487,7 +481,7 @@ jobs:
|
||||
name: Download WooCommerce Memberships
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-memberships-zip << parameters.woo_memberships_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-memberships-zip << parameters.woo_memberships_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
- when:
|
||||
condition: << parameters.automate_woo_version >>
|
||||
steps:
|
||||
@ -495,7 +489,7 @@ jobs:
|
||||
name: Download AutomateWoo
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
- run:
|
||||
name: Group acceptance tests
|
||||
command: |
|
||||
@ -524,7 +518,7 @@ jobs:
|
||||
--xml
|
||||
-g circleci_split_group
|
||||
)
|
||||
docker compose run -e SKIP_DEPS=1 \
|
||||
docker-compose run -e SKIP_DEPS=1 \
|
||||
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
|
||||
-e CIRCLE_JOB=${CIRCLE_JOB} \
|
||||
-e MULTISITE=<< parameters.multisite >> \
|
||||
@ -583,13 +577,13 @@ jobs:
|
||||
- run:
|
||||
name: 'Pull test docker images'
|
||||
# Pull docker images with 3 retries
|
||||
command: i='0';while ! docker compose -f tests/performance/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
command: i='0';while ! docker-compose -f tests/performance/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
- run:
|
||||
name: Create docker containers for test
|
||||
# We experienced some failures when creating containers so we do it explicitly with one retry
|
||||
command: |
|
||||
cd tests/performance
|
||||
docker compose create || docker compose create
|
||||
docker-compose create || docker-compose create
|
||||
- run:
|
||||
name: Run performance tests
|
||||
command: |
|
||||
@ -747,19 +741,19 @@ jobs:
|
||||
- run:
|
||||
name: 'Pull test docker images'
|
||||
# Pull docker images with 3 retries
|
||||
command: i='0';while ! docker compose -f ../tests_env/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
command: i='0';while ! docker-compose -f ../tests_env/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
|
||||
- run:
|
||||
name: Create docker containers for test
|
||||
# We experienced some failures when creating containers so we do it explicitly with one retry
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose create || docker compose create
|
||||
docker-compose create || docker-compose create
|
||||
- run:
|
||||
# Some tools we use may need different version based on PHP version used in docker
|
||||
name: Ensure correct versions of tools
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project -e COMPOSER_DEV_MODE=1 --entrypoint "php tools/install.php" codeception_integration
|
||||
docker-compose run --rm -w /project -e COMPOSER_DEV_MODE=1 --entrypoint "php tools/install.php" codeception_integration
|
||||
- when:
|
||||
condition: ${WOOCOMMERCE_VERSION}
|
||||
steps:
|
||||
@ -767,7 +761,7 @@ jobs:
|
||||
name: Download WooCommerce Core
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
- when:
|
||||
condition: << parameters.woo_subscriptions_version >>
|
||||
steps:
|
||||
@ -775,7 +769,7 @@ jobs:
|
||||
name: Download WooCommerce Subscriptions
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
- when:
|
||||
condition: << parameters.woo_memberships_version >>
|
||||
steps:
|
||||
@ -783,7 +777,7 @@ jobs:
|
||||
name: Download WooCommerce Memberships
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-memberships-zip << parameters.woo_memberships_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:woo-commerce-memberships-zip << parameters.woo_memberships_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
- when:
|
||||
condition: << parameters.automate_woo_version >>
|
||||
steps:
|
||||
@ -791,7 +785,7 @@ jobs:
|
||||
name: Download AutomateWoo
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker-compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
- run:
|
||||
name: 'PHP Integration tests'
|
||||
command: |
|
||||
@ -810,7 +804,7 @@ jobs:
|
||||
if [[ -n '<< parameters.skip_group >>' ]]; then
|
||||
args+=(--skip-group << parameters.skip_group >>)
|
||||
fi
|
||||
docker compose run -e SKIP_DEPS=1 \
|
||||
docker-compose run -e SKIP_DEPS=1 \
|
||||
-e CIRCLE_BRANCH=${CIRCLE_BRANCH} \
|
||||
-e CIRCLE_JOB=${CIRCLE_JOB} \
|
||||
-e SKIP_PLUGINS=<< parameters.skip_plugins >> \
|
||||
@ -863,30 +857,6 @@ jobs:
|
||||
root: /home/circleci/mailpoet
|
||||
paths:
|
||||
- mailpoet/release_zip_build_number.txt
|
||||
- mailpoet/mailpoet.zip
|
||||
qit_security_scan:
|
||||
executor: wpcli_php_latest
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Set up environment'
|
||||
command: |
|
||||
# Copy built ZIP to local directory for easier access from QIT
|
||||
cp /home/circleci/mailpoet/mailpoet.zip .
|
||||
# Authenticate in QIT
|
||||
./vendor/bin/qit partner:add --user="${QIT_PARTNER_USER}" --application_password="${QIT_PARTNER_SECRET}"
|
||||
- run:
|
||||
name: 'QIT Security Test'
|
||||
command: ./do qa:qit-security | tee tests/_output/qit-security
|
||||
- run:
|
||||
name: 'Retrieve test results'
|
||||
command: |
|
||||
# Download HTML report from QIT servers
|
||||
grep "Result Url" tests/_output/qit-security | awk '{ print $3 }' | xargs curl -o tests/_output/report.html
|
||||
when: always
|
||||
- store_artifacts:
|
||||
path: tests/_output
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
@ -940,28 +910,40 @@ workflows:
|
||||
name: acceptance_tests_base_and_woo
|
||||
enable_hpos: 1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_tests_woo_hpos_sync_on
|
||||
group: woo
|
||||
enable_hpos_sync: 1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_tests_woo_hpos_off
|
||||
group: woo
|
||||
disable_hpos: 1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_tests_blockbased_theme
|
||||
group: frontend
|
||||
blockbased_theme: 1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- js_tests:
|
||||
<<: *slack-fail-post-step
|
||||
requires:
|
||||
@ -972,41 +954,55 @@ workflows:
|
||||
enable_hpos_sync: 1
|
||||
name: integration_test_woo_hpos_sync_on
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
group: woo
|
||||
name: integration_test_woo_hpos_on
|
||||
enable_hpos: 1
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
group: woo
|
||||
disable_hpos: 1
|
||||
name: integration_test_woo_hpos_off
|
||||
requires:
|
||||
- build
|
||||
- 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:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
<<: *multisite_acceptance_config
|
||||
name: acceptance_tests_multisite
|
||||
requires:
|
||||
- build
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
<<: *only_trunk_and_release
|
||||
multisite: 1
|
||||
name: integration_tests_multisite
|
||||
requires:
|
||||
- build
|
||||
- unit_tests
|
||||
- static_analysis_php7
|
||||
- static_analysis_php8
|
||||
- qa_js
|
||||
- qa_php
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
<<: *only_release
|
||||
@ -1035,22 +1031,9 @@ workflows:
|
||||
- integration_test_woo_hpos_on
|
||||
- integration_test_woo_hpos_off
|
||||
- integration_test_woo_hpos_sync_on
|
||||
- integration_with_premium_latest
|
||||
- acceptance_tests_woo_hpos_sync_on
|
||||
- acceptance_tests_woo_hpos_off
|
||||
- acceptance_tests_blockbased_theme
|
||||
- static_analysis_php8
|
||||
- static_analysis_php7
|
||||
- unit_tests
|
||||
- qa_js
|
||||
- qa_php
|
||||
- qa_php_oldest
|
||||
- security_analysis
|
||||
- qa_php_max_wporg
|
||||
- qit_security_scan:
|
||||
<<: *slack-fail-post-step
|
||||
requires:
|
||||
- build_release_zip
|
||||
|
||||
nightly:
|
||||
triggers:
|
||||
@ -1082,15 +1065,15 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_oldest
|
||||
woo_core_version: 9.3.4
|
||||
woo_subscriptions_version: 6.8.0
|
||||
woo_core_version: 9.0.2
|
||||
woo_subscriptions_version: 6.4.1
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
automate_woo_version: 5.8.5
|
||||
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
|
||||
mysql_image: mysql:5.5
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.6.2
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.5.5
|
||||
requires:
|
||||
- build
|
||||
- performance_tests:
|
||||
@ -1123,14 +1106,14 @@ workflows:
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_oldest
|
||||
woo_core_version: 9.3.4
|
||||
woo_subscriptions_version: 6.8.0
|
||||
woo_core_version: 9.0.2
|
||||
woo_subscriptions_version: 6.4.1
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
automate_woo_version: 5.8.5
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.6.2
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI # We use image with PHP 6.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.5.5
|
||||
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
|
||||
mysql_image: mysql:5.5
|
||||
requires:
|
||||
- build
|
||||
@ -1186,26 +1169,26 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_with_premium_oldest
|
||||
woo_core_version: 9.3.4
|
||||
woo_subscriptions_version: 6.8.0
|
||||
woo_core_version: 9.0.2
|
||||
woo_subscriptions_version: 6.4.1
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
automate_woo_version: 5.8.5
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.6.2
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.5.5
|
||||
requires:
|
||||
- build_premium
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_with_premium_oldest
|
||||
woo_core_version: 9.3.4
|
||||
woo_subscriptions_version: 6.8.0
|
||||
woo_core_version: 9.0.2
|
||||
woo_subscriptions_version: 6.4.1
|
||||
woo_memberships_version: 1.25.2
|
||||
automate_woo_version: 6.0.33
|
||||
automate_woo_version: 5.8.5
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.6.2
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 6.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.5.5
|
||||
mysql_command: --max_allowed_packet=100M --default-storage-engine=MYISAM
|
||||
mysql_image: mysql:5.5
|
||||
requires:
|
||||
- build_premium
|
||||
|
@ -10,9 +10,6 @@ indent_size = 2
|
||||
ij_smart_tabs = false
|
||||
max_line_length = off
|
||||
|
||||
[packages/php/email-editor/**]
|
||||
indent_style = tab
|
||||
|
||||
[*.php]
|
||||
ij_php_align_key_value_pairs = false
|
||||
ij_php_align_multiline_chained_methods = false
|
||||
@ -58,5 +55,3 @@ ij_php_space_after_for_semicolon = true
|
||||
ij_php_space_after_colon_in_return_type = true
|
||||
ij_php_space_before_else_keyword = true
|
||||
ij_php_for_statement_new_line_after_left_paren = true
|
||||
ij_php_class_brace_style = end_of_line
|
||||
ij_php_comma_after_last_array_element = true
|
||||
|
@ -39,15 +39,3 @@ e66c76133ec3ef667e382203426d91a4b4aa5174
|
||||
|
||||
# Prettier autoformatting
|
||||
ab27eaee2df740c0add4331a7f8c115a87ecfa2b
|
||||
|
||||
# Move email editor to JS packages folder
|
||||
912282f57ccc839491ff951ec5cf7aa10c14f429
|
||||
|
||||
# Switch email editor js packages to WP coding style
|
||||
b2fb96f8793b63db629d5237010d87332330c51e
|
||||
|
||||
# Email editor Prettier autoformatting
|
||||
8c604453b1d82e3a2c731241e1c96ea8b32ec716
|
||||
|
||||
# Move email editor components out of the engine folder
|
||||
1c3ea9cd0a5fc8848a64d840e2fa16a6c7d8c1fe
|
||||
|
210
.github/workflows/email-editor-package.yml
vendored
210
.github/workflows/email-editor-package.yml
vendored
@ -1,210 +0,0 @@
|
||||
name: Email Editor Package Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['7.4', '8.2']
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache Composer vendor dependencies for MailPoet
|
||||
id: composer-mailpoet-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: mailpoet/vendor
|
||||
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('mailpoet/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Cache Composer vendor-prefixed dependencies for MailPoet
|
||||
id: vendor-prefixed-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: mailpoet/vendor-prefixed
|
||||
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Cache Composer vendor for test environment
|
||||
id: composer-tests-env-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: tests_env/vendor
|
||||
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('tests_env/composer.lock') }}-${{ hashFiles('tests_env/composer.json') }}
|
||||
|
||||
- name: Cache Composer dependencies for Email Editor
|
||||
id: composer-email-editor-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/php/email-editor/vendor
|
||||
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
extensions: gd
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
COMPOSER_DEV_MODE=1 php tools/install.php
|
||||
touch .env
|
||||
working-directory: mailpoet
|
||||
|
||||
# Install Test Environment dependencies only if the cache was not hit
|
||||
- name: Install test environment dependencies
|
||||
if: steps.composer-tests-env-cache.outputs.cache-hit != 'true'
|
||||
run: ../mailpoet/tools/vendor/composer.phar install
|
||||
working-directory: tests_env
|
||||
|
||||
# Install MailPoet dependencies only if the cache was not hit
|
||||
- name: Install mailpoet dependencies
|
||||
if: steps.composer-mailpoet-cache.outputs.cache-hit != 'true'
|
||||
run: ./tools/vendor/composer.phar install
|
||||
working-directory: mailpoet
|
||||
|
||||
# Install Email Editor dependencies only if the cache was not hit
|
||||
- name: Install email-editor dependencies
|
||||
if: steps.composer-email-editor-cache.outputs.cache-hit != 'true'
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar install
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
# Dump Email Editor autoload
|
||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
||||
- name: Dump email-editor autoload
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar dump-autoload
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
# Dump MailPoet autoload
|
||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
||||
- name: Dump MailPoet autoload
|
||||
run: ./tools/vendor/composer.phar dump-autoload
|
||||
working-directory: mailpoet
|
||||
|
||||
# Run Email Editor unit tests
|
||||
- name: Run email-editor package unit tests
|
||||
run: ../../../tests_env/vendor/bin/codecept build && ../../../mailpoet/tools/vendor/composer.phar unit-test
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
- name: Run email-editor package integration tests
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar integration-test
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
COMPOSER_DEV_MODE=1 php tools/install.php
|
||||
touch .env
|
||||
working-directory: mailpoet
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: ../../tools/vendor/composer.phar install
|
||||
working-directory: mailpoet/tasks/code_sniffer
|
||||
|
||||
- name: Run code style check
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar code-style
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
phpstan-static-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['7.4', '8.2']
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache Composer vendor dependencies for MailPoet
|
||||
id: composer-mailpoet-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: mailpoet/vendor
|
||||
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('mailpoet/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Cache Composer vendor-prefixed dependencies for MailPoet
|
||||
id: vendor-prefixed-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: mailpoet/vendor-prefixed
|
||||
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Cache Composer vendor for test environment
|
||||
id: composer-tests-env-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: tests_env/vendor
|
||||
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('tests_env/composer.lock') }}-${{ hashFiles('tests_env/composer.json') }}
|
||||
|
||||
- name: Cache Composer dependencies for Email Editor
|
||||
id: composer-email-editor-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/php/email-editor/vendor
|
||||
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
extensions: gd
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
COMPOSER_DEV_MODE=1 php tools/install.php
|
||||
touch .env
|
||||
working-directory: mailpoet
|
||||
|
||||
# Install Test Environment dependencies only if the cache was not hit
|
||||
- name: Install test environment dependencies
|
||||
if: steps.composer-tests-env-cache.outputs.cache-hit != 'true'
|
||||
run: ../mailpoet/tools/vendor/composer.phar install
|
||||
working-directory: tests_env
|
||||
|
||||
# Install MailPoet dependencies only if the cache was not hit
|
||||
- name: Install mailpoet dependencies
|
||||
if: steps.composer-mailpoet-cache.outputs.cache-hit != 'true'
|
||||
run: ./tools/vendor/composer.phar install
|
||||
working-directory: mailpoet
|
||||
|
||||
# Install Email Editor dependencies only if the cache was not hit
|
||||
- name: Install email-editor dependencies
|
||||
if: steps.composer-email-editor-cache.outputs.cache-hit != 'true'
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar install
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: ../../tools/vendor/composer.phar install
|
||||
working-directory: mailpoet/tasks/phpstan
|
||||
|
||||
# Dump Email Editor autoload
|
||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
||||
- name: Dump email-editor autoload
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar dump-autoload
|
||||
working-directory: packages/php/email-editor
|
||||
|
||||
# Dump MailPoet autoload
|
||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
||||
- name: Dump MailPoet autoload
|
||||
run: ./tools/vendor/composer.phar dump-autoload
|
||||
working-directory: mailpoet
|
||||
|
||||
- name: Run code phpstan
|
||||
run: ../../../mailpoet/tools/vendor/composer.phar phpstan -- --php-version=${{ matrix.php-version == '7.4' && '70400' || '80200' }}
|
||||
working-directory: packages/php/email-editor
|
@ -1,27 +0,0 @@
|
||||
name: Add link to WordPress Playground preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
add-wp-playground-link:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check and append description
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
BRANCH_NAME="${{ github.head_ref }}"
|
||||
DESCRIPTION="$(gh pr view $PR_NUMBER --json body -q .body)"
|
||||
HEADING="## Preview"
|
||||
CONTENT="$(printf "${HEADING}\n\n[Preview in WordPress Playground](https://account.mailpoet.com/playground/new/branch:${BRANCH_NAME})\n\n_The latest successful build from \`${BRANCH_NAME}\` will be used. If none is available, the link won't work._")"
|
||||
|
||||
if [[ "$DESCRIPTION" != *"$HEADING"* ]]; then
|
||||
gh pr edit $PR_NUMBER --body "$(printf "${DESCRIPTION}\n\n${CONTENT}")"
|
||||
fi
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,5 +9,3 @@ npm-debug.log
|
||||
mailpoet-premium
|
||||
tsconfig.tsbuildinfo
|
||||
/wordpress
|
||||
packages/php/*/vendor
|
||||
tests_env/vendor
|
||||
|
@ -7,6 +7,7 @@ export MP_GIT_HOOKS_ESLINT="${MP_GIT_HOOKS_ESLINT:-true}"
|
||||
export MP_GIT_HOOKS_STYLELINT="${MP_GIT_HOOKS_STYLELINT:-true}"
|
||||
export MP_GIT_HOOKS_PHPLINT="${MP_GIT_HOOKS_PHPLINT:-true}"
|
||||
export MP_GIT_HOOKS_CODE_SNIFFER="${MP_GIT_HOOKS_CODE_SNIFFER:-true}"
|
||||
export MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS="${MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS:-true}"
|
||||
export MP_GIT_HOOKS_PHPSTAN="${MP_GIT_HOOKS_PHPSTAN:-true}"
|
||||
export MP_GIT_HOOKS_INSTALL_JS="${MP_GIT_HOOKS_INSTALL_JS:-false}"
|
||||
export MP_GIT_HOOKS_INSTALL_PHP="${MP_GIT_HOOKS_INSTALL_PHP:-false}"
|
||||
|
@ -1,8 +1,7 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
. "$(dirname "$0")/common.sh"
|
||||
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
|
||||
|
||||
npx lint-staged -c mailpoet/package.json --cwd mailpoet
|
||||
npx lint-staged -c package.json
|
||||
npx lint-staged -c packages/js/email-editor/package.json --cwd packages/js/email-editor
|
||||
npx lint-staged -c packages/php/email-editor/.lintstagedrc.json --cwd packages/php/email-editor
|
||||
|
@ -25,5 +25,3 @@ vendor-prefixed
|
||||
/mailpoet/views
|
||||
/mailpoet-premium
|
||||
/wordpress
|
||||
/packages/php/email-editor
|
||||
/packages/js/email-editor
|
||||
|
@ -103,8 +103,8 @@ Then create a Docker Compose override file with NFS settings and restart contain
|
||||
```shell
|
||||
cp docker-compose.override.macos-sample.yml docker-compose.override.yml
|
||||
|
||||
docker compose down -v --remove-orphans
|
||||
docker compose up -d
|
||||
docker-compose down -v --remove-orphans
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**NOTE:** If you are on MacOS Catalina or newer, make sure to put the repository
|
||||
@ -163,10 +163,10 @@ To switch the environment to a different PHP version:
|
||||
dockerfile: dev/{PHP_VERSION}/Dockerfile
|
||||
```
|
||||
|
||||
3. Run `docker compose build wordpress`.
|
||||
3. Run `docker-compose build wordpress`.
|
||||
4. Start the stack with `./do start`.
|
||||
|
||||
To switch back to the default PHP version remove what was added in 2) and, run `docker compose build wordpress` for application container and `docker compose build test_wordpress` for tests container,
|
||||
To switch back to the default PHP version remove what was added in 2) and, run `docker-compose build wordpress` for application container and `docker-compose build test_wordpress` for tests container,
|
||||
and start the stack using `./do start`.
|
||||
|
||||
### Disabling the Tracy panel
|
||||
|
@ -21,7 +21,7 @@ mkdir -p wordpress/wp-content/plugins/mailpoet-premium
|
||||
mkdir -p dev/data/mailhog
|
||||
|
||||
for plugin in "mailpoet" "mailpoet-premium"; do
|
||||
docker compose run --rm wordpress /bin/sh -c "
|
||||
docker-compose run --rm wordpress /bin/sh -c "
|
||||
[ -d /var/www/html/wp-content/plugins/$plugin ] &&
|
||||
cd /var/www/html/wp-content/plugins/$plugin &&
|
||||
./do install &&
|
||||
@ -29,7 +29,7 @@ for plugin in "mailpoet" "mailpoet-premium"; do
|
||||
"
|
||||
done
|
||||
|
||||
docker compose run --rm wordpress /bin/sh -c "
|
||||
docker-compose run --rm wordpress /bin/sh -c "
|
||||
cd /var/www/templates &&
|
||||
mkdir assets classes exported
|
||||
"
|
||||
|
@ -29,6 +29,6 @@ cat <<EOT
|
||||
NFS volume sharing is set up. Recreate your containers and volumes using:
|
||||
cp docker-compose.override.macos-sample.yml docker-compose.override.yml
|
||||
|
||||
docker compose down -v --remove-orphans
|
||||
docker compose up -d
|
||||
docker-compose down -v --remove-orphans
|
||||
docker-compose up -d
|
||||
EOT
|
||||
|
16
do
16
do
@ -3,8 +3,8 @@
|
||||
function syntax {
|
||||
cat << EOF
|
||||
./do setup Setup the dev environment.
|
||||
./do start Start the docker containers (docker compose up -d).
|
||||
./do stop Stop the docker containers (docker compose stop).
|
||||
./do start Start the docker containers (docker-compose up -d).
|
||||
./do stop Stop the docker containers (docker-compose stop).
|
||||
./do ssh [--test] Run an interactive bash shell inside the plugin directory.
|
||||
./do run [--test] <command> Run a custom bash command in the wordpress container.
|
||||
./do acceptance [--premium] Run acceptance tests.
|
||||
@ -21,7 +21,7 @@ EOF
|
||||
function ssh_and_run {
|
||||
params=("$@")
|
||||
params=("${params[@]:1}")
|
||||
docker compose exec $1 bash -c "${params[@]}"
|
||||
docker-compose exec $1 bash -c "${params[@]}"
|
||||
}
|
||||
|
||||
if [ "$1" = "" -o "$1" = "--help" ]; then
|
||||
@ -31,10 +31,10 @@ elif [ "$1" = "setup" ]; then
|
||||
./dev/initial-setup.sh
|
||||
|
||||
elif [ "$1" = "start" ]; then
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
|
||||
elif [ "$1" = "stop" ]; then
|
||||
docker compose stop
|
||||
docker-compose stop
|
||||
|
||||
elif [ "$1" = "run" ]; then
|
||||
params=("$@")
|
||||
@ -54,9 +54,9 @@ elif [ "$1" = "ssh" ]; then
|
||||
fi
|
||||
|
||||
if [ "$2" = "--test" ] || [ "$3" = "--test" ]; then
|
||||
docker compose exec --workdir $dir test_wordpress bash
|
||||
docker-compose exec --workdir $dir test_wordpress bash
|
||||
else
|
||||
docker compose exec --workdir $dir wordpress bash
|
||||
docker-compose exec --workdir $dir wordpress bash
|
||||
fi
|
||||
|
||||
elif [ "$1" = "acceptance" ]; then
|
||||
@ -65,7 +65,7 @@ elif [ "$1" = "acceptance" ]; then
|
||||
else
|
||||
cd mailpoet
|
||||
fi
|
||||
COMPOSE_HTTP_TIMEOUT=200 docker compose run codeception_acceptance -e KEEP_DEPS=1 --steps --debug -vvv
|
||||
COMPOSE_HTTP_TIMEOUT=200 docker-compose run codeception_acceptance -e KEEP_DEPS=1 --steps --debug -vvv
|
||||
cd ..
|
||||
|
||||
elif [ "$1" = "build" ]; then
|
||||
|
@ -1,3 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# for M1 Macs
|
||||
db:
|
||||
|
@ -25,7 +25,7 @@ services:
|
||||
container_name: mp-wp
|
||||
build:
|
||||
context: .
|
||||
dockerfile: dev/php82/Dockerfile
|
||||
dockerfile: dev/php81/Dockerfile
|
||||
args:
|
||||
UID: ${UID:-1000}
|
||||
GID: ${GID:-1000}
|
||||
@ -46,16 +46,13 @@ services:
|
||||
NPM_CONFIG_CACHE: '/tmp/.npm'
|
||||
XDG_CACHE_HOME: '/tmp/.cache'
|
||||
MAILPOET_DEV_SITE: 1
|
||||
MP_ENV: development
|
||||
volumes:
|
||||
- './wordpress:/var/www/html'
|
||||
- './tsconfig.base.json:/var/www/html/wp-content/plugins/tsconfig.base.json:ro'
|
||||
- './.npmrc:/var/www/html/wp-content/plugins/.npmrc'
|
||||
- './package.json:/var/www/html/wp-content/plugins/package.json'
|
||||
- './pnpm-lock.yaml:/var/www/html/wp-content/plugins/pnpm-lock.yaml'
|
||||
- './pnpm-workspace.yaml:/var/www/html/wp-content/plugins/pnpm-workspace.yaml'
|
||||
- './patches:/var/www/html/wp-content/plugins/patches'
|
||||
- './tests_env:/var/www/html/wp-content/plugins/tests_env'
|
||||
- './mailpoet:/var/www/html/wp-content/plugins/mailpoet'
|
||||
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
||||
- './packages:/var/www/html/wp-content/plugins/packages'
|
||||
@ -81,7 +78,6 @@ services:
|
||||
WORDPRESS_DB_USER: wordpress
|
||||
WORDPRESS_DB_PASSWORD: wordpress
|
||||
PHP_IDE_CONFIG: 'serverName=Mailpoet'
|
||||
MP_ENV: test
|
||||
volumes:
|
||||
- './mailpoet:/var/www/html/wp-content/plugins/mailpoet'
|
||||
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
||||
|
@ -51,6 +51,7 @@ MP_GIT_HOOKS_ESLINT=true
|
||||
MP_GIT_HOOKS_STYLELINT=true
|
||||
MP_GIT_HOOKS_PHPLINT=true
|
||||
MP_GIT_HOOKS_CODE_SNIFFER=true
|
||||
MP_GIT_HOOKS_MINIMAL_PLUGIN_STANDARDS=true
|
||||
MP_GIT_HOOKS_PHPSTAN=true
|
||||
MP_GIT_HOOKS_INSTALL_JS=false
|
||||
MP_GIT_HOOKS_INSTALL_PHP=false
|
||||
|
@ -1,116 +1,5 @@
|
||||
== Changelog ==
|
||||
|
||||
= 5.5.2 - 2024-12-24 =
|
||||
|
||||
- Improved: minor changes and fixes.
|
||||
|
||||
= 5.5.1 - 2024-12-17 =
|
||||
|
||||
- Improved: added option to protect WordPress and WooCommerce registration forms with a captcha.
|
||||
- Changed: removed 3rd party Beamer integration;
|
||||
- Fixed: error when trying to use mailpoet_settings database table before it is available.
|
||||
|
||||
= 5.5.0 - 2024-12-09 =
|
||||
|
||||
- Added the Polish translation as an officially maintained translation;
|
||||
- Added: classes now allowed in links of HTML forms.
|
||||
|
||||
= 5.4.2 - 2024-12-02 =
|
||||
|
||||
- Improved: stability of post notification scheduling;
|
||||
- Changed: minimum required WordPress version to 6.6 and WooCommerce to 9.3.
|
||||
|
||||
= 5.4.1 - 2024-11-25 =
|
||||
|
||||
- Improved: minor changes and fixes.
|
||||
|
||||
= 5.4.0 - 2024-11-18 =
|
||||
|
||||
- Improved: tested with WooCommerce 9.4;
|
||||
- Improved: tested with WordPress 6.7;
|
||||
- Improved: tested with PHP 8.2.
|
||||
|
||||
= 5.3.7 - 2024-11-12 =
|
||||
|
||||
- Fixed: detecting table index existence in DB migrations for MySQL 5;
|
||||
- Fixed: re-activation of welcome emails with long history may take long time or fail.
|
||||
|
||||
= 5.3.6 - 2024-11-05 =
|
||||
|
||||
- Fixed: race condition in links tracking;
|
||||
- Fixed: WP 6.7 compatibility issues.
|
||||
|
||||
= 5.3.5 - 2024-10-30 =
|
||||
|
||||
- Fixed: Post notifications now correctly select new posts by ensuring consistent UTC date comparison, preventing posts published within certain time zones from being skipped after a notification is sent.
|
||||
|
||||
= 5.3.4 - 2024-10-28 =
|
||||
|
||||
- Improved: mark required fields in public forms also in screen readers for improved accessibility;
|
||||
- Improved: properly connect inputs and labels in public forms for improved accessibility;
|
||||
- Changed: when pausing a post notification newsletter, scheduled task is now also paused. This prevents the sending queue from becoming stalled.
|
||||
|
||||
= 5.3.3 - 2024-10-22 =
|
||||
|
||||
- Fixed: error on email statistics page.
|
||||
|
||||
= 5.3.2 - 2024-10-22 =
|
||||
|
||||
- Improved: form error messages now use rem instead of px to improve accessibility;
|
||||
- Improved: toggle and yes/no controls are focusable;
|
||||
- Fixed: Preview of the Sign Up Confirmation email is not working.
|
||||
|
||||
= 5.3.1 - 2024-10-15 =
|
||||
|
||||
- Improved: add validation for re-engagement emails period;
|
||||
- Improved: more balanced text wrapping to improve readability;
|
||||
- Improved: when the email authentication service is unavailable, use the old authentication status and continue sending.
|
||||
- Fixed: some email template previews are too wide;
|
||||
- Fixed: remove Google+ icon from email templates;
|
||||
- Fixed: Removed a warning when caption is not present on image;
|
||||
- Fixed: deprecation warning from mb_convert_encoding.
|
||||
|
||||
= 5.3.0 - 2024-10-03 =
|
||||
|
||||
- Fixed: abandoned cart automation trigger doesn't work in some cases.
|
||||
|
||||
= 5.2.3 - 2024-10-01 =
|
||||
|
||||
- Fixed: Compatibility with WooCommerce 9.4;
|
||||
- Fixed: Percentage in Analytics did not work correctly in if/else condtion.
|
||||
|
||||
= 5.2.2 - 2024-09-23 =
|
||||
|
||||
- Improved: when using FSE theme, link to Site Editor instead of Widgets in MailPoet Forms placement options;
|
||||
- Changed: replace deprecated woocommerce_before_cart_item_quantity_zero action with woocommerce_remove_cart_item;
|
||||
- Fixed: List-Unsubscribe URL no longer redirects when not using MailPoet Sending Service.
|
||||
|
||||
= 5.2.1 - 2024-09-17 =
|
||||
|
||||
- Added: WooCommerce email template reset button;
|
||||
- Improved: plugin activation and update performance.
|
||||
|
||||
= 5.2.0 - 2024-09-11 =
|
||||
|
||||
- Fixed: incorrect value Last run started in Cron status at the help page;
|
||||
- Fixed: The help page loads slowly for sites with long sending history;
|
||||
- Fixed: emails with incorrectly nested ALC or WooCommerce placeholder blocks can't be sent.
|
||||
|
||||
= 5.1.1 - 2024-09-04 =
|
||||
|
||||
- Fixed: broken email rendering when ALC, Products, or WC content blocks are manually dragged into a column block (since 4.56.0). If you experience this issue, please readd these blocks or drag and drop the existing ones which should fix the issue.
|
||||
|
||||
= 5.1.0 - 2024-09-02 =
|
||||
|
||||
- Improved: form validation messages;
|
||||
- Fixed: using ${var} in strings is deprecated, use {$var} instead.
|
||||
|
||||
= 5.0.2 - 2024-08-26 =
|
||||
|
||||
- Added: Ukrainian translations;
|
||||
- Improved: error messages in automations;
|
||||
- Changed: human and machine opens are merged by default, old behavior can be restored in settings.
|
||||
|
||||
= 5.0.1 - 2024-08-23 =
|
||||
|
||||
- Fixed: incorrect date in the scheduling calendar on the send page.
|
||||
|
@ -208,7 +208,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
$env = ($opts['env']) ?
|
||||
sprintf('./node_modules/.bin/cross-env NODE_ENV="%s"', $opts['env']) :
|
||||
null;
|
||||
return $this->_exec($env . ' ./node_modules/webpack/bin/webpack.js --env BUILD_TESTS=' . ($opts['skip-tests'] ? 'skip' : 'build') . ' --env BUILD_ONLY_TESTS=' . ($opts['only-tests'] ? 'true' : 'false'));
|
||||
return $this->_exec($env . ' ./node_modules/webpack/bin/webpack.js --env BUILD_TESTS=' . ($opts['skip-tests'] ? 'skip' : 'build') . '--env BUILD_ONLY_TESTS=' . ($opts['only-tests'] ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
public function compileCss($opts = ['env' => null]) {
|
||||
@ -220,7 +220,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
|
||||
$compilationResult = $this->taskExecStack()
|
||||
->exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"')
|
||||
->exec('pnpm run scss' . ($opts['env'] === 'production' ? ' --no-source-map' : ''))
|
||||
->exec('pnpm run scss')
|
||||
->exec('pnpm run autoprefixer')
|
||||
->run();
|
||||
|
||||
@ -330,7 +330,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function testUnit(array $opts = ['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
|
||||
$command = '../tests_env/vendor/bin/codecept run unit';
|
||||
$command = 'vendor/bin/codecept run unit';
|
||||
|
||||
if ($opts['file']) {
|
||||
$command .= ' -f ' . $opts['file'];
|
||||
@ -404,7 +404,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->testIntegration($opts);
|
||||
}
|
||||
|
||||
public function testAcceptance($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, 'disable-hpos' => false, 'enable-hpos-sync' => false, 'enable-hpos' => false, 'wordpress-version' => null, 'skip-plugins' => false]) {
|
||||
public function testAcceptance($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, 'disable-hpos' => false, 'enable-hpos-sync' => false, 'enable-hpos' => false, 'wordpress-version' => null]) {
|
||||
return $this->runTestsInContainer($opts);
|
||||
}
|
||||
|
||||
@ -420,7 +420,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
->option('env', 'US=' . $opts['us'])
|
||||
->option('env', 'PW=' . $opts['pw'])
|
||||
->option('env', 'K6_BROWSER_HEADLESS=' . ($opts['head'] ? 'false' : 'true'))
|
||||
->option('env', 'K6_BROWSER_TIMEOUT=' . getenv('K6_BROWSER_TIMEOUT'))
|
||||
->option('env', 'K6_BROWSER_TIMEOUT=120s')
|
||||
->option('env', 'SCENARIO=' . $opts['scenario'])
|
||||
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
||||
->dir($dir)->run();
|
||||
@ -437,7 +437,6 @@ class RoboFile extends \Robo\Tasks {
|
||||
->option('env', 'SCENARIO=' . $opts['scenario'])
|
||||
->option('env', 'K6_CLOUD_TOKEN=' . getenv('K6_CLOUD_TOKEN'))
|
||||
->option('env', 'K6_CLOUD_ID=' . getenv('K6_CLOUD_ID'))
|
||||
->option('env', 'K6_BROWSER_TIMEOUT=' . getenv('K6_BROWSER_TIMEOUT'))
|
||||
->option('env', 'K6_PROJECT_NAME=' . $opts['scenario'])
|
||||
->option('out', 'cloud')
|
||||
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
||||
@ -474,14 +473,14 @@ class RoboFile extends \Robo\Tasks {
|
||||
|
||||
// import data & run WordPress setup
|
||||
$this->say('Importing data and running a WordPress setup...');
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker compose run --rm -it setup')
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker-compose run --rm -it setup')
|
||||
->dir(__DIR__ . '/tests/performance')
|
||||
->run();
|
||||
$this->say('Data imported, WordPress set up.');
|
||||
}
|
||||
|
||||
public function testPerformanceClean() {
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker compose down --remove-orphans -v')
|
||||
$this->taskExec('COMPOSE_HTTP_TIMEOUT=200 docker-compose down --remove-orphans -v')
|
||||
->dir(__DIR__ . '/tests/performance')
|
||||
->run();
|
||||
}
|
||||
@ -495,7 +494,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
*/
|
||||
public function deleteDocker() {
|
||||
return $this->taskExec(
|
||||
'docker compose down -v --remove-orphans --rmi all'
|
||||
'docker-compose down -v --remove-orphans --rmi all'
|
||||
)->dir(__DIR__ . '/../tests_env/docker')->run();
|
||||
}
|
||||
|
||||
@ -505,20 +504,20 @@ class RoboFile extends \Robo\Tasks {
|
||||
public function resetTestDocker() {
|
||||
return $this
|
||||
->taskExec(
|
||||
'docker compose down -v --remove-orphans'
|
||||
'docker-compose down -v --remove-orphans'
|
||||
)->dir(__DIR__ . '/../tests_env/docker')
|
||||
->addCode([$this, 'cleanupCachedFiles'])
|
||||
->run();
|
||||
}
|
||||
|
||||
public function testFailedUnit() {
|
||||
$this->_exec('../tests_env/vendor/bin/codecept build');
|
||||
return $this->_exec('../tests_env/vendor/bin/codecept run unit -g failed');
|
||||
$this->_exec('vendor/bin/codecept build');
|
||||
return $this->_exec('vendor/bin/codecept run unit -g failed');
|
||||
}
|
||||
|
||||
public function testFailedIntegration() {
|
||||
$this->_exec('../tests_env/vendor/bin/codecept build');
|
||||
return $this->_exec('../tests_env/vendor/bin/codecept run integration -g failed');
|
||||
$this->_exec('vendor/bin/codecept build');
|
||||
return $this->_exec('vendor/bin/codecept run integration -g failed');
|
||||
}
|
||||
|
||||
public function containerDump() {
|
||||
@ -622,6 +621,9 @@ class RoboFile extends \Robo\Tasks {
|
||||
$collection->addCode(function() {
|
||||
return $this->qaCodeSniffer([]);
|
||||
});
|
||||
$collection->addCode(function() {
|
||||
return $this->qaMinimalPluginStandard([]);
|
||||
});
|
||||
return $collection->run();
|
||||
}
|
||||
|
||||
@ -648,7 +650,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
'lib/',
|
||||
'lib-3rd-party/',
|
||||
'vendor/composer',
|
||||
'vendor/dragonmantank',
|
||||
'vendor/mtdowling',
|
||||
'vendor-prefixed/',
|
||||
'vendor-prefixed/soundasleep',
|
||||
'mailpoet.php',
|
||||
@ -664,7 +666,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
'vendor-prefixed/cerdic/css-tidy/COPYING',
|
||||
'vendor-prefixed/cerdic/css-tidy/NEWS',
|
||||
'vendor-prefixed/cerdic/css-tidy/testing',
|
||||
'vendor/dragonmantank/cron-expression/tests',
|
||||
'vendor/mtdowling/cron-expression/tests',
|
||||
'vendor/phpmailer/phpmailer/test',
|
||||
'vendor-prefixed/psr/log/Psr/Log/Test',
|
||||
'vendor-prefixed/sabberworm/php-css-parser/tests',
|
||||
@ -684,21 +686,11 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function qaLintJavascript() {
|
||||
$collection = $this->collectionBuilder();
|
||||
return $collection->taskExecStack()
|
||||
->stopOnFail()
|
||||
->exec('pnpm run check-types && pnpm run lint')
|
||||
->exec('cd .. && cd packages/js/email-editor && pnpm run check-types && pnpm run lint:js')
|
||||
->run();
|
||||
return $this->_exec('pnpm run check-types && pnpm run lint');
|
||||
}
|
||||
|
||||
public function qaLintCss() {
|
||||
$collection = $this->collectionBuilder();
|
||||
return $collection->taskExecStack()
|
||||
->stopOnFail()
|
||||
->exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"')
|
||||
->exec('cd .. && cd packages/js/email-editor && pnpm run lint:css')
|
||||
->run();
|
||||
return $this->_exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"');
|
||||
}
|
||||
|
||||
public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) {
|
||||
@ -727,7 +719,6 @@ class RoboFile extends \Robo\Tasks {
|
||||
'tasks/code_sniffer/vendor',
|
||||
'tasks/phpstan/vendor',
|
||||
'tasks/makepot',
|
||||
'tasks/minimal-plugin-standard/vendor',
|
||||
'tools/vendor',
|
||||
'tools/wpscan-semgrep-rules',
|
||||
'temp',
|
||||
@ -752,6 +743,58 @@ class RoboFile extends \Robo\Tasks {
|
||||
->run();
|
||||
}
|
||||
|
||||
public function qaMinimalPluginStandard(array $filesToCheck, $opts = ['severity' => 'all']) {
|
||||
$severityFlag = $opts['severity'] === 'all' ? '-w' : '-n';
|
||||
|
||||
$task = implode(' ', [
|
||||
'php -d memory_limit=-1',
|
||||
'./tasks/code_sniffer/vendor/bin/phpcs',
|
||||
'--parallel=' . $this->getParallelism(),
|
||||
'--extensions=php',
|
||||
$severityFlag,
|
||||
'--standard=tasks/code_sniffer/vendor/wporg/plugin-directory/MinimalPluginStandard',
|
||||
'-s',
|
||||
]);
|
||||
|
||||
$ignorePaths = [
|
||||
'.mp_svn',
|
||||
'assets',
|
||||
'doc',
|
||||
'generated',
|
||||
'lib/Config/PopulatorData/Templates',
|
||||
'lib-3rd-party',
|
||||
'node_modules',
|
||||
'plugin_repository',
|
||||
'prefixer/build',
|
||||
'prefixer/vendor',
|
||||
'tasks/code_sniffer/vendor',
|
||||
'tasks/phpstan/vendor',
|
||||
'tasks/makepot',
|
||||
'tools/vendor',
|
||||
'tools/wpscan-semgrep-rules',
|
||||
'temp',
|
||||
'tests/_data',
|
||||
'tests/_output',
|
||||
'tests/_support/_generated',
|
||||
'vendor',
|
||||
'vendor-prefixed',
|
||||
'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) : '.';
|
||||
|
||||
return $this
|
||||
->taskExec($task)
|
||||
->arg('--ignore=' . implode(',', $ignorePatterns))
|
||||
->rawArg($stringFilesToCheck)
|
||||
->run();
|
||||
}
|
||||
|
||||
public function qaFixFile($filePath) {
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'php') {
|
||||
@ -759,7 +802,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->collectionBuilder()
|
||||
->taskExec(
|
||||
'./tasks/code_sniffer/vendor/bin/phpcbf ' .
|
||||
'--standard=tasks/code_sniffer/MailPoet/free-ruleset.xml ' .
|
||||
'--standard=./tasks/code_sniffer/MailPoet ' .
|
||||
'--runtime-set testVersion 7.4-8.2 ' .
|
||||
$filePath . ' -n'
|
||||
)
|
||||
@ -785,7 +828,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
// make sure Codeception support files are present to avoid invalid errors when running PHPStan
|
||||
$this->_exec('../tests_env/vendor/bin/codecept build');
|
||||
$this->_exec('vendor/bin/codecept build');
|
||||
|
||||
// PHPStan must be run out of main plugin directory to avoid its autoloading
|
||||
// from vendor/autoload.php where some dev dependencies cause conflicts.
|
||||
@ -809,10 +852,6 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->_exec('./tools/semgrep.sh lib/ lib-3rd-party/');
|
||||
}
|
||||
|
||||
public function qaQitSecurity() {
|
||||
return $this->_exec('./vendor/bin/qit run:security mailpoet --zip=mailpoet.zip --wait');
|
||||
}
|
||||
|
||||
public function svnCheckout() {
|
||||
$svnDir = ".mp_svn";
|
||||
|
||||
@ -1609,7 +1648,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
$annotationReaderProvider = new \MailPoet\Doctrine\Annotations\AnnotationReaderProvider();
|
||||
$configuration = (new \MailPoet\Doctrine\ConfigurationFactory($annotationReaderProvider, true))->createConfiguration();
|
||||
$platformClass = \MailPoetVendor\Doctrine\DBAL\Platforms\MySQLPlatform::class;
|
||||
$platformClass = \MailPoet\Doctrine\ConnectionFactory::PLATFORM_CLASS;
|
||||
return \MailPoetVendor\Doctrine\ORM\EntityManager::create([
|
||||
'driverClass' => \MailPoet\Doctrine\ConnectionFactory::DRIVER_CLASS,
|
||||
'platform' => new $platformClass,
|
||||
@ -1620,7 +1659,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
$testType = $opts['test_type'] ?? 'acceptance';
|
||||
$this->doctrineGenerateCache();
|
||||
return $this->taskExec(
|
||||
'COMPOSE_HTTP_TIMEOUT=200 docker compose run ' .
|
||||
'COMPOSE_HTTP_TIMEOUT=200 docker-compose run ' .
|
||||
(isset($opts['wordpress-version']) && $opts['wordpress-version'] ? '-e WORDPRESS_VERSION=' . $opts['wordpress-version'] . ' ' : '') .
|
||||
(isset($opts['skip-deps']) && $opts['skip-deps'] ? '-e SKIP_DEPS=1 ' : '') .
|
||||
(isset($opts['disable-hpos']) && $opts['disable-hpos'] ? '-e DISABLE_HPOS=1 ' : '') .
|
||||
|
@ -1,16 +1,11 @@
|
||||
.mailpoet_history_wrapper {
|
||||
display: grid;
|
||||
grid-gap: $grid-gap;
|
||||
grid-template-columns: 26px 26px auto;
|
||||
display: flex;
|
||||
padding: 12px 20px;
|
||||
|
||||
.mailpoet_reset_template {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_history_arrow {
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
|
@ -0,0 +1,61 @@
|
||||
.mailpoet_feature_announcement {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.button.mailpoet_feature_announcement_button {
|
||||
height: 28px;
|
||||
min-height: auto;
|
||||
padding: 0 5px 1px;
|
||||
position: relative;
|
||||
|
||||
@include respond-to(small-screen) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_feature_announcement_icon {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.mailpoet_feature_announcement_dot:before {
|
||||
background: #d54e21;
|
||||
border-radius: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.mailpoet_in_beamer_update_notice {
|
||||
background: #f00;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
padding: 20px 10px;
|
||||
position: fixed;
|
||||
right: -400px;
|
||||
text-align: center;
|
||||
transition: right 0.2s ease-in;
|
||||
width: 400px;
|
||||
z-index: 10000000000; // really has to be this high
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.beamer_show & {
|
||||
right: 0;
|
||||
}
|
||||
}
|
@ -95,6 +95,10 @@ h1.title.mailpoet-newsletter-listing-heading {
|
||||
#mailpoet_editor_steps_heading {
|
||||
.mailpoet-top-bar {
|
||||
left: 0;
|
||||
|
||||
.mailpoet-top-bar-beamer {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,11 +30,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-template-preview-image {
|
||||
max-width: 100%;
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.mailpoet-template-info {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -90,19 +90,14 @@
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.mailpoet-re-engagement-scheduling {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr 2fr 3fr;
|
||||
|
||||
.mailpoet-form-input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-re-engagement-scheduling-note {
|
||||
color: $color-input-error;
|
||||
}
|
||||
|
||||
.mailpoet-re-engagement-scheduling .mailpoet-form-input-small {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
[data-type='re_engagement'] .mailpoet-newsletter-type-image {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 178.78 177.29'%3E%3Cpath fill='%23fb8959' d='M147 35.1a.15.15 0 00-.05-.28l-2.28-.5a4.12 4.12 0 00-.68-1 4.34 4.34 0 00-.72-.62c-.1-.08-.2-.14-.29-.2l-.11-.07a4.3 4.3 0 00-.57-.28 3.82 3.82 0 00-2-.16c1.76-6.91-1.65-14.3-1.65-14.3L130 33l-14.63-11.19a21 21 0 00-.57 7v.38a14.93 14.93 0 003.95 8.81s.22.22.59.56a16 16 0 001.58 1.28 13.83 13.83 0 002.56 1.55c-1.16.86-2 1.55-2.51 1.92l-1.56 1.17-3 2.3-4.37 3.3s2.22.4 3.28-.51c0 0-1.36 1.45-1 2.86l4.17-3.37 4.55-3.67a20.58 20.58 0 002.85 1.67q.39.2.78.36h.1c.51.21 1 .39 1.49.54h.09a13.41 13.41 0 001.43.34h.08a9.48 9.48 0 001.36.16h.08a10.9 10.9 0 001.28 0h.08a9.77 9.77 0 001.21-.16h.07c.39-.08.77-.17 1.14-.28h.07a11.07 11.07 0 001.06-.4h.07q.53-.22 1-.48h.07l.47-.27.43-.28h.07l.43-.3.39-.3.06-.05c.14-.1.27-.21.4-.32l.35-.32.37-.34c.12-.1.22-.21.33-.32l.33-.35.29-.33.3-.35.43-.61.27-.34c.57-.74 1-1.4 1.34-1.9a8.91 8.91 0 001.39-3.58 2.47 2.47 0 00.07-.61 1.09 1.09 0 000-.18z'/%3E%3Cpath fill='%23a73e27' d='M76.9 67.52l23.39-41.74s22.9 49.52-21.93 65.53z'/%3E%3Cpath fill='%23fb8959' d='M86.34 74.31l-49-37.48S25.82 76.07 61.74 90.9z'/%3E%3Cpath fill='%23f86937' d='M84 73.17s-21.38 11.94-37.62 7.58c0 0 7.81 7.81 15.36 10.15z'/%3E%3Cpath fill='%23fb8959' d='M51.39 95.92s45.69-36.64 58.83-31c9.41 4 9.37 14.38 3.64 23.23s-26.35 38.28-62.47 7.77z'/%3E%3Cpath fill='%23fb8959' d='M55.52 92.76L28 113.55s6 1.09 8.88-1.4c0 0-3.68 3.95-2.66 7.77l26.83-21.69z'/%3E%3Cpath fill='%23a73e27' d='M115.62 70.48l7.19 1.6a.42.42 0 01.15.76l-5.57 3.78a5.27 5.27 0 00-1.77-6.14z'/%3E%3Cpath fill='%237e9ffc' d='M117.7 78.41s-10.67 10-18.61 5.21c-3.67-2.23-4.47-10.94 2.09-10.81 8.17.17 9-7.87 9-7.87s9.22 3.45 7.52 13.47z'/%3E%3Ccircle fill='%230a0851' cx='111.39' cy='72.88' r='1.46'/%3E%3Cpath fill='%23f86937' d='M57.78 100.81s-6-4.2-6.39-4.89l-3.32 2.47z'/%3E%3Cpath fill='%23f5a278' opacity='.55' d='M114.54 134.69l-2.43-1.4a1.73 1.73 0 000-.68 3 3 0 00-1.61-1.67 13.58 13.58 0 00-4.09-1.21 26.84 26.84 0 00-4.4-.23c4-3.54 7.92-7.83 0-10.86-5.93 2.78-13.18 5.3-16.88 8.58l-3.35 2.26-1.34.91-30.95-9s-8.56 11 17.13 15.62c-2.5.7-4.4 1.26-5.4 1.55l-19 5.43s4.69.32 6.92-.41c0 0-2.87 1.17-2.07 2.29l18.38-5.62c25 6.5 39.47-1.28 43.66-3.73a10.79 10.79 0 001.78-1.28z'/%3E%3Cpath fill='%23a73e27' d='M35.76 55.86s6.46 6 14.46 5.21c0 0-7.75 1.91-14.17-3.09zM37.16 63.77s7.24 5.67 15.37 3.14a17 17 0 01-14.68-1z'/%3E%3Cpath fill='%23f86937' d='M94.88 95.61c-4.4 8.61-22.37 9.68-29.33 9.77 24.07 11.66 39.33-4.57 46-13.85-1.29-4.35-11.76-5.53-16.67 4.08z'/%3E%3Cpath fill='%23a73e27' d='M45.51 110.79l-11.29 9.13c-1-3.82 2.66-7.77 2.66-7.77-2.86 2.49-8.88 1.4-8.88 1.4l11.83-8.94s-1.08 5.94 5.68 6.18z'/%3E%3Cpath fill='%230a0851' d='M117.89 76.75S107 87 99.09 82.14a6.8 6.8 0 01-2.76-5.07c-.26 2.51.86 5.39 2.76 6.55 7.94 4.81 18.61-5.21 18.61-5.21a7.87 7.87 0 00.19-1.66z'/%3E%3Cpath fill='%23fb8959' d='M73 34.31c.25-.08.49-.17.73-.27.24-.11.47-.22.69-.34l.33-.19.3-.19.3-.21.27-.21.28-.22.24-.22.2-.19c5.15 3.35 12.35.38 12.35.38l-9.08-5.59v-.15a2.19 2.19 0 000-.43v-.13l1.32-.9a.11.11 0 000-.19l-1.6-.36A3.14 3.14 0 0078 23.28c-1.3-.56-3.84.5-6.48 2a2.46 2.46 0 00-3.08.88l-1.29 1.94C64.76 29.76 63 31.2 63 31.2h.05l-6.55 1.16a3.29 3.29 0 002.19.73s-1.3.46-1.52 1.45l6.59-1.37-2.65 4s6.66 2.16 11.15-2.66l.68-.17zm6.92-8.44a3.82 3.82 0 00-.12-.66 1.43 1.43 0 01.12.66z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
@ -559,3 +559,38 @@ h2.mailpoet-heading {
|
||||
font-size: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
// screen-reader-text CSS class only exists within the WordPress environment
|
||||
// the class does not exist when using iFrame forms due to these being used outside WordPress
|
||||
// prefixing with mailpoet-* to not interfere with the default WordPress screen-reader-text class
|
||||
.mailpoet-screen-reader-text {
|
||||
border: 0;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
-webkit-clip-path: inset(50%);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
word-wrap: normal !important;
|
||||
}
|
||||
|
||||
.mailpoet-screen-reader-text:focus {
|
||||
background-color: #ddd;
|
||||
clip: auto !important;
|
||||
-webkit-clip-path: none;
|
||||
clip-path: none;
|
||||
color: #444;
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
padding: 15px 23px 14px;
|
||||
right: 5px;
|
||||
text-decoration: none;
|
||||
top: 5px;
|
||||
width: auto;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
@ -10,15 +10,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WP registration form
|
||||
form#registerform .g-recaptcha:not([data-size='invisible']) {
|
||||
scale: 0.9;
|
||||
-webkit-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
// WC registration form
|
||||
form.woocommerce-form-register .g-recaptcha {
|
||||
padding-inline-start: 3px;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ textarea.parsley-error {
|
||||
|
||||
.parsley-errors-list {
|
||||
color: #900;
|
||||
font-size: 0.8rem;
|
||||
font-size: 13px;
|
||||
line-height: 1em;
|
||||
list-style-type: none;
|
||||
margin: 8px 0 3px;
|
||||
|
@ -1,3 +1,5 @@
|
||||
$beamer-dot-size: 8px;
|
||||
|
||||
.mailpoet-top-bar {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
@ -81,7 +83,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-top-bar-tutorial {
|
||||
.mailpoet-top-bar-beamer {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
border: none;
|
||||
@ -94,7 +96,6 @@
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
top: 4px;
|
||||
width: 75px;
|
||||
|
||||
svg {
|
||||
@ -102,3 +103,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-top-bar-beamer-dot:before {
|
||||
background: $color-editor-warning;
|
||||
border-radius: $beamer-dot-size;
|
||||
content: '';
|
||||
display: block;
|
||||
height: $beamer-dot-size;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
width: $beamer-dot-size;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
&:focus {
|
||||
~ .mailpoet-form-checkbox-control {
|
||||
border-color: $color-input-border-focus;
|
||||
border: 2px solid $color-input-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
&:focus {
|
||||
~ .mailpoet-form-radio-control {
|
||||
border-color: $color-input-border-focus;
|
||||
border: 2px solid $color-input-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,15 +9,9 @@
|
||||
|
||||
input {
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
width: 1px;
|
||||
|
||||
&:focus {
|
||||
~ .mailpoet-form-toggle-control {
|
||||
border-color: $color-input-border-focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,16 +22,9 @@
|
||||
|
||||
input {
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
width: 1px;
|
||||
|
||||
&:focus {
|
||||
~ .mailpoet-form-yesno-control {
|
||||
box-shadow: 0 0 0 1px $color-input-border-focus;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,3 +110,10 @@ span.mailpoet-gap-half {
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.mailpoet-is-dragging {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -4,36 +4,6 @@
|
||||
line-height: $line-height;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
caption,
|
||||
figcaption {
|
||||
// https://developer.chrome.com/docs/css-ui/css-text-wrap-balance
|
||||
text-wrap: balance;
|
||||
|
||||
// Skip in email editor to preserve WYSIWYG functionality
|
||||
.mailpoet_newsletter_wrapper & {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
// https://developer.chrome.com/blog/css-text-wrap-pretty/
|
||||
text-wrap: pretty;
|
||||
|
||||
// Skip in email editor to preserve WYSIWYG functionality
|
||||
.mailpoet_newsletter_wrapper & {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.admin_page_mailpoet-form-editor #wpbody {
|
||||
color: inherit;
|
||||
}
|
||||
|
@ -87,6 +87,7 @@
|
||||
@import 'components-plugin/newsletter-types';
|
||||
@import 'components-plugin/newsletter-template-styles';
|
||||
@import 'components-plugin/welcome-wizard';
|
||||
@import 'components-plugin/feature-announcement';
|
||||
@import 'components-plugin/newsletter-congratulate';
|
||||
@import 'components-plugin/discounts';
|
||||
@import 'components-plugin/review-request';
|
||||
|
@ -31,7 +31,6 @@ $color-tertiary-hover: darken($color-tertiary, 10%);
|
||||
$color-tertiary-light: #dcdcde;
|
||||
$color-grey-0: #f6f7f7;
|
||||
$color-tertiary-light-hover: darken($color-tertiary-light, 10%);
|
||||
$color-tertiary-light-focus: #777;
|
||||
$color-tertiary-light-background: rgba($color-tertiary-light, 0.3);
|
||||
$color-destructive: #b52727;
|
||||
$color-destructive-hover: #a02222;
|
||||
@ -47,7 +46,6 @@ $color-text-dark: #2c3338;
|
||||
// Form colors
|
||||
$color-input-background: #fdfdff;
|
||||
$color-input-border: $color-tertiary-light-hover;
|
||||
$color-input-border-focus: $color-tertiary-light-focus;
|
||||
$color-input-error: #f00;
|
||||
$color-input-success: #7ed321;
|
||||
|
||||
|
@ -104,30 +104,11 @@ function cacheEvent(forced, name, data, options, callback) {
|
||||
}
|
||||
|
||||
export function initializeMixpanelWhenLoaded() {
|
||||
const MAX_RETRY = 5;
|
||||
let intervalId;
|
||||
let retryCount = 0;
|
||||
|
||||
const setupMixpanel = () => {
|
||||
if (typeof window.mixpanel === 'object') {
|
||||
exportMixpanel();
|
||||
trackCachedEvents();
|
||||
};
|
||||
|
||||
if (typeof window.mixpanel === 'object') {
|
||||
setupMixpanel();
|
||||
} else {
|
||||
intervalId = setInterval(() => {
|
||||
if (typeof window.mixpanel === 'object') {
|
||||
clearInterval(intervalId);
|
||||
setupMixpanel();
|
||||
} else {
|
||||
retryCount += 1;
|
||||
}
|
||||
|
||||
if (retryCount > MAX_RETRY) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 100);
|
||||
setTimeout(initializeMixpanelWhenLoaded, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
import classnames from 'classnames';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { withFeatureAnnouncement } from './with-feature-announcement';
|
||||
|
||||
type Props = {
|
||||
hasNews: boolean;
|
||||
onBeamerClick: () => void;
|
||||
};
|
||||
|
||||
function FeatureAnnouncementComponent({ hasNews, onBeamerClick }: Props) {
|
||||
const buttonClasses = classnames(
|
||||
'button mailpoet_feature_announcement_button',
|
||||
hasNews ? 'mailpoet_feature_announcement_dot' : '',
|
||||
);
|
||||
return (
|
||||
<div className="mailpoet_feature_announcement">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBeamerClick}
|
||||
className={buttonClasses}
|
||||
title={MailPoet.I18n.t('whatsNew')}
|
||||
>
|
||||
<span className="mailpoet_feature_announcement_icon dashicons dashicons-carrot" />
|
||||
</button>
|
||||
<span id="beamer-empty-element" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FeatureAnnouncement = withFeatureAnnouncement(
|
||||
FeatureAnnouncementComponent,
|
||||
);
|
||||
export { FeatureAnnouncement };
|
@ -0,0 +1,111 @@
|
||||
import { ComponentType, FC } from 'react';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import jQuery from 'jquery';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
interface FeatureAnnouncementWindow extends Window {
|
||||
Beamer: {
|
||||
show: () => void;
|
||||
};
|
||||
mailpoet_feature_announcement_has_news: boolean;
|
||||
mailpoet_update_available: boolean;
|
||||
beamer_config: {
|
||||
product_id: string;
|
||||
selector: string;
|
||||
language: string;
|
||||
callback: () => void;
|
||||
filter?: string;
|
||||
};
|
||||
mailpoet_user_locale: string;
|
||||
}
|
||||
|
||||
declare let window: FeatureAnnouncementWindow;
|
||||
|
||||
export const withFeatureAnnouncement = <P extends Record<string, unknown>>(
|
||||
Component: ComponentType<P>,
|
||||
): FC<Omit<P, 'hasNews' | 'onBeamerClick'>> => {
|
||||
const isBeamerInitialized = () => typeof window.Beamer !== 'undefined';
|
||||
let showDot = window.mailpoet_feature_announcement_has_news;
|
||||
let beamerCallback;
|
||||
|
||||
function showPluginUpdateNotice() {
|
||||
if (
|
||||
!window.mailpoet_update_available ||
|
||||
document.getElementById('mailpoet_update_notice')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const updateMailPoetNotice = ReactStringReplace(
|
||||
MailPoet.I18n.t('updateMailPoetNotice'),
|
||||
/\[link\](.*?)\[\/link\]/,
|
||||
(match) => `<a href="update-core.php">${match}</a>`,
|
||||
).join('');
|
||||
jQuery('#beamerOverlay').append(
|
||||
`<p id="mailpoet_update_notice" class="mailpoet_in_beamer_update_notice">${updateMailPoetNotice}</p>`,
|
||||
);
|
||||
}
|
||||
|
||||
function updateLastAnnouncementSeenValue() {
|
||||
const data = { last_announcement_seen: Math.floor(Date.now() / 1000) };
|
||||
void MailPoet.Ajax.post({
|
||||
api_version: MailPoet.apiVersion,
|
||||
endpoint: 'user_flags',
|
||||
action: 'set',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
function loadBeamer() {
|
||||
window.beamer_config = {
|
||||
product_id: 'VvHbhYWy7118',
|
||||
selector: '#beamer-empty-element',
|
||||
language: window.mailpoet_user_locale,
|
||||
callback: beamerCallback,
|
||||
};
|
||||
if (MailPoet.isWoocommerceActive) {
|
||||
window.beamer_config.filter = 'woocommerce';
|
||||
}
|
||||
MailPoet.Modal.loading(true);
|
||||
window.mailpoet_feature_announcement_has_news = false;
|
||||
const s = document.createElement('script');
|
||||
s.type = 'text/javascript';
|
||||
s.src = 'https://app.getbeamer.com/js/beamer-embed.js';
|
||||
document.getElementsByTagName('body')[0].appendChild(s);
|
||||
}
|
||||
|
||||
function showBeamer(event = null) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (!isBeamerInitialized()) {
|
||||
loadBeamer();
|
||||
return;
|
||||
}
|
||||
showDot = false;
|
||||
beamerCallback = noop; // We show Beamer panel only on first callback after initialization
|
||||
MailPoet.Modal.loading(false);
|
||||
window.Beamer.show();
|
||||
updateLastAnnouncementSeenValue();
|
||||
showPluginUpdateNotice();
|
||||
}
|
||||
|
||||
beamerCallback = () => {
|
||||
if (!isBeamerInitialized()) {
|
||||
return;
|
||||
}
|
||||
showBeamer();
|
||||
};
|
||||
|
||||
return function withFeatureAnnouncementRenderer({
|
||||
...props
|
||||
}: Omit<P, 'hasNews' | 'onBeamerClick'>) {
|
||||
return (
|
||||
<Component
|
||||
{...(props as P)}
|
||||
onBeamerClick={(e) => showBeamer(e)}
|
||||
hasNews={showDot}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { TopBarWithBoundary } from 'common/top-bar/top-bar';
|
||||
import { TopBarWithBeamer } from 'common/top-bar/top-bar';
|
||||
import { SlotFillProvider } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { registerTranslations } from 'common';
|
||||
@ -72,7 +72,7 @@ function Automations(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarWithBoundary />
|
||||
<TopBarWithBeamer />
|
||||
<GlobalNotices />
|
||||
<Notices />
|
||||
<MssAccessNotices />
|
||||
|
@ -3,23 +3,14 @@ import { FlowSeparator } from './flow-separator';
|
||||
import { Step as StepData } from './types';
|
||||
|
||||
type Props = {
|
||||
previousStepData: StepData;
|
||||
stepData: StepData;
|
||||
index: number;
|
||||
nextStepData?: StepData;
|
||||
};
|
||||
|
||||
export function FlowEnding({
|
||||
previousStepData,
|
||||
index,
|
||||
nextStepData,
|
||||
}: Props): JSX.Element {
|
||||
export function FlowEnding({ stepData, index }: Props): JSX.Element {
|
||||
return (
|
||||
<div className="mailpoet-automation-editor-step-wrapper">
|
||||
<FlowSeparator
|
||||
previousStepData={previousStepData}
|
||||
nextStepData={nextStepData}
|
||||
index={index}
|
||||
/>
|
||||
<FlowSeparator stepData={stepData} index={index} />
|
||||
<Icon
|
||||
className="mailpoet-automation-editor-automation-end"
|
||||
icon={check}
|
||||
|
@ -6,9 +6,8 @@ import { Step as StepData } from './types';
|
||||
import { RenderStepSeparatorType } from '../../../types/filters';
|
||||
|
||||
type Props = {
|
||||
previousStepData: StepData;
|
||||
stepData: StepData;
|
||||
index: number;
|
||||
nextStepData?: StepData;
|
||||
};
|
||||
|
||||
export function FlowSeparator(props: Props): JSX.Element {
|
||||
@ -36,9 +35,5 @@ export function FlowSeparator(props: Props): JSX.Element {
|
||||
),
|
||||
[context],
|
||||
);
|
||||
return renderSeparator(
|
||||
props.previousStepData,
|
||||
props.index,
|
||||
props.nextStepData,
|
||||
);
|
||||
return renderSeparator(props.stepData, props.index);
|
||||
}
|
||||
|
@ -40,24 +40,13 @@ export function Flow({ stepData, row }: Props): JSX.Element {
|
||||
|
||||
return nextStepData ? (
|
||||
<div key={id}>
|
||||
{row > 0 && (
|
||||
<FlowSeparator
|
||||
previousStepData={stepData}
|
||||
index={i}
|
||||
nextStepData={nextStepData}
|
||||
/>
|
||||
)}
|
||||
{row > 0 && <FlowSeparator stepData={stepData} index={i} />}
|
||||
<FlowStep stepData={nextStepData} index={i} />
|
||||
<Flow stepData={nextStepData} row={row + 1} />
|
||||
</div>
|
||||
) : (
|
||||
<FlowEnding
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
previousStepData={stepData}
|
||||
index={i}
|
||||
nextStepData={nextStepData}
|
||||
/>
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<FlowEnding key={i} stepData={stepData} index={i} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -50,7 +50,6 @@ export function initHooks() {
|
||||
return function StatisticSeparatorWrapper(
|
||||
previousStepData: StepData,
|
||||
index: number,
|
||||
nextStepData: StepData,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
@ -63,11 +62,7 @@ export function initHooks() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StatisticSeparator
|
||||
previousStep={previousStepData}
|
||||
nextStep={nextStepData}
|
||||
index={index}
|
||||
/>
|
||||
<StatisticSeparator previousStep={previousStepData} index={index} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -8,13 +8,11 @@ import { Step } from '../../../../../../editor/components/automation/types';
|
||||
type Props = {
|
||||
previousStep: Step;
|
||||
index: number;
|
||||
nextStep?: Step;
|
||||
};
|
||||
|
||||
export function StatisticSeparator({
|
||||
previousStep,
|
||||
index,
|
||||
nextStep,
|
||||
}: Props): JSX.Element | null {
|
||||
const { section, stepType } = useSelect(
|
||||
(s) => ({
|
||||
@ -54,34 +52,15 @@ export function StatisticSeparator({
|
||||
);
|
||||
}
|
||||
|
||||
const completed = data.step_data?.completed || {};
|
||||
const failed = data.step_data?.failed || {};
|
||||
const waiting = data.step_data?.waiting || {};
|
||||
const calculateTotals = (id) =>
|
||||
(completed[id] ?? 0) + (failed[id] ?? 0) + (waiting[id] ?? 0);
|
||||
let totalEntered = 0;
|
||||
if (nextStep) {
|
||||
totalEntered = calculateTotals(nextStep.id);
|
||||
} else if (previousStep.next_steps.length === 2) {
|
||||
// When there is no next step and the previous step has 2+ next steps we are
|
||||
// in an empty if/else branch. To calculate the total we need to subtract
|
||||
// totalEntered of the sibling step from totalEntered of previousStep
|
||||
const siblingStep = previousStep.next_steps.find((step) => step.id);
|
||||
const totalEnteredSibling = siblingStep
|
||||
? calculateTotals(siblingStep.id)
|
||||
: 0;
|
||||
const totalEnteredPrevious = completed[previousStep.id] ?? 0;
|
||||
totalEntered = totalEnteredPrevious - totalEnteredSibling;
|
||||
} else {
|
||||
totalEntered = completed[previousStep.id] ?? 0;
|
||||
}
|
||||
const flow = data.step_data?.flow;
|
||||
const value = flow !== undefined ? flow[previousStep.id] ?? 0 : 0;
|
||||
const percent =
|
||||
data.step_data.total > 0
|
||||
? Math.round((totalEntered / data.step_data.total) * 100)
|
||||
? Math.round((value / data.step_data.total) * 100)
|
||||
: 0;
|
||||
const formattedValue = Intl.NumberFormat(locale.toString(), {
|
||||
notation: 'compact',
|
||||
}).format(totalEntered);
|
||||
}).format(value);
|
||||
const formattedPercent = Intl.NumberFormat(locale.toString(), {
|
||||
style: 'percent',
|
||||
}).format(percent / 100);
|
||||
|
@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { dispatch, select, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { TopBarWithBoundary } from '../../../../common/top-bar/top-bar';
|
||||
import { TopBarWithBeamer } from '../../../../common/top-bar/top-bar';
|
||||
import { Notices } from '../../../listing/components/notices';
|
||||
import { Header } from './components/header';
|
||||
import { Overview } from './components/overview';
|
||||
@ -49,7 +49,7 @@ function TopBarWithBreadcrumb(): JSX.Element {
|
||||
}));
|
||||
|
||||
return (
|
||||
<TopBarWithBoundary>
|
||||
<TopBarWithBeamer>
|
||||
<p className="mailpoet-automation-analytics-title">
|
||||
<a href={MailPoet.urls.automationListing}>
|
||||
{__('Automations', 'mailpoet')}
|
||||
@ -57,7 +57,7 @@ function TopBarWithBreadcrumb(): JSX.Element {
|
||||
› <strong>{automation.name}</strong>
|
||||
<AutomationStatus status={automation.status} />
|
||||
</p>
|
||||
</TopBarWithBoundary>
|
||||
</TopBarWithBeamer>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ const sections: Record<string, Section> = {
|
||||
},
|
||||
},
|
||||
customQuery: {
|
||||
order: 'desc',
|
||||
order: 'asc',
|
||||
order_by: 'updated_at',
|
||||
limit: 25,
|
||||
page: 1,
|
||||
|
@ -167,7 +167,7 @@ export type StepFlowData = {
|
||||
total: number;
|
||||
waiting: Record<string, number> | undefined;
|
||||
failed: Record<string, number> | undefined;
|
||||
completed: Record<string, number> | undefined;
|
||||
flow: Record<string, number> | undefined;
|
||||
};
|
||||
|
||||
export type AutomationFlowSectionData = SectionData & {
|
||||
|
@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
|
||||
import { registerTranslations } from 'common';
|
||||
import { automationTemplateCategories, automationTemplates } from './config';
|
||||
import { initializeApi } from '../api';
|
||||
import { TopBarWithBoundary } from '../../common/top-bar/top-bar';
|
||||
import { TopBarWithBeamer } from '../../common/top-bar/top-bar';
|
||||
import { FromScratchButton } from './components/from-scratch';
|
||||
import { BackButton, PageHeader } from '../../common/page-header';
|
||||
import { MailPoet } from '../../mailpoet';
|
||||
@ -50,7 +50,7 @@ function Templates(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="mailpoet-main-container">
|
||||
<TopBarWithBoundary />
|
||||
<TopBarWithBeamer />
|
||||
<PageHeader
|
||||
heading={__('Start with a template', 'mailpoet')}
|
||||
headingPrefix={
|
||||
|
@ -37,9 +37,8 @@ export type RenderStepFooterType = JSX.Element | null;
|
||||
|
||||
// mailpoet.automation.render_step_separator
|
||||
export type RenderStepSeparatorType = (
|
||||
previousStep: Step,
|
||||
step: Step,
|
||||
index: number,
|
||||
nextStep?: Step,
|
||||
) => JSX.Element;
|
||||
|
||||
// mailpoet.automation.editor.create_store
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import { PlacesType } from 'react-tooltip';
|
||||
import { Badge } from './badge';
|
||||
|
||||
@ -10,59 +10,47 @@ type StatsBadgeProps = {
|
||||
isInverted?: boolean;
|
||||
};
|
||||
|
||||
const getStats = () => ({
|
||||
const stats = {
|
||||
opened: {
|
||||
badgeRanges: [30, 10, 0],
|
||||
badgeTypes: ['excellent', 'good', 'critical'],
|
||||
tooltipText: {
|
||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
excellent: sprintf(__('above %s%%', 'mailpoet'), 30),
|
||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 10, 30),
|
||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
critical: sprintf(__('below %s%%', 'mailpoet'), 10),
|
||||
excellent: _x('above 30%', 'Excellent open rate', 'mailpoet'),
|
||||
good: _x('between 10 and 30%', 'Good open rate', 'mailpoet'),
|
||||
critical: _x('under 10%', 'Critical open rate', 'mailpoet'),
|
||||
},
|
||||
},
|
||||
clicked: {
|
||||
badgeRanges: [3, 1, 0],
|
||||
badgeTypes: ['excellent', 'good', 'critical'],
|
||||
tooltipText: {
|
||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
excellent: sprintf(__('above %s%%', 'mailpoet'), 3),
|
||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 1, 3),
|
||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
critical: sprintf(__('below %s%%', 'mailpoet'), 1),
|
||||
excellent: _x('above 3%', 'Excellent click rate', 'mailpoet'),
|
||||
good: _x('between 1 and 3%', 'Good click rate', 'mailpoet'),
|
||||
critical: _x('under 1%', 'Critical click rate', 'mailpoet'),
|
||||
},
|
||||
},
|
||||
bounced: {
|
||||
badgeRanges: [1.5, 0.5, 0],
|
||||
badgeTypes: ['critical', 'good', 'excellent'],
|
||||
tooltipText: {
|
||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
excellent: sprintf(__('below %s%%', 'mailpoet'), 0.5),
|
||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 0.5, 1.5),
|
||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
critical: sprintf(__('above %s%%', 'mailpoet'), 1.5),
|
||||
excellent: _x('below 0.5%', 'Excellent bounce rate', 'mailpoet'),
|
||||
good: _x('between 0.5% and 1.5%', 'Good bounce rate', 'mailpoet'),
|
||||
critical: _x('above 1.5%', 'Critical bounce rate', 'mailpoet'),
|
||||
},
|
||||
},
|
||||
unsubscribed: {
|
||||
badgeRanges: [0.7, 0.3, 0],
|
||||
badgeTypes: ['critical', 'good', 'excellent'],
|
||||
tooltipText: {
|
||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
excellent: sprintf(__('below %s%%', 'mailpoet'), 0.3),
|
||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 0.3, 0.7),
|
||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
||||
critical: sprintf(__('above %s%%', 'mailpoet'), 0.7),
|
||||
excellent: _x('Below 0.3%', 'Excellent unsubscribe rate', 'mailpoet'),
|
||||
good: _x('between 0.3% and 0.7%', 'Good unsubscribe rate', 'mailpoet'),
|
||||
critical: _x('above 0.7%', 'Critical unsubscribe rate', 'mailpoet'),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getBadgeType = (statName, rate) => {
|
||||
const stat = getStats()[statName] || null;
|
||||
const stat = stats[statName] || null;
|
||||
if (!stat) {
|
||||
return null;
|
||||
}
|
||||
@ -103,7 +91,7 @@ function StatsBadge(props: StatsBadgeProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = getStats()[props.stat] || null;
|
||||
const stat = stats[props.stat] || null;
|
||||
if (!stat) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { action } from '_storybook/action';
|
||||
import { TopBar } from '../top-bar';
|
||||
|
||||
export default {
|
||||
@ -16,7 +17,7 @@ export function TopBarWithoutChildren() {
|
||||
left: '0px',
|
||||
}}
|
||||
>
|
||||
<TopBar />
|
||||
<TopBar hasNews={false} onBeamerClick={action('beamer click')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -33,7 +34,7 @@ export function TopBarWithoutChildrenWithNews() {
|
||||
left: '0px',
|
||||
}}
|
||||
>
|
||||
<TopBar />
|
||||
<TopBar hasNews onBeamerClick={action('beamer click')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { action } from '_storybook/action';
|
||||
import { TopBar } from '../top-bar';
|
||||
import { Button } from '../../button/button';
|
||||
|
||||
@ -17,7 +18,7 @@ export function TopBarWithChildren() {
|
||||
left: '0px',
|
||||
}}
|
||||
>
|
||||
<TopBar>
|
||||
<TopBar hasNews={false} onBeamerClick={action('beamer click')}>
|
||||
<Button>Button</Button>
|
||||
</TopBar>
|
||||
</div>
|
||||
|
22
mailpoet/assets/js/src/common/top-bar/beamer-icon.tsx
Normal file
22
mailpoet/assets/js/src/common/top-bar/beamer-icon.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
export function BeamerIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 17V14.5002L6.00101 14.4997C7.10049 13.9995 7.90003 12.8998 8 11.7002V9.50016C8 7.96615 8.78223 6.62769 10.0309 5.95845C10.3288 5.79879 10.6533 5.67721 11 5.60016C10.9 5.50016 10.8 5.30016 10.8 5.10016C10.8 5.01146 10.8109 4.92493 10.8315 4.84189C10.95 4.36325 11.3887 4.00017 11.9 4.00017C11.9334 4.00017 11.9672 3.99696 12 4C12.0332 3.99688 12.0661 4.00004 12.1 4.00004C12.6113 4.00004 13.05 4.36313 13.1685 4.84177C13.1891 4.92481 13.2 5.01133 13.2 5.10004C13.2 5.30004 13.1 5.50004 13 5.60004C13.3467 5.67709 13.6712 5.79867 13.9691 5.95833C15.2178 6.62757 16 7.96602 16 9.50004V11.7C16.1 12.9 16.9 14.0002 18 14.5002V17.0002L6 17ZM17 16H7V15.148C8.2117 14.4742 8.98499 13.2218 9.09655 11.8831L9.1 9.50004C9.1 8.15301 9.99683 6.94434 11.3676 6.56356L11.7403 6.46006L12.0003 6.40228L12.2553 6.45896L12.6324 6.56368C14.0032 6.94446 14.9 8.15313 14.9 9.50016L14.9035 11.8832C15.015 13.2219 15.7883 14.4743 17 15.1481V16ZM12.0001 5.3601C12.221 5.3601 12.4001 5.18102 12.4001 4.9601C12.4001 4.73919 12.221 4.5601 12.0001 4.5601C11.7792 4.5601 11.6001 4.73919 11.6001 4.9601C11.6001 5.18102 11.7792 5.3601 12.0001 5.3601Z"
|
||||
fill="#2C3338"
|
||||
/>
|
||||
<path
|
||||
d="M13.8303 19C14.0017 18.7054 14.1 18.3637 14.1 18L9.9 18.0002C9.9 18.3638 9.99834 18.7055 10.1697 19.0002C10.5169 19.5968 11.1636 20.0002 11.9 20.0002C11.9339 20.0002 11.9677 19.9993 12.0012 19.9976C12.034 19.9992 12.0669 20 12.1 20C12.8364 20 13.4831 19.5967 13.8303 19Z"
|
||||
fill="#2C3338"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,25 +1,62 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { withFeatureAnnouncement } from 'announcements/with-feature-announcement';
|
||||
import { HideScreenOptions } from 'common/hide-screen-options/hide-screen-options';
|
||||
import { MailPoetLogoResponsive } from './mailpoet-logo-responsive';
|
||||
import { BeamerIcon } from './beamer-icon';
|
||||
import { ScreenOptionsFix } from './screen-options-fix';
|
||||
import { withBoundary } from '../error-boundary';
|
||||
import { MailPoet } from '../../mailpoet';
|
||||
|
||||
type Props = {
|
||||
children?: ReactNode;
|
||||
hasNews?: boolean;
|
||||
onBeamerClick?: () => void;
|
||||
logoWithLink?: boolean;
|
||||
hideScreenOptions?: boolean;
|
||||
};
|
||||
|
||||
export function TopBar({
|
||||
children,
|
||||
hasNews,
|
||||
onBeamerClick,
|
||||
logoWithLink = true,
|
||||
hideScreenOptions = false,
|
||||
}: Props) {
|
||||
const buttonClasses = classnames(
|
||||
'mailpoet-top-bar-beamer',
|
||||
hasNews ? 'mailpoet-top-bar-beamer-dot' : '',
|
||||
);
|
||||
return (
|
||||
<div className="mailpoet-top-bar">
|
||||
<MailPoetLogoResponsive withLink={logoWithLink} />
|
||||
<div className="mailpoet-top-bar-children">{children}</div>
|
||||
<div className="mailpoet-flex-grow" />
|
||||
{onBeamerClick && MailPoet.libs3rdPartyEnabled && (
|
||||
<div>
|
||||
<a
|
||||
role="button"
|
||||
onClick={onBeamerClick}
|
||||
className={buttonClasses}
|
||||
title={__('What’s new', 'mailpoet')}
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
['keydown', 'keypress'].includes(event.type) &&
|
||||
['Enter', ' '].includes(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
onBeamerClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BeamerIcon />
|
||||
<span>{__('Updates', 'mailpoet')}</span>
|
||||
</a>
|
||||
<span id="beamer-empty-element" />
|
||||
</div>
|
||||
)}
|
||||
<ScreenOptionsFix />
|
||||
{hideScreenOptions && <HideScreenOptions />}
|
||||
</div>
|
||||
@ -27,4 +64,4 @@ export function TopBar({
|
||||
}
|
||||
|
||||
TopBar.displayName = 'TopBar';
|
||||
export const TopBarWithBoundary = withBoundary(TopBar);
|
||||
export const TopBarWithBeamer = withFeatureAnnouncement(withBoundary(TopBar));
|
||||
|
@ -2,12 +2,11 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
||||
import { PanelBody, RadioControl, Icon } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import metadata from './block.json';
|
||||
import { storeName } from '../../engine/store/constants';
|
||||
import MailPoetIcon from './mailpoet-icon';
|
||||
|
||||
const getCdnUrl = () => window.mailpoet_cdn_url;
|
||||
const getPremiumPluginStatus = () => window.mailpoet_premium_active;
|
||||
|
||||
function LogoImage({
|
||||
logoSrc,
|
||||
style = {},
|
||||
@ -28,10 +27,13 @@ function Edit({
|
||||
setAttributes: (value: { logo: string }) => void;
|
||||
}): JSX.Element {
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const cdnUrl = getCdnUrl();
|
||||
const isPremiumPluginActive = getPremiumPluginStatus();
|
||||
|
||||
const { cdnUrl, isPremiumPluginActive } = useSelect(
|
||||
(select) => ({
|
||||
cdnUrl: select(storeName).getCdnUrl(),
|
||||
isPremiumPluginActive: select(storeName).isPremiumPluginActive(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
if (isPremiumPluginActive) {
|
||||
return null;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Disables Styles for button
|
||||
* Currently we are not able to read these styles in renderer
|
||||
*/
|
||||
function enhanceButtonBlock() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/change-button',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/button') {
|
||||
return { ...settings, styles: [] };
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { enhanceButtonBlock };
|
@ -0,0 +1,28 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Switch layout to reduced flex email layout
|
||||
* Email render engine can't handle full flex layout se we need to switch to reduced flex layout
|
||||
*/
|
||||
function enhanceButtonsBlock() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/change-buttons',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/buttons') {
|
||||
return {
|
||||
...settings,
|
||||
supports: {
|
||||
...settings.supports,
|
||||
layout: false, // disable block editor's layouts
|
||||
__experimentalEmailFlexLayout: true, // enable MailPoet's reduced flex email layout
|
||||
},
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { enhanceButtonsBlock };
|
@ -0,0 +1,25 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
|
||||
function enhanceColumnBlock() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/change-column',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/column') {
|
||||
return {
|
||||
...settings,
|
||||
supports: {
|
||||
...settings.supports,
|
||||
background: {
|
||||
backgroundImage: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { enhanceColumnBlock };
|
@ -0,0 +1,87 @@
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
const columnsEditCallback = createHigherOrderComponent(
|
||||
(BlockEdit) =>
|
||||
function alterBlocksEdits(props) {
|
||||
if (props.name !== 'core/columns') {
|
||||
return <BlockEdit {...props} />;
|
||||
}
|
||||
// CSS sets opacity by the class is-disabled by the toggle component from the Gutenberg package
|
||||
// To deactivating the input we use CSS pointer-events because we want to avoid JavaScript hacks
|
||||
const deactivateToggleCss = `
|
||||
.components-panel__body .components-toggle-control .components-form-toggle { opacity: 0.3; }
|
||||
.components-panel__body .components-toggle-control .components-form-toggle__input { pointer-events: none; }
|
||||
.components-panel__body .components-toggle-control label { pointer-events: none; }
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockEdit {...props} />
|
||||
<InspectorControls>
|
||||
<style>{deactivateToggleCss}</style>
|
||||
</InspectorControls>
|
||||
</>
|
||||
);
|
||||
},
|
||||
'columnsEditCallback',
|
||||
);
|
||||
|
||||
function deactivateStackOnMobile() {
|
||||
addFilter(
|
||||
'editor.BlockEdit',
|
||||
'mailpoet-email-editor/deactivate-stack-on-mobile',
|
||||
columnsEditCallback,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables layout support for columns and column blocks because
|
||||
* the default layout `flex` add gaps between columns that it is not possible to support in emails.
|
||||
*/
|
||||
function disableColumnsLayout() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/disable-columns-layout',
|
||||
(settings, name) => {
|
||||
if (name === 'core/columns' || name === 'core/column') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...settings,
|
||||
supports: {
|
||||
...settings.supports,
|
||||
layout: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function enhanceColumnsBlock() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/change-columns',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/columns') {
|
||||
return {
|
||||
...settings,
|
||||
supports: {
|
||||
...settings.supports,
|
||||
background: {
|
||||
backgroundImage: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { deactivateStackOnMobile, disableColumnsLayout, enhanceColumnsBlock };
|
@ -1,8 +1,8 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import {
|
||||
Block as WPBlock,
|
||||
BlockSupports as WPBlockSupports,
|
||||
} from '@wordpress/blocks/index';
|
||||
Block as WPBlock,
|
||||
BlockSupports as WPBlockSupports,
|
||||
} from '@wordpress/blocks';
|
||||
|
||||
// Extend the BlockSupports type to include shadow
|
||||
// The shadow is not included in WP6.4 but it is in WP6.5
|
||||
@ -15,19 +15,19 @@ type Block = WPBlock & { supports?: BlockSupports };
|
||||
* Currently we are not able to read these styles in renderer
|
||||
*/
|
||||
function alterSupportConfiguration() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/block-support',
|
||||
( settings: Block ) => {
|
||||
if ( settings.supports?.shadow ) {
|
||||
return {
|
||||
...settings,
|
||||
supports: { ...settings.supports, shadow: false },
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
);
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/block-support',
|
||||
(settings: Block) => {
|
||||
if (settings.supports?.shadow) {
|
||||
return {
|
||||
...settings,
|
||||
supports: { ...settings.supports, shadow: false },
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { alterSupportConfiguration };
|
@ -0,0 +1,26 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Disables layout support for group blocks because the default layout `flex` add gaps between columns that it is not possible to support in emails.
|
||||
*/
|
||||
function disableGroupVariations() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/disable-group-variations',
|
||||
(settings, name) => {
|
||||
if (name === 'core/group') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...settings,
|
||||
variations: settings.variations.filter(
|
||||
(variation) => variation.name === 'group',
|
||||
),
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { disableGroupVariations };
|
@ -0,0 +1,61 @@
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
|
||||
const imageEditCallback = createHigherOrderComponent(
|
||||
(BlockEdit) =>
|
||||
function alterBlocksEdits(props) {
|
||||
if (props.name !== 'core/image') {
|
||||
return <BlockEdit {...props} />;
|
||||
}
|
||||
// Because we cannot support displaying the modal with image after clicking in the email we have to hide the toggle
|
||||
const deactivateToggleCss = `
|
||||
.components-tools-panel .components-toggle-control { display: none; }
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockEdit {...props} />
|
||||
<InspectorControls>
|
||||
<style>{deactivateToggleCss}</style>
|
||||
</InspectorControls>
|
||||
</>
|
||||
);
|
||||
},
|
||||
'imageEditCallback',
|
||||
);
|
||||
|
||||
/**
|
||||
* Because CSS property filter is not supported in almost 50% of email clients we have to disable it
|
||||
*/
|
||||
function disableImageFilter() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/deactivate-image-filter',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/image') {
|
||||
return {
|
||||
...settings,
|
||||
supports: {
|
||||
...settings.supports,
|
||||
filter: {
|
||||
duetone: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function hideExpandOnClick() {
|
||||
addFilter(
|
||||
'editor.BlockEdit',
|
||||
'mailpoet-email-editor/hide-expand-on-click',
|
||||
imageEditCallback,
|
||||
);
|
||||
}
|
||||
|
||||
export { hideExpandOnClick, disableImageFilter };
|
@ -0,0 +1,58 @@
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { Block } from '@wordpress/blocks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
function Placeholder({ layoutClassNames }) {
|
||||
const blockProps = useBlockProps({ className: layoutClassNames });
|
||||
return (
|
||||
<div {...blockProps}>
|
||||
<p>{__('This is the Content block.', 'mailpoet')}</p>
|
||||
<p>
|
||||
{__(
|
||||
'It will display all the blocks in the email content, which might be only simple text paragraphs. You can enrich your message with images, incorporate data through tables, explore different layout designs with columns, or use any other block type.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Curried function to add a custom placeholder to the post content block, or just use the original Edit component.
|
||||
function PostContentEdit(OriginalEditComponent) {
|
||||
return function Edit({
|
||||
context,
|
||||
__unstableLayoutClassNames: layoutClassNames,
|
||||
}) {
|
||||
const { postId: contextPostId, postType: contextPostType } = context;
|
||||
const hasContent = contextPostId && contextPostType;
|
||||
|
||||
if (hasContent) {
|
||||
return (
|
||||
<OriginalEditComponent
|
||||
{...{ context, __unstableLayoutClassNames: layoutClassNames }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Placeholder layoutClassNames={layoutClassNames} />;
|
||||
};
|
||||
}
|
||||
|
||||
function enhancePostContentBlock() {
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
'mailpoet-email-editor/change-post-content',
|
||||
(settings: Block, name) => {
|
||||
if (name === 'core/post-content') {
|
||||
return {
|
||||
...settings,
|
||||
edit: PostContentEdit(settings.edit),
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { enhancePostContentBlock };
|
@ -5,14 +5,14 @@ import { unregisterFormatType } from '@wordpress/rich-text';
|
||||
* Note: This will remove its support for all blocks in the email editor e.g., p, h1,h2, etc
|
||||
*/
|
||||
function disableCertainRichTextFormats() {
|
||||
// remove support for inline image - We can't use it
|
||||
unregisterFormatType( 'core/image' );
|
||||
// remove support for inline image - We can't use it
|
||||
unregisterFormatType('core/image');
|
||||
|
||||
// remove support for Inline code - Not well formatted
|
||||
unregisterFormatType( 'core/code' );
|
||||
// remove support for Inline code - Not well formatted
|
||||
unregisterFormatType('core/code');
|
||||
|
||||
// remove support for Language - Not supported for now
|
||||
unregisterFormatType( 'core/language' );
|
||||
// remove support for Language - Not supported for now
|
||||
unregisterFormatType('core/language');
|
||||
}
|
||||
|
||||
export { disableCertainRichTextFormats };
|
@ -1,9 +1,9 @@
|
||||
import { registerCoreBlocks } from '@wordpress/block-library';
|
||||
import { enhanceColumnBlock } from './core/column';
|
||||
import {
|
||||
disableColumnsLayout,
|
||||
deactivateStackOnMobile,
|
||||
enhanceColumnsBlock,
|
||||
disableColumnsLayout,
|
||||
deactivateStackOnMobile,
|
||||
enhanceColumnsBlock,
|
||||
} from './core/columns';
|
||||
import { enhancePostContentBlock } from './core/post-content';
|
||||
import { disableGroupVariations } from './core/group';
|
||||
@ -14,17 +14,17 @@ import { enhanceButtonsBlock } from './core/buttons';
|
||||
import { alterSupportConfiguration } from './core/general-block-support';
|
||||
|
||||
export function initBlocks() {
|
||||
deactivateStackOnMobile();
|
||||
hideExpandOnClick();
|
||||
disableImageFilter();
|
||||
disableCertainRichTextFormats();
|
||||
disableColumnsLayout();
|
||||
disableGroupVariations();
|
||||
enhanceButtonBlock();
|
||||
enhanceButtonsBlock();
|
||||
enhanceColumnBlock();
|
||||
enhanceColumnsBlock();
|
||||
enhancePostContentBlock();
|
||||
alterSupportConfiguration();
|
||||
registerCoreBlocks();
|
||||
deactivateStackOnMobile();
|
||||
hideExpandOnClick();
|
||||
disableImageFilter();
|
||||
disableCertainRichTextFormats();
|
||||
disableColumnsLayout();
|
||||
disableGroupVariations();
|
||||
enhanceButtonBlock();
|
||||
enhanceButtonsBlock();
|
||||
enhanceColumnBlock();
|
||||
enhanceColumnsBlock();
|
||||
enhancePostContentBlock();
|
||||
alterSupportConfiguration();
|
||||
registerCoreBlocks();
|
||||
}
|
@ -9,31 +9,31 @@ import { storeName } from '../../store';
|
||||
* We could use the action `setEditedPost` from the editor package, but it is only in a newer version of the editor package.
|
||||
*/
|
||||
export function AutosaveMonitor() {
|
||||
const { hasEdits, autosaveInterval } = useSelect(
|
||||
( select ) => ( {
|
||||
hasEdits: select( storeName ).hasEdits(),
|
||||
autosaveInterval: select( storeName ).getAutosaveInterval(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
const { hasEdits, autosaveInterval } = useSelect(
|
||||
(select) => ({
|
||||
hasEdits: select(storeName).hasEdits(),
|
||||
autosaveInterval: select(storeName).getAutosaveInterval(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { saveEditedEmail } = useDispatch( storeName );
|
||||
const { saveEditedEmail } = useDispatch(storeName);
|
||||
|
||||
useEffect( () => {
|
||||
let autosaveTimer: NodeJS.Timeout | undefined;
|
||||
useEffect(() => {
|
||||
let autosaveTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
if ( hasEdits && autosaveInterval > 0 ) {
|
||||
autosaveTimer = setTimeout( () => {
|
||||
void saveEditedEmail();
|
||||
}, autosaveInterval * 1000 );
|
||||
}
|
||||
if (hasEdits && autosaveInterval > 0) {
|
||||
autosaveTimer = setTimeout(() => {
|
||||
void saveEditedEmail();
|
||||
}, autosaveInterval * 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if ( autosaveTimer ) {
|
||||
clearTimeout( autosaveTimer );
|
||||
}
|
||||
};
|
||||
}, [ hasEdits, autosaveInterval, saveEditedEmail ] );
|
||||
return () => {
|
||||
if (autosaveTimer) {
|
||||
clearTimeout(autosaveTimer);
|
||||
}
|
||||
};
|
||||
}, [hasEdits, autosaveInterval, saveEditedEmail]);
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
import {
|
||||
ErrorBoundary,
|
||||
PostLockedModal,
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
privateApis as editorPrivateApis,
|
||||
} from '@wordpress/editor';
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import { SlotFillProvider, Spinner } from '@wordpress/components';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Layout } from './layout';
|
||||
import { unlock } from '../../../lock-unlock';
|
||||
import { useNavigateToEntityRecord } from '../../hooks/use-navigate-to-entity-record';
|
||||
|
||||
const { ExperimentalEditorProvider } = unlock(editorPrivateApis);
|
||||
|
||||
export function InnerEditor({
|
||||
postId: initialPostId,
|
||||
postType: initialPostType,
|
||||
settings,
|
||||
initialEdits,
|
||||
...props
|
||||
}) {
|
||||
const {
|
||||
currentPost,
|
||||
onNavigateToEntityRecord,
|
||||
onNavigateToPreviousEntityRecord,
|
||||
} = useNavigateToEntityRecord(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
initialPostId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
initialPostType,
|
||||
'post-only',
|
||||
);
|
||||
|
||||
const { post, template } = useSelect(
|
||||
(select) => {
|
||||
const { getEntityRecord } = select(coreStore);
|
||||
const { getEditedPostTemplate } = select(storeName);
|
||||
const postObject = getEntityRecord(
|
||||
'postType',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
currentPost.postType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
currentPost.postId,
|
||||
);
|
||||
return {
|
||||
template:
|
||||
currentPost.postType !== 'wp_template'
|
||||
? getEditedPostTemplate()
|
||||
: null,
|
||||
post: postObject,
|
||||
};
|
||||
},
|
||||
[currentPost.postType, currentPost.postId],
|
||||
);
|
||||
|
||||
const editorSettings = useMemo(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
() => ({
|
||||
...settings,
|
||||
onNavigateToEntityRecord,
|
||||
onNavigateToPreviousEntityRecord,
|
||||
defaultRenderingMode: 'template-locked',
|
||||
supportsTemplateMode: true,
|
||||
}),
|
||||
[settings, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord],
|
||||
);
|
||||
|
||||
if (!post || (currentPost.postType !== 'wp_template' && !template)) {
|
||||
return (
|
||||
<div className="spinner-container">
|
||||
<Spinner style={{ width: '80px', height: '80px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlotFillProvider>
|
||||
<ExperimentalEditorProvider
|
||||
settings={editorSettings}
|
||||
post={post}
|
||||
initialEdits={initialEdits}
|
||||
useSubRegistry={false}
|
||||
__unstableTemplate={template}
|
||||
{...props}
|
||||
>
|
||||
{/* @ts-expect-error ErrorBoundary type is incorrect there is no onError */}
|
||||
<ErrorBoundary>
|
||||
<Layout />
|
||||
<PostLockedModal />
|
||||
</ErrorBoundary>
|
||||
</ExperimentalEditorProvider>
|
||||
</SlotFillProvider>
|
||||
);
|
||||
}
|
@ -1,18 +1,5 @@
|
||||
@import '~@wordpress/base-styles/colors';
|
||||
|
||||
.spinner-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Fix editor width. We don't use resizable editor wrapper so we need to set the width manually here
|
||||
.editor-visual-editor > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mailpoet-email-editor {
|
||||
.editor-header__toolbar {
|
||||
flex-grow: 1;
|
||||
@ -48,20 +35,6 @@
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-email-type-info__content_heading {
|
||||
display: flex;
|
||||
|
||||
div:last-child {
|
||||
margin-left: auto;
|
||||
margin-right: -8px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +101,33 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Set default padding-left to have consistent default look in editor and in email
|
||||
// This also overrides the default values in browsers for padding-inline-start
|
||||
ul,
|
||||
ol,
|
||||
ul.has-background,
|
||||
ol.has-background {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
// Override default button border radius which is set in core to 9999px
|
||||
.wp-block-button__link {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.is-mobile-preview {
|
||||
.wp-block-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.wp-block-column {
|
||||
box-sizing: border-box;
|
||||
// override flex-basis set in style attribute to fix the height of the column in mobile preview. Blocks overriding is as a part of style.css in blocks-library
|
||||
flex-basis: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for mobile preview height
|
||||
&.is-mobile-preview {
|
||||
> div {
|
||||
@ -139,6 +139,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
.has-global-padding .wp-block-post-content > .alignfull {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
|
||||
// For the WYSIWYG experience we don't want to display any margins between blocks in the editor
|
||||
.wp-block {
|
||||
clear: both; // for ensuring that floated elements (images) are cleared
|
||||
}
|
||||
|
||||
// Resetting the margin for images in the editor to avoid unexpected spacing
|
||||
.editor-styles-wrapper .is-layout-constrained .wp-block-image {
|
||||
figcaption {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.alignleft,
|
||||
&.alignright {
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
text-align: center;
|
||||
}
|
||||
&.aligncenter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-styles-wrapper {
|
||||
.wp-block-columns:not(.is-not-stacked-on-mobile)
|
||||
> .wp-block-column[style*='flex-basis'] {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
// Content does not need margin. The rendered template does not add margins to match.
|
||||
.editor-styles-wrapper .wp-block-post-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Hide the advanced settings in the sidebar. This panel is not used in the email editor at this moment.
|
||||
.block-editor-block-inspector__advanced {
|
||||
display: none;
|
@ -1,2 +1,4 @@
|
||||
import './index.scss';
|
||||
|
||||
export * from './editor';
|
||||
export * from './layout';
|
@ -0,0 +1,151 @@
|
||||
import {
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
__experimentalUseResizeCanvas as useResizeCanvas,
|
||||
BlockSelectionClearer,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
import {
|
||||
UnsavedChangesWarning,
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
privateApis as editorPrivateApis,
|
||||
store as editorStore,
|
||||
} from '@wordpress/editor';
|
||||
import { uploadMedia } from '@wordpress/media-utils';
|
||||
import classnames from 'classnames';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import {
|
||||
ComplementaryArea,
|
||||
FullscreenMode,
|
||||
InterfaceSkeleton,
|
||||
} from '@wordpress/interface';
|
||||
|
||||
import './index.scss';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { storeName } from '../../store';
|
||||
import { useEmailCss } from '../../hooks';
|
||||
import { AutosaveMonitor } from '../autosave';
|
||||
import { BlockCompatibilityWarnings, Sidebar } from '../sidebar';
|
||||
import { Header } from '../header';
|
||||
import { ListviewSidebar } from '../listview-sidebar/listview-sidebar';
|
||||
import { InserterSidebar } from '../inserter-sidebar/inserter-sidebar';
|
||||
import { EditorNotices, SentEmailNotice } from '../notices';
|
||||
import { StylesSidebar } from '../styles-sidebar';
|
||||
import { unlock } from '../../../lock-unlock';
|
||||
import { TemplateSelection } from '../template-select';
|
||||
|
||||
const { EditorCanvas } = unlock(editorPrivateApis);
|
||||
|
||||
export function Layout() {
|
||||
const {
|
||||
isFullscreenActive,
|
||||
isSidebarOpened,
|
||||
initialSettings,
|
||||
previewDeviceType,
|
||||
isInserterSidebarOpened,
|
||||
isListviewSidebarOpened,
|
||||
canUserEditMedia,
|
||||
hasFixedToolbar,
|
||||
focusMode,
|
||||
styles,
|
||||
isEditingTemplate,
|
||||
} = useSelect(
|
||||
(select) => ({
|
||||
isFullscreenActive: select(storeName).isFeatureActive('fullscreenMode'),
|
||||
isSidebarOpened: select(storeName).isSidebarOpened(),
|
||||
isInserterSidebarOpened: select(storeName).isInserterSidebarOpened(),
|
||||
isListviewSidebarOpened: select(storeName).isListviewSidebarOpened(),
|
||||
initialSettings: select(storeName).getInitialEditorSettings(),
|
||||
previewDeviceType: select(storeName).getPreviewState().deviceType,
|
||||
canUserEditMedia: select(coreStore).canUser('create', 'media'),
|
||||
hasFixedToolbar: select(storeName).isFeatureActive('fixedToolbar'),
|
||||
focusMode: select(storeName).isFeatureActive('focusMode'),
|
||||
styles: select(storeName).getStyles(),
|
||||
isEditingTemplate:
|
||||
select(editorStore).getCurrentPostType() === 'wp_template',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { toggleInserterSidebar } = useDispatch(storeName);
|
||||
|
||||
const [emailCss] = useEmailCss();
|
||||
const className = classnames('edit-post-layout', {
|
||||
'is-sidebar-opened': isSidebarOpened,
|
||||
});
|
||||
|
||||
const contentWrapperStyles = useResizeCanvas(previewDeviceType);
|
||||
|
||||
if (isEditingTemplate) {
|
||||
contentWrapperStyles.height = '100%';
|
||||
}
|
||||
|
||||
// Styles for the canvas. Based on template-canvas.php, this equates to the body element.
|
||||
const canvasStyles = {
|
||||
background:
|
||||
previewDeviceType === 'Desktop' ? styles.color.background : 'transparent',
|
||||
fontFamily: styles.typography.fontFamily,
|
||||
transition: 'all 0.3s ease 0s',
|
||||
};
|
||||
|
||||
const settings = {
|
||||
...initialSettings,
|
||||
mediaUpload: canUserEditMedia ? uploadMedia : null,
|
||||
hasFixedToolbar,
|
||||
focusMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullscreenMode isActive={isFullscreenActive} />
|
||||
<UnsavedChangesWarning />
|
||||
<AutosaveMonitor />
|
||||
<SentEmailNotice />
|
||||
<Sidebar />
|
||||
<StylesSidebar />
|
||||
<TemplateSelection />
|
||||
<InterfaceSkeleton
|
||||
className={className}
|
||||
header={<Header />}
|
||||
editorNotices={<EditorNotices />}
|
||||
content={
|
||||
<>
|
||||
<EditorNotices />
|
||||
<BlockSelectionClearer
|
||||
className="edit-post-visual-editor"
|
||||
style={canvasStyles}
|
||||
onClick={() => {
|
||||
// Clear inserter sidebar when canvas is clicked.
|
||||
if (isInserterSidebarOpened) {
|
||||
void toggleInserterSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames('visual-editor__email_content_wrapper', {
|
||||
'is-mobile-preview': previewDeviceType === 'Mobile',
|
||||
'is-desktop-preview': previewDeviceType === 'Desktop',
|
||||
})}
|
||||
style={contentWrapperStyles}
|
||||
>
|
||||
<EditorCanvas
|
||||
disableIframe={false}
|
||||
styles={[...settings.styles, ...emailCss]}
|
||||
autoFocus
|
||||
className="has-global-padding"
|
||||
renderAppender={false} // With false the appender is rendered in the template mode
|
||||
/>
|
||||
</div>
|
||||
</BlockSelectionClearer>
|
||||
</>
|
||||
}
|
||||
sidebar={<ComplementaryArea.Slot scope={storeName} />}
|
||||
secondarySidebar={
|
||||
(isInserterSidebarOpened && <InserterSidebar />) ||
|
||||
(isListviewSidebarOpened && <ListviewSidebar />)
|
||||
}
|
||||
/>
|
||||
{/* Rendering Warning component here ensures that the warning is displayed under the border configuration. */}
|
||||
<BlockCompatibilityWarnings />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { useRef } from '@wordpress/element';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
VisuallyHidden,
|
||||
__experimentalText as Text,
|
||||
TextControl,
|
||||
} from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
// @see https://github.com/WordPress/gutenberg/blob/5e0ffdbc36cb2e967dfa6a6b812a10a2e56a598f/packages/edit-post/src/components/header/document-actions/index.js
|
||||
|
||||
export function CampaignName() {
|
||||
const { showIconLabels } = useSelect(
|
||||
(select) => ({
|
||||
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
|
||||
postId: select(storeName).getEmailPostId(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [emailTitle = '', setTitle] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'title',
|
||||
);
|
||||
|
||||
const titleRef = useRef(null);
|
||||
return (
|
||||
<div ref={titleRef} className="mailpoet-email-editor-campaign-name">
|
||||
<Dropdown
|
||||
popoverProps={{
|
||||
placement: 'bottom',
|
||||
anchor: titleRef.current,
|
||||
}}
|
||||
contentClassName="mailpoet-email-editor-campaign-name__dropdown"
|
||||
renderToggle={({ isOpen, onToggle }) => (
|
||||
<>
|
||||
<Button
|
||||
onClick={onToggle}
|
||||
className="mailpoet-email-campaign-name__link"
|
||||
>
|
||||
<Text size="body" as="h1">
|
||||
<VisuallyHidden as="span">
|
||||
{__('Editing email:', 'mailpoet')}
|
||||
</VisuallyHidden>
|
||||
{emailTitle}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
className="mailpoet-email-campaign-name__toggle"
|
||||
icon={chevronDown}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
onClick={onToggle}
|
||||
label={__('Change campaign name', 'mailpoet')}
|
||||
>
|
||||
{showIconLabels && __('Rename', 'mailpoet')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<div className="mailpoet-email-editor-email-title-edit">
|
||||
<TextControl
|
||||
label={__('Campaign name', 'mailpoet')}
|
||||
value={emailTitle}
|
||||
onChange={(newTitle) => {
|
||||
setTitle(newTitle);
|
||||
}}
|
||||
name="campaign_name"
|
||||
help={__(
|
||||
`Name your email campaign to indicate its purpose. This would only be visible to you and not shown to your subscribers.`,
|
||||
'mailpoet',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
import { useRef, useState } from '@wordpress/element';
|
||||
import { PinnedItems } from '@wordpress/interface';
|
||||
import { Button, ToolbarItem as WpToolbarItem } from '@wordpress/components';
|
||||
import {
|
||||
NavigableToolbar,
|
||||
BlockToolbar as WPBlockToolbar,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { store as coreDataStore } from '@wordpress/core-data';
|
||||
// @ts-expect-error DocumentBar types are not available
|
||||
import { DocumentBar, store as editorStore } from '@wordpress/editor';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { plus, listView, undo, redo, next, previous } from '@wordpress/icons';
|
||||
import classnames from 'classnames';
|
||||
import { storeName } from '../../store';
|
||||
import { MoreMenu } from './more-menu';
|
||||
import { PreviewDropdown } from '../preview';
|
||||
import { SaveButton } from './save-button';
|
||||
import { CampaignName } from './campaign-name';
|
||||
import { SendButton } from './send-button';
|
||||
|
||||
import { unlock } from '../../../lock-unlock';
|
||||
|
||||
// Build type for ToolbarItem contains only "as" and "children" properties but it takes all props from
|
||||
// component passed to "as" property (in this case Button). So as fix for TS errors we need to pass all props from Button to ToolbarItem.
|
||||
// We should be able to remove this fix when ToolbarItem will be fixed in Gutenberg.
|
||||
const ToolbarItem = WpToolbarItem as React.ForwardRefExoticComponent<
|
||||
React.ComponentProps<typeof WpToolbarItem> &
|
||||
React.ComponentProps<typeof Button>
|
||||
>;
|
||||
|
||||
// Definition of BlockToolbar in currently installed Gutenberg packages (wp-6.4) is missing hideDragHandle prop
|
||||
// After updating to newer version of Gutenberg we should be able to remove this fix
|
||||
const BlockToolbar = WPBlockToolbar as React.FC<
|
||||
React.ComponentProps<typeof WPBlockToolbar> & {
|
||||
hideDragHandle?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export function Header() {
|
||||
const inserterButton = useRef();
|
||||
const listviewButton = useRef();
|
||||
const undoButton = useRef();
|
||||
const redoButton = useRef();
|
||||
|
||||
const [isBlockToolsCollapsed, setIsBlockToolsCollapsed] = useState(false);
|
||||
|
||||
const { toggleInserterSidebar, toggleListviewSidebar } =
|
||||
useDispatch(storeName);
|
||||
const { undo: undoAction, redo: redoAction } = useDispatch(coreDataStore);
|
||||
const {
|
||||
isInserterSidebarOpened,
|
||||
isListviewSidebarOpened,
|
||||
isFixedToolbarActive,
|
||||
isBlockSelected,
|
||||
hasUndo,
|
||||
hasRedo,
|
||||
hasDocumentNavigationHistory,
|
||||
} = useSelect((select) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { getEditorSettings: _getEditorSettings } = unlock(
|
||||
select(editorStore),
|
||||
);
|
||||
const editorSettings = _getEditorSettings();
|
||||
|
||||
return {
|
||||
isInserterSidebarOpened: select(storeName).isInserterSidebarOpened(),
|
||||
isListviewSidebarOpened: select(storeName).isListviewSidebarOpened(),
|
||||
isFixedToolbarActive: select(preferencesStore).get(
|
||||
'core',
|
||||
'fixedToolbar',
|
||||
),
|
||||
isBlockSelected: !!select(blockEditorStore).getBlockSelectionStart(),
|
||||
hasUndo: select(coreDataStore).hasUndo(),
|
||||
hasRedo: select(coreDataStore).hasRedo(),
|
||||
hasDocumentNavigationHistory:
|
||||
!!editorSettings.onNavigateToPreviousEntityRecord,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const preventDefault = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const shortLabelInserter = !isInserterSidebarOpened
|
||||
? __('Add', 'mailpoet')
|
||||
: __('Close', 'mailpoet');
|
||||
|
||||
return (
|
||||
<div className="editor-header edit-post-header">
|
||||
<div className="editor-header__toolbar edit-post-header__toolbar">
|
||||
<NavigableToolbar
|
||||
className="editor-header__toolbar edit-post-header-toolbar is-unstyled editor-document-tools"
|
||||
aria-label={__('Email document tools', 'mailpoet')}
|
||||
>
|
||||
<div className="editor-document-tools__left">
|
||||
<ToolbarItem
|
||||
ref={inserterButton}
|
||||
as={Button}
|
||||
className="editor-header-toolbar__inserter-toggle edit-post-header-toolbar__inserter-toggle"
|
||||
variant="primary"
|
||||
isPressed={isInserterSidebarOpened}
|
||||
onMouseDown={preventDefault}
|
||||
onClick={toggleInserterSidebar}
|
||||
disabled={false}
|
||||
icon={plus}
|
||||
label={shortLabelInserter}
|
||||
showTooltip
|
||||
aria-expanded={isInserterSidebarOpened}
|
||||
/>
|
||||
<ToolbarItem
|
||||
ref={undoButton}
|
||||
as={Button}
|
||||
className="editor-history__undo"
|
||||
isPressed={false}
|
||||
onMouseDown={preventDefault}
|
||||
onClick={undoAction}
|
||||
disabled={!hasUndo}
|
||||
icon={undo}
|
||||
label={__('Undo', 'mailpoet')}
|
||||
showTooltip
|
||||
/>
|
||||
<ToolbarItem
|
||||
ref={redoButton}
|
||||
as={Button}
|
||||
className="editor-history__redo"
|
||||
isPressed={false}
|
||||
onMouseDown={preventDefault}
|
||||
onClick={redoAction}
|
||||
disabled={!hasRedo}
|
||||
icon={redo}
|
||||
label={__('Redo', 'mailpoet')}
|
||||
showTooltip
|
||||
/>
|
||||
<ToolbarItem
|
||||
ref={listviewButton}
|
||||
as={Button}
|
||||
className="editor-header-toolbar__document-overview-toggle edit-post-header-toolbar__document-overview-toggle"
|
||||
isPressed={isListviewSidebarOpened}
|
||||
onMouseDown={preventDefault}
|
||||
onClick={toggleListviewSidebar}
|
||||
disabled={false}
|
||||
icon={listView}
|
||||
label={__('List view', 'mailpoet')}
|
||||
showTooltip
|
||||
aria-expanded={isInserterSidebarOpened}
|
||||
/>
|
||||
</div>
|
||||
</NavigableToolbar>
|
||||
{isFixedToolbarActive && isBlockSelected && (
|
||||
<>
|
||||
<div
|
||||
className={classnames('selected-block-tools-wrapper', {
|
||||
'is-collapsed': isBlockToolsCollapsed,
|
||||
})}
|
||||
>
|
||||
<BlockToolbar hideDragHandle />
|
||||
</div>
|
||||
<Button
|
||||
className="editor-header__block-tools-toggle edit-post-header__block-tools-toggle"
|
||||
icon={isBlockToolsCollapsed ? next : previous}
|
||||
onClick={() => {
|
||||
setIsBlockToolsCollapsed((collapsed) => !collapsed);
|
||||
}}
|
||||
label={
|
||||
isBlockToolsCollapsed
|
||||
? __('Show block tools', 'mailpoet')
|
||||
: __('Hide block tools', 'mailpoet')
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(!isFixedToolbarActive ||
|
||||
!isBlockSelected ||
|
||||
isBlockToolsCollapsed) && (
|
||||
<div className="editor-header__center edit-post-header__center">
|
||||
{hasDocumentNavigationHistory ? <DocumentBar /> : <CampaignName />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="editor-header__settings edit-post-header__settings">
|
||||
<SaveButton />
|
||||
<PreviewDropdown />
|
||||
<SendButton />
|
||||
<PinnedItems.Slot scope={storeName} />
|
||||
<MoreMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -39,20 +39,3 @@
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
// We currently don't support command palette so we hide the shortcut
|
||||
.editor-document-bar__shortcut {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Temporarily hide text about amount of changes to save. The wording is not great for email context.
|
||||
.entities-saved-states__text-prompt {
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure checkbox controls in save panel dropdown to span the full width
|
||||
.mailpoet-email-editor-save-button__dropdown .components-panel__row .components-checkbox-control {
|
||||
width: 100%;
|
||||
}
|
@ -1 +1,3 @@
|
||||
import './index.scss';
|
||||
|
||||
export * from './header';
|
@ -0,0 +1,113 @@
|
||||
import { MenuGroup, MenuItem, DropdownMenu } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { displayShortcut } from '@wordpress/keycodes';
|
||||
import { moreVertical } from '@wordpress/icons';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import { PreferenceToggleMenuItem } from '@wordpress/preferences';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { storeName } from '../../store';
|
||||
import { TrashModal } from './trash-modal';
|
||||
|
||||
// See:
|
||||
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/more-menu/index.js
|
||||
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/more-menu/index.js
|
||||
|
||||
export function MoreMenu(): JSX.Element {
|
||||
const [showTrashModal, setShowTrashModal] = useState(false);
|
||||
const { urls, postId } = useSelect(
|
||||
(select) => ({
|
||||
urls: select(storeName).getUrls(),
|
||||
postId: select(storeName).getEmailPostId(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const [status, setStatus] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'status',
|
||||
);
|
||||
const { saveEditedEmail, updateEmailMailPoetProperty } =
|
||||
useDispatch(storeName);
|
||||
const goToListings = () => {
|
||||
window.location.href = urls.listings;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu
|
||||
className="edit-site-more-menu"
|
||||
popoverProps={{
|
||||
className: 'edit-site-more-menu__content',
|
||||
}}
|
||||
icon={moreVertical}
|
||||
label={__('More', 'mailpoet')}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<MenuGroup label={_x('View', 'noun', 'mailpoet')}>
|
||||
<PreferenceToggleMenuItem
|
||||
scope={storeName}
|
||||
name="fixedToolbar"
|
||||
label={__('Top toolbar', 'mailpoet')}
|
||||
info={__(
|
||||
'Access all block and document tools in a single place',
|
||||
'mailpoet',
|
||||
)}
|
||||
messageActivated={__('Top toolbar activated', 'mailpoet')}
|
||||
messageDeactivated={__('Top toolbar deactivated', 'mailpoet')}
|
||||
/>
|
||||
<PreferenceToggleMenuItem
|
||||
scope={storeName}
|
||||
name="focusMode"
|
||||
label={__('Spotlight mode', 'mailpoet')}
|
||||
info={__('Focus at one block at a time', 'mailpoet')}
|
||||
messageActivated={__('Spotlight mode activated', 'mailpoet')}
|
||||
messageDeactivated={__(
|
||||
'Spotlight mode deactivated',
|
||||
'mailpoet',
|
||||
)}
|
||||
/>
|
||||
<PreferenceToggleMenuItem
|
||||
scope={storeName}
|
||||
name="fullscreenMode"
|
||||
label={__('Fullscreen mode', 'mailpoet')}
|
||||
info={__('Work without distraction', 'mailpoet')}
|
||||
messageActivated={__('Fullscreen mode activated', 'mailpoet')}
|
||||
messageDeactivated={__(
|
||||
'Fullscreen mode deactivated',
|
||||
'mailpoet',
|
||||
)}
|
||||
shortcut={displayShortcut.secondary('f')}
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
{status === 'trash' ? (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await setStatus('draft');
|
||||
await updateEmailMailPoetProperty('deleted_at', '');
|
||||
await saveEditedEmail();
|
||||
}}
|
||||
>
|
||||
{__('Restore from trash', 'mailpoet')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => setShowTrashModal(true)} isDestructive>
|
||||
{__('Move to trash', 'mailpoet')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuGroup>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
{showTrashModal && (
|
||||
<TrashModal
|
||||
onClose={() => setShowTrashModal(false)}
|
||||
onRemove={goToListings}
|
||||
postId={postId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { Button, Dropdown } from '@wordpress/components';
|
||||
import {
|
||||
// @ts-expect-error No types available for useEntitiesSavedStatesIsDirty
|
||||
useEntitiesSavedStatesIsDirty,
|
||||
// @ts-expect-error Our current version of packages doesn't have EntitiesSavedStates export
|
||||
EntitiesSavedStates,
|
||||
} from '@wordpress/editor';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { check, cloud, Icon } from '@wordpress/icons';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
export function SaveButton() {
|
||||
const { saveEditedEmail } = useDispatch(storeName);
|
||||
|
||||
const { dirtyEntityRecords } = useEntitiesSavedStatesIsDirty();
|
||||
|
||||
const { hasEdits, isEmpty, isSaving } = useSelect(
|
||||
(select) => ({
|
||||
hasEdits: select(storeName).hasEdits(),
|
||||
isEmpty: select(storeName).isEmpty(),
|
||||
isSaving: select(storeName).isSaving(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
const hasNonEmailEdits = dirtyEntityRecords.some(
|
||||
(entity) => entity.name !== 'mailpoet_email',
|
||||
);
|
||||
|
||||
const isSaved = !isEmpty && !isSaving && !hasEdits;
|
||||
const isDisabled = isEmpty || isSaving || isSaved;
|
||||
|
||||
let label = __('Save Draft', 'mailpoet');
|
||||
if (isSaved) {
|
||||
label = __('Saved', 'mailpoet');
|
||||
} else if (isSaving) {
|
||||
label = __('Saving', 'mailpoet');
|
||||
}
|
||||
|
||||
return hasNonEmailEdits ? (
|
||||
<div ref={buttonRef}>
|
||||
<Dropdown
|
||||
popoverProps={{
|
||||
placement: 'bottom',
|
||||
anchor: buttonRef.current,
|
||||
}}
|
||||
contentClassName="mailpoet-email-editor-save-button__dropdown"
|
||||
renderToggle={({ onToggle }) => (
|
||||
<Button onClick={onToggle} variant="tertiary">
|
||||
{hasEdits
|
||||
? __('Save email & template', 'mailpoet')
|
||||
: __('Save template', 'mailpoet')}
|
||||
</Button>
|
||||
)}
|
||||
renderContent={({ onToggle }) => (
|
||||
<EntitiesSavedStates close={onToggle} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="tertiary" disabled={isDisabled} onClick={saveEditedEmail}>
|
||||
{isSaving && <Icon icon={cloud} />}
|
||||
{isSaved && <Icon icon={check} />}
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import {
|
||||
store as editorStore,
|
||||
// @ts-expect-error No types available for useEntitiesSavedStatesIsDirty
|
||||
useEntitiesSavedStatesIsDirty,
|
||||
} from '@wordpress/editor';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { MailPoetEmailData, storeName } from 'email-editor/engine/store';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useContentValidation } from 'email-editor/engine/hooks';
|
||||
|
||||
export function SendButton() {
|
||||
const [mailpoetEmail] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'mailpoet_data',
|
||||
);
|
||||
|
||||
const { isDirty } = useEntitiesSavedStatesIsDirty();
|
||||
|
||||
const { validateContent, isValid } = useContentValidation();
|
||||
const { hasEmptyContent, isEmailSent, isEditingTemplate } = useSelect(
|
||||
(select) => ({
|
||||
hasEmptyContent: select(storeName).hasEmptyContent(),
|
||||
isEmailSent: select(storeName).isEmailSent(),
|
||||
isEditingTemplate:
|
||||
select(editorStore).getCurrentPostType() === 'wp_template',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const isDisabled =
|
||||
isEditingTemplate || hasEmptyContent || isEmailSent || isValid || isDirty;
|
||||
|
||||
const mailpoetEmailData: MailPoetEmailData = mailpoetEmail;
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (validateContent()) {
|
||||
window.location.href = `admin.php?page=mailpoet-newsletters#/send/${mailpoetEmailData.id}`;
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{__('Send', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
export function TrashModal({
|
||||
onClose,
|
||||
onRemove,
|
||||
postId,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onRemove: () => void;
|
||||
postId: number;
|
||||
}) {
|
||||
const { getLastEntityDeleteError } = useSelect(coreStore);
|
||||
const { deleteEntityRecord } = useDispatch(coreStore);
|
||||
const { createErrorNotice } = useDispatch(noticesStore);
|
||||
const closeCallback = () => {
|
||||
onClose();
|
||||
};
|
||||
const trashCallback = async () => {
|
||||
const success = await deleteEntityRecord(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
postId as unknown as string,
|
||||
{},
|
||||
{ throwOnError: false },
|
||||
);
|
||||
if (success) {
|
||||
onRemove();
|
||||
} else {
|
||||
const lastError = getLastEntityDeleteError(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
postId,
|
||||
);
|
||||
// Already deleted.
|
||||
if (lastError?.code === 410) {
|
||||
onRemove();
|
||||
} else {
|
||||
const errorMessage = lastError?.message
|
||||
? (lastError.message as string)
|
||||
: __(
|
||||
'An error occurred while moving the email to the trash.',
|
||||
'mailpoet',
|
||||
);
|
||||
await createErrorNotice(errorMessage, {
|
||||
type: 'snackbar',
|
||||
isDismissible: true,
|
||||
context: 'email-editor',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
className="mailpoet-move-to-trash-modal"
|
||||
title={__('Move to trash', 'mailpoet')}
|
||||
onRequestClose={closeCallback}
|
||||
focusOnMount="firstContentElement"
|
||||
>
|
||||
<p>
|
||||
{__('Are you sure you want to move this email to trash?', 'mailpoet')}
|
||||
</p>
|
||||
<div className="mailpoet-send-preview-modal-footer">
|
||||
<Button variant="tertiary" onClick={closeCallback}>
|
||||
{__('Cancel', 'mailpoet')}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={trashCallback}>
|
||||
{__('Move to trash', 'mailpoet')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrashModal;
|
@ -0,0 +1,31 @@
|
||||
import {
|
||||
__experimentalLibrary as Library,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { store as editorStore } from '@wordpress/editor';
|
||||
|
||||
export function InserterSidebar() {
|
||||
const { postContentId, isEditingEmailContent } = useSelect((select) => {
|
||||
const blocks = select(blockEditorStore).getBlocks();
|
||||
return {
|
||||
postContentId: blocks.find((block) => block.name === 'core/post-content')
|
||||
?.clientId,
|
||||
isEditingEmailContent:
|
||||
select(editorStore).getCurrentPostType() !== 'wp_template',
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="edit-post-editor__inserter-panel">
|
||||
<div className="edit-post-editor__inserter-panel-content">
|
||||
<Library
|
||||
showMostUsedBlocks
|
||||
showInserterHelpPanel={false}
|
||||
// In the email content mode we insert primarily into the post content block.
|
||||
rootClientId={isEditingEmailContent ? postContentId : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import {
|
||||
useShortcut,
|
||||
store as keyboardShortcutsStore,
|
||||
} from '@wordpress/keyboard-shortcuts';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
// See:
|
||||
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js
|
||||
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/keyboard-shortcuts/index.js
|
||||
|
||||
export function KeyboardShortcuts(): null {
|
||||
const { isSidebarOpened, hasEdits, isSaving } = useSelect((select) => ({
|
||||
isSidebarOpened: select(storeName).isSidebarOpened(),
|
||||
isSaving: select(storeName).isSaving(),
|
||||
hasEdits: select(storeName).hasEdits(),
|
||||
}));
|
||||
|
||||
const { openSidebar, closeSidebar, saveEditedEmail, toggleFeature } =
|
||||
useDispatch(storeName);
|
||||
|
||||
const { registerShortcut } = useDispatch(keyboardShortcutsStore);
|
||||
|
||||
useEffect(() => {
|
||||
void registerShortcut({
|
||||
name: 'mailpoet/email-editor/toggle-fullscreen',
|
||||
category: 'global',
|
||||
description: __('Toggle fullscreen mode.', 'mailpoet'),
|
||||
keyCombination: {
|
||||
modifier: 'secondary',
|
||||
character: 'f',
|
||||
},
|
||||
});
|
||||
|
||||
void registerShortcut({
|
||||
name: 'mailpoet/email-editor/toggle-sidebar',
|
||||
category: 'global',
|
||||
description: __('Show or hide the settings sidebar.', 'mailpoet'),
|
||||
keyCombination: {
|
||||
modifier: 'primaryShift',
|
||||
character: ',',
|
||||
},
|
||||
});
|
||||
|
||||
void registerShortcut({
|
||||
name: 'mailpoet/email-editor/save',
|
||||
category: 'global',
|
||||
description: __('Save your changes.', 'mailpoet'),
|
||||
keyCombination: {
|
||||
modifier: 'primary',
|
||||
character: 's',
|
||||
},
|
||||
});
|
||||
}, [registerShortcut]);
|
||||
|
||||
useShortcut('mailpoet/email-editor/toggle-fullscreen', () => {
|
||||
void toggleFeature('fullscreenMode');
|
||||
});
|
||||
|
||||
useShortcut('mailpoet/email-editor/toggle-sidebar', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isSidebarOpened) {
|
||||
void closeSidebar();
|
||||
} else {
|
||||
void openSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
useShortcut('mailpoet/email-editor/save', (event) => {
|
||||
event.preventDefault();
|
||||
if (!hasEdits || isSaving) {
|
||||
return;
|
||||
}
|
||||
void saveEditedEmail();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { __experimentalListView as ListView } from '@wordpress/block-editor';
|
||||
|
||||
export function ListviewSidebar() {
|
||||
return (
|
||||
<div className="edit-post-editor__inserter-panel edit-post-editor__document-overview-panel">
|
||||
<div className="edit-post-editor__list-view-panel-content">
|
||||
<div className="edit-post-editor__list-view-container">
|
||||
<ListView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import './index.scss';
|
||||
|
||||
export { EditorNotices } from './notices';
|
||||
export { EditorSnackbars } from './snackbars';
|
||||
export { SentEmailNotice } from './sent-email-notice';
|
@ -0,0 +1,43 @@
|
||||
import { NoticeList } from '@wordpress/components';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { ValidationNotices } from './validation-notices';
|
||||
import { EditorSnackbars } from './snackbars';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/5be0ec4153c3adf9f0f2513239f4f7a358ba7948/packages/editor/src/components/editor-notices/index.js
|
||||
|
||||
export function EditorNotices() {
|
||||
const { notices } = useSelect(
|
||||
(select) => ({
|
||||
notices: select(noticesStore).getNotices('email-editor'),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { removeNotice } = useDispatch(noticesStore);
|
||||
|
||||
const dismissibleNotices = notices.filter(
|
||||
({ isDismissible, type }) => isDismissible && type === 'default',
|
||||
);
|
||||
|
||||
const nonDismissibleNotices = notices.filter(
|
||||
({ isDismissible, type }) => !isDismissible && type === 'default',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NoticeList
|
||||
notices={nonDismissibleNotices}
|
||||
className="components-editor-notices__pinned"
|
||||
/>
|
||||
<NoticeList
|
||||
notices={dismissibleNotices}
|
||||
className="components-editor-notices__dismissible"
|
||||
onRemove={(id) => removeNotice(id, 'email-editor')}
|
||||
/>
|
||||
<ValidationNotices />
|
||||
<EditorSnackbars context="global" />
|
||||
<EditorSnackbars context="email-editor" />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import { storeName } from '../../store';
|
||||
|
||||
export function SentEmailNotice() {
|
||||
const { isEmailSent } = useSelect(
|
||||
(select) => ({
|
||||
isEmailSent: select(storeName).isEmailSent(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmailSent) {
|
||||
void dispatch(noticesStore).createNotice(
|
||||
'warning',
|
||||
__(
|
||||
'This email has already been sent. It can be edited, but not sent again. Duplicate this email if you want to send it again.',
|
||||
'mailpoet',
|
||||
),
|
||||
{
|
||||
id: 'email-sent',
|
||||
isDismissible: false,
|
||||
context: 'email-editor',
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [isEmailSent]);
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { SnackbarList } from '@wordpress/components';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/2788a9cf8b8149be3ee52dd15ce91fa55815f36a/packages/editor/src/components/editor-snackbars/index.js
|
||||
|
||||
export function EditorSnackbars({ context = 'email-editor' }) {
|
||||
const { notices } = useSelect(
|
||||
(select) => ({
|
||||
notices: select(noticesStore).getNotices(context),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { removeNotice } = useDispatch(noticesStore);
|
||||
|
||||
const snackbarNotices = notices.filter(({ type }) => type === 'snackbar');
|
||||
|
||||
return (
|
||||
<SnackbarList
|
||||
notices={snackbarNotices}
|
||||
className="components-editor-notices__snackbar"
|
||||
onRemove={(id) => removeNotice(id, context)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Notice, Button } from '@wordpress/components';
|
||||
import { useValidationNotices } from 'email-editor/engine/hooks';
|
||||
|
||||
export function ValidationNotices() {
|
||||
const { notices } = useValidationNotices();
|
||||
|
||||
if (notices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Notice
|
||||
status="error"
|
||||
className="mailpoet-email-editor-validation-errors components-editor-notices__pinned"
|
||||
isDismissible={false}
|
||||
>
|
||||
<>
|
||||
<strong>{__('Fix errors to continue:', 'mailpoet')}</strong>
|
||||
<ul>
|
||||
{notices.map(({ id, content, actions }) => (
|
||||
<li key={id}>
|
||||
{content}
|
||||
{actions.length > 0
|
||||
? actions.map(({ label, onClick }) => (
|
||||
<Button key={label} onClick={onClick} variant="link">
|
||||
{label}
|
||||
</Button>
|
||||
))
|
||||
: null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
</Notice>
|
||||
);
|
||||
}
|
@ -1,2 +1,4 @@
|
||||
import './index.scss';
|
||||
|
||||
export * from './preview-dropdown';
|
||||
export * from './send-preview-email';
|
@ -0,0 +1,100 @@
|
||||
import {
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
} from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, external, check, mobile, desktop } from '@wordpress/icons';
|
||||
import { SendPreviewEmail } from './send-preview-email';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
export function PreviewDropdown() {
|
||||
const [mailpoetEmailData] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'mailpoet_data',
|
||||
);
|
||||
|
||||
const previewDeviceType = useSelect((select) => {
|
||||
const { deviceType } = select(storeName).getPreviewState();
|
||||
return deviceType;
|
||||
}, []);
|
||||
|
||||
const { changePreviewDeviceType, togglePreviewModal } =
|
||||
useDispatch(storeName);
|
||||
const newsletterPreviewUrl: string = mailpoetEmailData?.preview_url || '';
|
||||
|
||||
const changeDeviceType = (newDeviceType: string) => {
|
||||
void changePreviewDeviceType(newDeviceType);
|
||||
};
|
||||
|
||||
const openInNewTab = (url: string) => {
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
};
|
||||
|
||||
const deviceIcons = {
|
||||
mobile,
|
||||
desktop,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu
|
||||
className="mailpoet-preview-dropdown"
|
||||
label={__('Preview', 'mailpoet')}
|
||||
icon={deviceIcons[previewDeviceType.toLowerCase()]}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
className="block-editor-post-preview__button-resize"
|
||||
onClick={() => changeDeviceType('Desktop')}
|
||||
icon={previewDeviceType === 'Desktop' && check}
|
||||
>
|
||||
{__('Desktop', 'mailpoet')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="block-editor-post-preview__button-resize"
|
||||
onClick={() => changeDeviceType('Mobile')}
|
||||
icon={previewDeviceType === 'Mobile' && check}
|
||||
>
|
||||
{__('Mobile', 'mailpoet')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
className="block-editor-post-preview__button-resize"
|
||||
onClick={() => {
|
||||
void togglePreviewModal(true);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{__('Send a test email', 'mailpoet')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
{newsletterPreviewUrl ? (
|
||||
<MenuGroup>
|
||||
<div className="edit-post-header-preview__grouping-external">
|
||||
<Button
|
||||
className="edit-post-header-preview__button-external components-menu-item__button"
|
||||
onClick={() => {
|
||||
openInNewTab(newsletterPreviewUrl);
|
||||
}}
|
||||
>
|
||||
{__('Preview in new tab', 'mailpoet')}
|
||||
<Icon icon={external} />
|
||||
</Button>
|
||||
</div>
|
||||
</MenuGroup>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
<SendPreviewEmail />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { check, Icon } from '@wordpress/icons';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { ENTER } from '@wordpress/keycodes';
|
||||
import { isEmail } from '@wordpress/url';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import {
|
||||
MailPoetEmailData,
|
||||
SendingPreviewStatus,
|
||||
storeName,
|
||||
} from '../../store';
|
||||
|
||||
export function SendPreviewEmail() {
|
||||
const sendToRef = useRef(null);
|
||||
|
||||
const {
|
||||
requestSendingNewsletterPreview,
|
||||
togglePreviewModal,
|
||||
updateSendPreviewEmail,
|
||||
} = useDispatch(storeName);
|
||||
|
||||
const {
|
||||
toEmail: previewToEmail,
|
||||
isSendingPreviewEmail,
|
||||
sendingPreviewStatus,
|
||||
isModalOpened,
|
||||
} = useSelect((select) => select(storeName).getPreviewState(), []);
|
||||
|
||||
const [mailpoetEmailData] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'mailpoet_data',
|
||||
) as [MailPoetEmailData, unknown, unknown];
|
||||
|
||||
const handleSendPreviewEmail = () => {
|
||||
void requestSendingNewsletterPreview(mailpoetEmailData.id, previewToEmail);
|
||||
};
|
||||
|
||||
const closeCallback = () => togglePreviewModal(false);
|
||||
|
||||
// We use this effect to focus on the input field when the modal is opened
|
||||
useEffect(() => {
|
||||
if (isModalOpened) {
|
||||
sendToRef.current?.focus();
|
||||
}
|
||||
}, [isModalOpened]);
|
||||
|
||||
if (!isModalOpened) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="mailpoet-send-preview-email"
|
||||
title={__('Send a test email', 'mailpoet')}
|
||||
onRequestClose={closeCallback}
|
||||
focusOnMount={false}
|
||||
>
|
||||
{sendingPreviewStatus === SendingPreviewStatus.ERROR ? (
|
||||
<div className="mailpoet-send-preview-modal-notice-error">
|
||||
{__('Sorry, we were unable to send this email.', 'mailpoet')}
|
||||
<ul>
|
||||
<li>
|
||||
{createInterpolateElement(
|
||||
__(
|
||||
'Please check your <link>sending method configuration</link> with your hosting provider.',
|
||||
'mailpoet',
|
||||
),
|
||||
{
|
||||
link: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href="admin.php?page=mailpoet-settings#mta"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{createInterpolateElement(
|
||||
__(
|
||||
'Or, sign up for MailPoet Sending Service to easily send emails. <link>Sign up for free</link>',
|
||||
'mailpoet',
|
||||
),
|
||||
{
|
||||
link: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href={new URL(
|
||||
'free-plan',
|
||||
'https://www.mailpoet.com/',
|
||||
).toString()}
|
||||
key="sign-up-for-free"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
<p>
|
||||
{createInterpolateElement(
|
||||
__(
|
||||
'Send yourself a test email to test how your email would look like in different email apps. You can also test your spam score by sending a test email to <link1>{$serviceName}</link1>. <link2>Learn more</link2>.',
|
||||
'mailpoet',
|
||||
).replace('{$serviceName}', 'Mail Tester'),
|
||||
{
|
||||
link1: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href="https://www.mail-tester.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
link2: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href="https://kb.mailpoet.com/article/147-test-your-spam-score-with-mail-tester"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<TextControl
|
||||
label={__('Send to', 'mailpoet')}
|
||||
onChange={(email) => {
|
||||
void updateSendPreviewEmail(email);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === ENTER) {
|
||||
event.preventDefault();
|
||||
handleSendPreviewEmail();
|
||||
}
|
||||
}}
|
||||
value={previewToEmail}
|
||||
type="email"
|
||||
ref={sendToRef}
|
||||
required
|
||||
/>
|
||||
{sendingPreviewStatus === SendingPreviewStatus.SUCCESS ? (
|
||||
<p className="mailpoet-send-preview-modal-notice-success">
|
||||
<Icon icon={check} style={{ fill: '#4AB866' }} />
|
||||
{__('Test email sent successfully!', 'mailpoet')}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mailpoet-send-preview-modal-footer">
|
||||
<Button variant="tertiary" onClick={closeCallback}>
|
||||
{__('Close', 'mailpoet')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSendPreviewEmail}
|
||||
disabled={isSendingPreviewEmail || !isEmail(previewToEmail)}
|
||||
>
|
||||
{isSendingPreviewEmail
|
||||
? __('Sending...', 'mailpoet')
|
||||
: __('Send test email', 'mailpoet')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import { hasBlockSupport, getBlockSupport } from '@wordpress/blocks';
|
||||
import { Fill, Notice } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const hasBackgroundImageSupport = (nameOrType: string) => {
|
||||
// @ts-expect-error not yet supported in the types
|
||||
const backgroundSupport = getBlockSupport(nameOrType, 'background') as Record<
|
||||
string,
|
||||
boolean
|
||||
>;
|
||||
|
||||
return backgroundSupport && backgroundSupport?.backgroundImage !== false;
|
||||
};
|
||||
|
||||
export function BlockCompatibilityWarnings(): JSX.Element {
|
||||
// Select the currently selected block
|
||||
const selectedBlock = useSelect(
|
||||
(sel) => sel('core/block-editor').getSelectedBlock(),
|
||||
[],
|
||||
);
|
||||
|
||||
// Check if the selected block has enabled border configuration
|
||||
const hasBorderSupport = hasBlockSupport(
|
||||
selectedBlock?.name,
|
||||
// @ts-expect-error Border is not yet supported in the types
|
||||
'__experimentalBorder',
|
||||
false,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasBorderSupport && (
|
||||
<Fill name="InspectorControlsBorder">
|
||||
<Notice
|
||||
className="mailpoet__grid-full-width"
|
||||
status="warning"
|
||||
isDismissible={false}
|
||||
>
|
||||
{__(
|
||||
'Border display may vary or be unsupported in some email clients.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</Notice>
|
||||
</Fill>
|
||||
)}
|
||||
{hasBackgroundImageSupport(selectedBlock?.name) && (
|
||||
<Fill name="InspectorControlsBackground">
|
||||
<Notice
|
||||
className="mailpoet__grid-full-width"
|
||||
status="warning"
|
||||
isDismissible={false}
|
||||
>
|
||||
{__(
|
||||
'Select a background color for email clients that do not support background images.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</Notice>
|
||||
</Fill>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import {
|
||||
__experimentalText as Text,
|
||||
ExternalLink,
|
||||
PanelBody,
|
||||
TextareaControl,
|
||||
} from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { storeName } from '../../store';
|
||||
|
||||
const previewTextMaxLength = 150;
|
||||
const previewTextRecommendedLength = 80;
|
||||
|
||||
export function DetailsPanel() {
|
||||
const [mailpoetEmailData] = useEntityProp(
|
||||
'postType',
|
||||
'mailpoet_email',
|
||||
'mailpoet_data',
|
||||
);
|
||||
|
||||
const { updateEmailMailPoetProperty } = useDispatch(storeName);
|
||||
const subjectHelp = createInterpolateElement(
|
||||
__(
|
||||
'Use shortcodes to personalize your email, or learn more about <bestPracticeLink>best practices</bestPracticeLink> and using <emojiLink>emoji in subject lines</emojiLink>.',
|
||||
'mailpoet',
|
||||
),
|
||||
{
|
||||
bestPracticeLink: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href="https://www.mailpoet.com/blog/17-email-subject-line-best-practices-to-boost-engagement/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
emojiLink: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href="https://www.mailpoet.com/blog/tips-using-emojis-in-subject-lines/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const subjectLabel = (
|
||||
<>
|
||||
<span>{__('Subject', 'mailpoet')}</span>
|
||||
<ExternalLink href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes#list">
|
||||
{__('Shortcode guide', 'mailpoet')}
|
||||
</ExternalLink>
|
||||
</>
|
||||
);
|
||||
|
||||
const previewTextLength = mailpoetEmailData?.preheader?.length ?? 0;
|
||||
|
||||
const preheaderLabel = (
|
||||
<>
|
||||
<span>{__('Preview text', 'mailpoet')}</span>
|
||||
<span
|
||||
className={classnames('mailpoet-settings-panel__preview-text-length', {
|
||||
'mailpoet-settings-panel__preview-text-length-warning':
|
||||
previewTextLength > previewTextRecommendedLength,
|
||||
'mailpoet-settings-panel__preview-text-length-error':
|
||||
previewTextLength > previewTextMaxLength,
|
||||
})}
|
||||
>
|
||||
{previewTextLength}/{previewTextMaxLength}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelBody
|
||||
title={__('Details', 'mailpoet')}
|
||||
className="mailpoet-email-editor__settings-panel"
|
||||
>
|
||||
<TextareaControl
|
||||
className="mailpoet-settings-panel__subject"
|
||||
label={subjectLabel}
|
||||
placeholder={__('Eg. The summer sale is here!', 'mailpoet')}
|
||||
value={mailpoetEmailData?.subject ?? ''}
|
||||
onChange={(value) => updateEmailMailPoetProperty('subject', value)}
|
||||
data-automation-id="email_subject"
|
||||
/>
|
||||
<div className="mailpoet-settings-panel__help">
|
||||
<Text>{subjectHelp}</Text>
|
||||
</div>
|
||||
|
||||
<TextareaControl
|
||||
className="mailpoet-settings-panel__preview-text"
|
||||
label={preheaderLabel}
|
||||
placeholder={__(
|
||||
"Add a preview text to capture subscribers' attention and increase open rates.",
|
||||
'mailpoet',
|
||||
)}
|
||||
value={mailpoetEmailData?.preheader ?? ''}
|
||||
onChange={(value) => updateEmailMailPoetProperty('preheader', value)}
|
||||
data-automation-id="email_preview_text"
|
||||
/>
|
||||
<div className="mailpoet-settings-panel__help">
|
||||
<Text>
|
||||
{createInterpolateElement(
|
||||
__(
|
||||
'<link>This text</link> will appear in the inbox, underneath the subject line.',
|
||||
'mailpoet',
|
||||
),
|
||||
{
|
||||
link: (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||
<a
|
||||
href={new URL(
|
||||
'article/418-preview-text',
|
||||
'https://kb.mailpoet.com/',
|
||||
).toString()}
|
||||
key="preview-text-kb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import { Panel } from '@wordpress/components';
|
||||
import { DetailsPanel } from './details-panel';
|
||||
import { EmailTypeInfo } from './email-type-info';
|
||||
import { TemplatesPanel } from './templates-panel';
|
||||
|
||||
export function EmailSettings() {
|
||||
return (
|
||||
<Panel>
|
||||
<EmailTypeInfo />
|
||||
<DetailsPanel />
|
||||
</Panel>
|
||||
);
|
||||
return (
|
||||
<Panel>
|
||||
<EmailTypeInfo />
|
||||
<TemplatesPanel />
|
||||
<DetailsPanel />
|
||||
</Panel>
|
||||
);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Panel, PanelBody, PanelRow } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, megaphone } from '@wordpress/icons';
|
||||
|
||||
export function EmailTypeInfo() {
|
||||
return (
|
||||
<Panel className="mailpoet-email-sidebar__email-type-info">
|
||||
<PanelBody>
|
||||
<PanelRow>
|
||||
<span className="mailpoet-email-type-info__icon">
|
||||
<Icon icon={megaphone} />
|
||||
</span>
|
||||
<div className="mailpoet-email-type-info__content">
|
||||
<h2>{__('Newsletter', 'mailpoet')}</h2>
|
||||
<span>
|
||||
{__(
|
||||
'Send or schedule a newsletter to connect with your subscribers.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</PanelRow>
|
||||
</PanelBody>
|
||||
</Panel>
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import * as React from '@wordpress/element';
|
||||
import { privateApis as componentsPrivateApis } from '@wordpress/components';
|
||||
import { mainSidebarEmailTab, mainSidebarBlockTab } from '../../store';
|
||||
import { unlock } from '../../../lock-unlock';
|
||||
|
||||
const { Tabs } = unlock(componentsPrivateApis);
|
||||
|
||||
export function HeaderTabs(_, ref) {
|
||||
return (
|
||||
<Tabs.TabList ref={ref}>
|
||||
<Tabs.Tab tabId={mainSidebarEmailTab}>{__('Email', 'mailpoet')}</Tabs.Tab>
|
||||
<Tabs.Tab tabId={mainSidebarBlockTab}>{__('Block')}</Tabs.Tab>
|
||||
</Tabs.TabList>
|
||||
);
|
||||
}
|
||||
|
||||
export const Header = React.forwardRef(HeaderTabs);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user