Compare commits

..

3 Commits
5.5.2 ... 5.0.1

Author SHA1 Message Date
fe6fa9480a Release 5.0.1 2024-08-23 15:42:48 +02:00
6cb51a3841 Remove unused parameters in MailPoet.date.toDate
The options were just set in init but there was no effect.
[MAILPOET-6197]
2024-08-23 15:37:16 +02:00
122f73c3c2 Do not apply offset when manipulating with date in the Date picker
We use DateTime component on the send page.
The DateTime component uses DateText for picking date.
We add site's offset before we pass the date to the DateTime.

The date-text component is nested in the DateTime and was also applying
offset on the date.

The second issue was we needed to convert the value to JS Date.
The MailPoet.Date.toDate expects the value to be in UTC, and we provided an already converted date.

So for offset -07:00
MailPoet.Date.toDate('2023-09-01');
ends up
Thu Aug 31 2023 17:00:00 GMT-0700 (Pacific Daylight Time) and calendar display's incorrect value.

This commit fixes it so that we don't touch offset in the DateText component.
[MAILPOET-6197]
2024-08-23 15:37:07 +02:00
867 changed files with 19278 additions and 36764 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -9,5 +9,3 @@ npm-debug.log
mailpoet-premium
tsconfig.tsbuildinfo
/wordpress
packages/php/*/vendor
tests_env/vendor

View File

@ -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}"

View File

@ -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

View File

@ -25,5 +25,3 @@ vendor-prefixed
/mailpoet/views
/mailpoet-premium
/wordpress
/packages/php/email-editor
/packages/js/email-editor

View File

@ -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

View File

@ -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
"

View File

@ -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
View File

@ -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

View File

@ -1,3 +1,5 @@
version: '3.8'
services:
# for M1 Macs
db:

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -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 ' : '') .

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -30,11 +30,6 @@
}
}
.mailpoet-template-preview-image {
max-width: 100%;
width: 700px;
}
.mailpoet-template-info {
align-items: center;
display: flex;

View File

@ -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");
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -21,7 +21,7 @@
&:focus {
~ .mailpoet-form-checkbox-control {
border-color: $color-input-border-focus;
border: 2px solid $color-input-border;
}
}
}

View File

@ -21,7 +21,7 @@
&:focus {
~ .mailpoet-form-radio-control {
border-color: $color-input-border-focus;
border: 2px solid $color-input-border;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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 };

View File

@ -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}
/>
);
};
};

View File

@ -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 />

View File

@ -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}

View File

@ -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);
}

View File

@ -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>

View File

@ -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} />
</>
);
};

View File

@ -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);

View File

@ -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>
);
}

View File

@ -38,7 +38,7 @@ const sections: Record<string, Section> = {
},
},
customQuery: {
order: 'desc',
order: 'asc',
order_by: 'updated_at',
limit: 25,
page: 1,

View File

@ -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 & {

View File

@ -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={

View File

@ -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

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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>

View 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>
);
}

View File

@ -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={__('Whats 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));

View File

@ -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;
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -1,2 +1,4 @@
import './index.scss';
export * from './editor';
export * from './layout';

View File

@ -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 />
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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%;
}

View File

@ -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}
/>
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -1,3 +1,5 @@
import './index.scss';
export { EditorNotices } from './notices';
export { EditorSnackbars } from './snackbars';
export { SentEmailNotice } from './sent-email-notice';

View File

@ -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" />
</>
);
}

View File

@ -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;
}

View File

@ -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)}
/>
);
}

View File

@ -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>
);
}

View File

@ -1,2 +1,4 @@
import './index.scss';
export * from './preview-dropdown';
export * from './send-preview-email';

View File

@ -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 />
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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