Compare commits
1 Commits
update-plu
...
5.3.5
Author | SHA1 | Date | |
---|---|---|---|
81619d62ab |
@ -99,12 +99,12 @@ executors:
|
|||||||
wpcli_php_max_wporg:
|
wpcli_php_max_wporg:
|
||||||
<<: *default_job_config
|
<<: *default_job_config
|
||||||
docker:
|
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:
|
wpcli_php_latest:
|
||||||
<<: *default_job_config
|
<<: *default_job_config
|
||||||
docker:
|
docker:
|
||||||
- image: mailpoet/wordpress:8.2_20241126.1
|
- image: mailpoet/wordpress:8.1_20230307.1
|
||||||
|
|
||||||
wpcli_php_mysql_oldest:
|
wpcli_php_mysql_oldest:
|
||||||
<<: *default_job_config
|
<<: *default_job_config
|
||||||
@ -115,7 +115,7 @@ executors:
|
|||||||
wpcli_php_mysql_latest:
|
wpcli_php_mysql_latest:
|
||||||
<<: *default_job_config
|
<<: *default_job_config
|
||||||
docker:
|
docker:
|
||||||
- image: mailpoet/wordpress:8.2_20241126.1
|
- image: mailpoet/wordpress:8.1_20230307.1
|
||||||
- image: cimg/mysql:8.0
|
- image: cimg/mysql:8.0
|
||||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_520_ci
|
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" }}
|
key: composer-{{ checksum "tasks/code_sniffer/composer.json" }}-{{ checksum "tasks/code_sniffer/composer.lock" }}
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
||||||
- restore_cache:
|
|
||||||
key: composer-{{ checksum "../tests_env/composer.json" }}-{{ checksum "../tests_env/composer.lock" }}
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
@ -159,7 +157,7 @@ jobs:
|
|||||||
./do install
|
./do install
|
||||||
./do compile:all --env production --skip-tests
|
./do compile:all --env production --skip-tests
|
||||||
./do doctrine:generate-cache
|
./do doctrine:generate-cache
|
||||||
../tests_env/vendor/bin/codecept build
|
vendor/bin/codecept build
|
||||||
./do twig:generate-cache
|
./do twig:generate-cache
|
||||||
- run:
|
- run:
|
||||||
name: 'Check Prettier formatting'
|
name: 'Check Prettier formatting'
|
||||||
@ -180,10 +178,6 @@ jobs:
|
|||||||
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
key: composer-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
|
||||||
paths:
|
paths:
|
||||||
- vendor
|
- vendor
|
||||||
- save_cache:
|
|
||||||
key: composer-{{ checksum "../tests_env/composer.json" }}-{{ checksum "../tests_env/composer.lock" }}
|
|
||||||
paths:
|
|
||||||
- ../tests_env/vendor
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
||||||
paths:
|
paths:
|
||||||
@ -197,10 +191,10 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Download additional WP Plugins for tests
|
name: Download additional WP Plugins for tests
|
||||||
command: |
|
command: |
|
||||||
./do download:woo-commerce-zip 9.6.1
|
./do download:woo-commerce-zip 9.3.3
|
||||||
./do download:woo-commerce-subscriptions-zip 7.1.0
|
./do download:woo-commerce-subscriptions-zip 6.8.0
|
||||||
./do download:woo-commerce-memberships-zip 1.26.5
|
./do download:woo-commerce-memberships-zip 1.26.5
|
||||||
./do download:automate-woo-zip 6.1.5
|
./do download:automate-woo-zip 6.1.0
|
||||||
- run:
|
- run:
|
||||||
name: Dump tests ENV variables for acceptance tests
|
name: Dump tests ENV variables for acceptance tests
|
||||||
command: |
|
command: |
|
||||||
@ -1082,15 +1076,15 @@ workflows:
|
|||||||
- acceptance_tests:
|
- acceptance_tests:
|
||||||
<<: *slack-fail-post-step
|
<<: *slack-fail-post-step
|
||||||
name: acceptance_oldest
|
name: acceptance_oldest
|
||||||
woo_core_version: 9.5.2
|
woo_core_version: 9.2.3
|
||||||
woo_subscriptions_version: 7.0.0
|
woo_subscriptions_version: 6.7.0
|
||||||
woo_memberships_version: 1.25.2
|
woo_memberships_version: 1.25.2
|
||||||
automate_woo_version: 6.0.33
|
automate_woo_version: 6.0.33
|
||||||
mysql_command: --max_allowed_packet=100M
|
mysql_command: --max_allowed_packet=100M
|
||||||
mysql_image: mysql:5.5
|
mysql_image: mysql:5.5
|
||||||
codeception_image_version: 7.4-cli_20220605.0
|
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_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_version: 6.5.5
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
- performance_tests:
|
- performance_tests:
|
||||||
@ -1123,13 +1117,13 @@ workflows:
|
|||||||
- integration_tests:
|
- integration_tests:
|
||||||
<<: *slack-fail-post-step
|
<<: *slack-fail-post-step
|
||||||
name: integration_oldest
|
name: integration_oldest
|
||||||
woo_core_version: 9.5.2
|
woo_core_version: 9.2.3
|
||||||
woo_subscriptions_version: 7.0.0
|
woo_subscriptions_version: 6.7.0
|
||||||
woo_memberships_version: 1.25.2
|
woo_memberships_version: 1.25.2
|
||||||
automate_woo_version: 6.0.33
|
automate_woo_version: 6.0.33
|
||||||
codeception_image_version: 7.4-cli_20220605.0
|
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_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
|
wordpress_version: 6.5.5
|
||||||
mysql_command: --max_allowed_packet=100M
|
mysql_command: --max_allowed_packet=100M
|
||||||
mysql_image: mysql:5.5
|
mysql_image: mysql:5.5
|
||||||
requires:
|
requires:
|
||||||
@ -1186,25 +1180,25 @@ workflows:
|
|||||||
- acceptance_tests:
|
- acceptance_tests:
|
||||||
<<: *slack-fail-post-step
|
<<: *slack-fail-post-step
|
||||||
name: acceptance_with_premium_oldest
|
name: acceptance_with_premium_oldest
|
||||||
woo_core_version: 9.5.2
|
woo_core_version: 9.2.3
|
||||||
woo_subscriptions_version: 7.0.0
|
woo_subscriptions_version: 6.7.0
|
||||||
woo_memberships_version: 1.25.2
|
woo_memberships_version: 1.25.2
|
||||||
automate_woo_version: 6.0.33
|
automate_woo_version: 6.0.33
|
||||||
codeception_image_version: 7.4-cli_20220605.0
|
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_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_version: 6.5.5
|
||||||
requires:
|
requires:
|
||||||
- build_premium
|
- build_premium
|
||||||
- integration_tests:
|
- integration_tests:
|
||||||
<<: *slack-fail-post-step
|
<<: *slack-fail-post-step
|
||||||
name: integration_with_premium_oldest
|
name: integration_with_premium_oldest
|
||||||
woo_core_version: 9.5.2
|
woo_core_version: 9.2.3
|
||||||
woo_subscriptions_version: 7.0.0
|
woo_subscriptions_version: 6.7.0
|
||||||
woo_memberships_version: 1.25.2
|
woo_memberships_version: 1.25.2
|
||||||
automate_woo_version: 6.0.33
|
automate_woo_version: 6.0.33
|
||||||
codeception_image_version: 7.4-cli_20220605.0
|
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_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_version: 6.5.5
|
||||||
mysql_command: --max_allowed_packet=100M
|
mysql_command: --max_allowed_packet=100M
|
||||||
mysql_image: mysql:5.5
|
mysql_image: mysql:5.5
|
||||||
requires:
|
requires:
|
||||||
|
@ -10,12 +10,6 @@ indent_size = 2
|
|||||||
ij_smart_tabs = false
|
ij_smart_tabs = false
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
|
|
||||||
[packages/php/email-editor/**]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[packages/js/email-editor/**.{js,jsx,ts,tsx,scss}]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[*.php]
|
[*.php]
|
||||||
ij_php_align_key_value_pairs = false
|
ij_php_align_key_value_pairs = false
|
||||||
ij_php_align_multiline_chained_methods = false
|
ij_php_align_multiline_chained_methods = false
|
||||||
@ -62,4 +56,3 @@ ij_php_space_after_colon_in_return_type = true
|
|||||||
ij_php_space_before_else_keyword = true
|
ij_php_space_before_else_keyword = true
|
||||||
ij_php_for_statement_new_line_after_left_paren = true
|
ij_php_for_statement_new_line_after_left_paren = true
|
||||||
ij_php_class_brace_style = end_of_line
|
ij_php_class_brace_style = end_of_line
|
||||||
ij_php_comma_after_last_array_element = true
|
|
||||||
|
@ -39,15 +39,3 @@ e66c76133ec3ef667e382203426d91a4b4aa5174
|
|||||||
|
|
||||||
# Prettier autoformatting
|
# Prettier autoformatting
|
||||||
ab27eaee2df740c0add4331a7f8c115a87ecfa2b
|
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
|
|
||||||
|
161
.github/workflows/email-editor-package.yml
vendored
161
.github/workflows/email-editor-package.yml
vendored
@ -20,28 +20,30 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: mailpoet/vendor
|
path: mailpoet/vendor
|
||||||
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('mailpoet/composer.lock') }}-${{ hashFiles('mailpoet/composer.json') }}
|
key: ${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-${{ hashFiles('mailpoet/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-mailpoet-${{ matrix.php-version }}-
|
||||||
|
${{ runner.os }}-composer-mailpoet-
|
||||||
|
|
||||||
- name: Cache Composer vendor-prefixed dependencies for MailPoet
|
- name: Cache Composer vendor-prefixed dependencies for MailPoet
|
||||||
id: vendor-prefixed-cache
|
id: vendor-prefixed-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: mailpoet/vendor-prefixed
|
path: mailpoet/vendor-prefixed
|
||||||
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}-${{ hashFiles('mailpoet/prefixer/composer.json') }}
|
key: ${{ runner.os }}-vendor-prefixed-${{ matrix.php-version }}-${{ hashFiles('mailpoet/prefixer/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
- name: Cache Composer vendor for test environment
|
${{ runner.os }}-prefixer-mailpoet-${{ matrix.php-version }}-
|
||||||
id: composer-tests-env-cache
|
${{ runner.os }}-prefixer-mailpoet-
|
||||||
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
|
- name: Cache Composer dependencies for Email Editor
|
||||||
id: composer-email-editor-cache
|
id: composer-email-editor-cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: packages/php/email-editor/vendor
|
path: packages/php/email-editor/vendor
|
||||||
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}-${{ hashFiles('packages/php/email-editor/composer.json') }}
|
key: ${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-${{ hashFiles('packages/php/email-editor/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-email-editor-${{ matrix.php-version }}-
|
||||||
|
${{ runner.os }}-composer-email-editor-
|
||||||
|
|
||||||
- name: Set up PHP
|
- name: Set up PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@ -55,16 +57,9 @@ jobs:
|
|||||||
touch .env
|
touch .env
|
||||||
working-directory: mailpoet
|
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
|
# Install MailPoet dependencies only if the cache was not hit
|
||||||
- name: Install mailpoet dependencies
|
- name: Install mailpoet dependencies
|
||||||
if: |
|
if: steps.composer-mailpoet-cache.outputs.cache-hit != 'true'
|
||||||
steps.composer-mailpoet-cache.outputs.cache-hit != 'true' || steps.vendor-prefixed-cache.outputs.cache-hit != 'true'
|
|
||||||
run: ./tools/vendor/composer.phar install
|
run: ./tools/vendor/composer.phar install
|
||||||
working-directory: mailpoet
|
working-directory: mailpoet
|
||||||
|
|
||||||
@ -74,139 +69,11 @@ jobs:
|
|||||||
run: ../../../mailpoet/tools/vendor/composer.phar install
|
run: ../../../mailpoet/tools/vendor/composer.phar install
|
||||||
working-directory: packages/php/email-editor
|
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
|
# Run Email Editor unit tests
|
||||||
- name: Run email-editor package unit tests
|
- name: Run email-editor package unit tests
|
||||||
run: ../../../tests_env/vendor/bin/codecept build && ../../../mailpoet/tools/vendor/composer.phar unit-test
|
run: ./vendor/bin/codecept build && ../../../mailpoet/tools/vendor/composer.phar unit-test
|
||||||
working-directory: packages/php/email-editor
|
working-directory: packages/php/email-editor
|
||||||
|
|
||||||
- name: Run email-editor package integration tests
|
- name: Run email-editor package integration tests
|
||||||
run: ../../../mailpoet/tools/vendor/composer.phar integration-test
|
run: ../../../mailpoet/tools/vendor/composer.phar integration-test
|
||||||
working-directory: packages/php/email-editor
|
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/prefixer/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('packages/php/email-editor/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' || steps.vendor-prefixed-cache.outputs.cache-hit != 'true'
|
|
||||||
run: ./tools/vendor/composer.phar install
|
|
||||||
working-directory: mailpoet
|
|
||||||
|
|
||||||
# Install Email Editor dependencies only if the cache was not hit
|
|
||||||
- name: Install email-editor dependencies
|
|
||||||
if: steps.composer-email-editor-cache.outputs.cache-hit != 'true'
|
|
||||||
run: ../../../mailpoet/tools/vendor/composer.phar install
|
|
||||||
working-directory: packages/php/email-editor
|
|
||||||
|
|
||||||
- name: Install composer dependencies
|
|
||||||
run: ../../tools/vendor/composer.phar install
|
|
||||||
working-directory: mailpoet/tasks/phpstan
|
|
||||||
|
|
||||||
# Dump Email Editor autoload
|
|
||||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
|
||||||
- name: Dump email-editor autoload
|
|
||||||
run: ../../../mailpoet/tools/vendor/composer.phar dump-autoload
|
|
||||||
working-directory: packages/php/email-editor
|
|
||||||
|
|
||||||
# Dump MailPoet autoload
|
|
||||||
# This is needed to refresh classmap autoload when the composer cache is hit
|
|
||||||
- name: Dump MailPoet autoload
|
|
||||||
run: ./tools/vendor/composer.phar dump-autoload
|
|
||||||
working-directory: mailpoet
|
|
||||||
|
|
||||||
- name: Run code phpstan
|
|
||||||
run: ../../../mailpoet/tools/vendor/composer.phar phpstan -- --php-version=${{ matrix.php-version == '7.4' && '70400' || '80200' }}
|
|
||||||
working-directory: packages/php/email-editor
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,4 +10,3 @@ mailpoet-premium
|
|||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
/wordpress
|
/wordpress
|
||||||
packages/php/*/vendor
|
packages/php/*/vendor
|
||||||
tests_env/vendor
|
|
||||||
|
@ -4,5 +4,3 @@
|
|||||||
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
|
[ "$MP_GIT_HOOKS_ENABLE" != "true" ] && exit 0
|
||||||
|
|
||||||
installIfUpdates
|
installIfUpdates
|
||||||
|
|
||||||
./do cleanup:cached-files
|
|
||||||
|
@ -4,5 +4,3 @@
|
|||||||
|
|
||||||
npx lint-staged -c mailpoet/package.json --cwd mailpoet
|
npx lint-staged -c mailpoet/package.json --cwd mailpoet
|
||||||
npx lint-staged -c package.json
|
npx lint-staged -c package.json
|
||||||
npx lint-staged -c packages/js/email-editor/package.json --cwd packages/js/email-editor
|
|
||||||
npx lint-staged -c packages/php/email-editor/.lintstagedrc.json --cwd packages/php/email-editor
|
|
||||||
|
@ -25,5 +25,3 @@ vendor-prefixed
|
|||||||
/mailpoet/views
|
/mailpoet/views
|
||||||
/mailpoet-premium
|
/mailpoet-premium
|
||||||
/wordpress
|
/wordpress
|
||||||
/packages/php/email-editor
|
|
||||||
/packages/js/email-editor
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
clone:
|
|
||||||
git:
|
|
||||||
image: woodpeckerci/plugin-git
|
|
||||||
settings:
|
|
||||||
depth: 1
|
|
||||||
|
|
||||||
steps:
|
|
||||||
build:
|
|
||||||
image: node:current-bookworm-slim
|
|
||||||
commands:
|
|
||||||
- apt update
|
|
||||||
- apt install php php-symfony bash -y
|
|
||||||
- npm install pnpm
|
|
||||||
- cd mailpoet
|
|
||||||
- bash build.sh
|
|
||||||
- mkdir ../output
|
|
||||||
- mv mailpoet.zip ../output
|
|
||||||
- cd ..
|
|
||||||
|
|
||||||
release:
|
|
||||||
image: woodpeckerci/plugin-gitea-release:latest
|
|
||||||
settings:
|
|
||||||
base_url: https://git.cavemanon.xyz
|
|
||||||
api_key:
|
|
||||||
from_secret: releasesmithapikey
|
|
||||||
files: "output/"
|
|
||||||
prerelease: false
|
|
||||||
title: "${CI_COMMIT_TAG}"
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
@ -25,7 +25,7 @@ services:
|
|||||||
container_name: mp-wp
|
container_name: mp-wp
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: dev/php82/Dockerfile
|
dockerfile: dev/php81/Dockerfile
|
||||||
args:
|
args:
|
||||||
UID: ${UID:-1000}
|
UID: ${UID:-1000}
|
||||||
GID: ${GID:-1000}
|
GID: ${GID:-1000}
|
||||||
@ -54,7 +54,6 @@ services:
|
|||||||
- './pnpm-lock.yaml:/var/www/html/wp-content/plugins/pnpm-lock.yaml'
|
- './pnpm-lock.yaml:/var/www/html/wp-content/plugins/pnpm-lock.yaml'
|
||||||
- './pnpm-workspace.yaml:/var/www/html/wp-content/plugins/pnpm-workspace.yaml'
|
- './pnpm-workspace.yaml:/var/www/html/wp-content/plugins/pnpm-workspace.yaml'
|
||||||
- './patches:/var/www/html/wp-content/plugins/patches'
|
- './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:/var/www/html/wp-content/plugins/mailpoet'
|
||||||
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
||||||
- './packages:/var/www/html/wp-content/plugins/packages'
|
- './packages:/var/www/html/wp-content/plugins/packages'
|
||||||
|
2718
mailpoet/CHANGELOG.md
Normal file
2718
mailpoet/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,6 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
return $this->taskExecStack()
|
return $this->taskExecStack()
|
||||||
->stopOnFail()
|
->stopOnFail()
|
||||||
->exec('./tools/vendor/composer.phar install')
|
->exec('./tools/vendor/composer.phar install')
|
||||||
->exec('cd ../packages/php/email-editor && ../../../mailpoet/tools/vendor/composer.phar install && cd -')
|
|
||||||
->exec('cd .. && pnpm install --frozen-lockfile --prefer-offline')
|
->exec('cd .. && pnpm install --frozen-lockfile --prefer-offline')
|
||||||
->addCode([$this, 'cleanupCachedFiles'])
|
->addCode([$this, 'cleanupCachedFiles'])
|
||||||
->run();
|
->run();
|
||||||
@ -33,7 +32,6 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
return $this->taskExecStack()
|
return $this->taskExecStack()
|
||||||
->stopOnFail()
|
->stopOnFail()
|
||||||
->exec('./tools/vendor/composer.phar install')
|
->exec('./tools/vendor/composer.phar install')
|
||||||
->exec('cd ../packages/php/email-editor && ../../../mailpoet/tools/vendor/composer.phar install && cd -')
|
|
||||||
->addCode([$this, 'cleanupCachedFiles'])
|
->addCode([$this, 'cleanupCachedFiles'])
|
||||||
->run();
|
->run();
|
||||||
}
|
}
|
||||||
@ -210,7 +208,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
$env = ($opts['env']) ?
|
$env = ($opts['env']) ?
|
||||||
sprintf('./node_modules/.bin/cross-env NODE_ENV="%s"', $opts['env']) :
|
sprintf('./node_modules/.bin/cross-env NODE_ENV="%s"', $opts['env']) :
|
||||||
null;
|
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]) {
|
public function compileCss($opts = ['env' => null]) {
|
||||||
@ -332,7 +330,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testUnit(array $opts = ['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
|
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']) {
|
if ($opts['file']) {
|
||||||
$command .= ' -f ' . $opts['file'];
|
$command .= ' -f ' . $opts['file'];
|
||||||
@ -406,7 +404,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
return $this->testIntegration($opts);
|
return $this->testIntegration($opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAcceptance($opts = ['file' => null, 'skip-deps' => false, 'group' => null, 'timeout' => null, '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);
|
return $this->runTestsInContainer($opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +420,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
->option('env', 'US=' . $opts['us'])
|
->option('env', 'US=' . $opts['us'])
|
||||||
->option('env', 'PW=' . $opts['pw'])
|
->option('env', 'PW=' . $opts['pw'])
|
||||||
->option('env', 'K6_BROWSER_HEADLESS=' . ($opts['head'] ? 'false' : 'true'))
|
->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'])
|
->option('env', 'SCENARIO=' . $opts['scenario'])
|
||||||
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
||||||
->dir($dir)->run();
|
->dir($dir)->run();
|
||||||
@ -439,7 +437,6 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
->option('env', 'SCENARIO=' . $opts['scenario'])
|
->option('env', 'SCENARIO=' . $opts['scenario'])
|
||||||
->option('env', 'K6_CLOUD_TOKEN=' . getenv('K6_CLOUD_TOKEN'))
|
->option('env', 'K6_CLOUD_TOKEN=' . getenv('K6_CLOUD_TOKEN'))
|
||||||
->option('env', 'K6_CLOUD_ID=' . getenv('K6_CLOUD_ID'))
|
->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('env', 'K6_PROJECT_NAME=' . $opts['scenario'])
|
||||||
->option('out', 'cloud')
|
->option('out', 'cloud')
|
||||||
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
->arg($path ?? "$dir/tests/performance/scenarios.js")
|
||||||
@ -514,13 +511,13 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testFailedUnit() {
|
public function testFailedUnit() {
|
||||||
$this->_exec('../tests_env/vendor/bin/codecept build');
|
$this->_exec('vendor/bin/codecept build');
|
||||||
return $this->_exec('../tests_env/vendor/bin/codecept run unit -g failed');
|
return $this->_exec('vendor/bin/codecept run unit -g failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFailedIntegration() {
|
public function testFailedIntegration() {
|
||||||
$this->_exec('../tests_env/vendor/bin/codecept build');
|
$this->_exec('vendor/bin/codecept build');
|
||||||
return $this->_exec('../tests_env/vendor/bin/codecept run integration -g failed');
|
return $this->_exec('vendor/bin/codecept run integration -g failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function containerDump() {
|
public function containerDump() {
|
||||||
@ -686,21 +683,11 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function qaLintJavascript() {
|
public function qaLintJavascript() {
|
||||||
$collection = $this->collectionBuilder();
|
return $this->_exec('pnpm run check-types && pnpm run lint');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function qaLintCss() {
|
public function qaLintCss() {
|
||||||
$collection = $this->collectionBuilder();
|
return $this->_exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) {
|
public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) {
|
||||||
@ -761,7 +748,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
return $this->collectionBuilder()
|
return $this->collectionBuilder()
|
||||||
->taskExec(
|
->taskExec(
|
||||||
'./tasks/code_sniffer/vendor/bin/phpcbf ' .
|
'./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 ' .
|
'--runtime-set testVersion 7.4-8.2 ' .
|
||||||
$filePath . ' -n'
|
$filePath . ' -n'
|
||||||
)
|
)
|
||||||
@ -787,7 +774,7 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure Codeception support files are present to avoid invalid errors when running PHPStan
|
// 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
|
// PHPStan must be run out of main plugin directory to avoid its autoloading
|
||||||
// from vendor/autoload.php where some dev dependencies cause conflicts.
|
// from vendor/autoload.php where some dev dependencies cause conflicts.
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
.mailpoet_feature_announcement {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.mailpoet_feature_announcement_button {
|
||||||
|
height: 28px;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0 5px 1px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include respond-to(small-screen) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet_feature_announcement_icon {
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet_feature_announcement_dot:before {
|
||||||
|
background: #d54e21;
|
||||||
|
border-radius: 10px;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
right: -4px;
|
||||||
|
top: -4px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet_in_beamer_update_notice {
|
||||||
|
background: #f00;
|
||||||
|
bottom: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px 10px;
|
||||||
|
position: fixed;
|
||||||
|
right: -400px;
|
||||||
|
text-align: center;
|
||||||
|
transition: right 0.2s ease-in;
|
||||||
|
width: 400px;
|
||||||
|
z-index: 10000000000; // really has to be this high
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.beamer_show & {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -95,6 +95,10 @@ h1.title.mailpoet-newsletter-listing-heading {
|
|||||||
#mailpoet_editor_steps_heading {
|
#mailpoet_editor_steps_heading {
|
||||||
.mailpoet-top-bar {
|
.mailpoet-top-bar {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
|
.mailpoet-top-bar-beamer {
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,12 +161,3 @@ ul.sending-method-benefits {
|
|||||||
.mailpoet_install_premium_message {
|
.mailpoet_install_premium_message {
|
||||||
margin-bottom: $grid-gap-medium;
|
margin-bottom: $grid-gap-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailpoet-verify-key-button {
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mailpoet-premium-key-toggle {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
}
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
$beamer-dot-size: 8px;
|
||||||
|
|
||||||
.mailpoet-top-bar {
|
.mailpoet-top-bar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
@ -81,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailpoet-top-bar-tutorial {
|
.mailpoet-top-bar-beamer {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border: none;
|
border: none;
|
||||||
@ -94,7 +96,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
top: 4px;
|
|
||||||
width: 75px;
|
width: 75px;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@ -102,3 +103,15 @@
|
|||||||
width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
@ -87,6 +87,7 @@
|
|||||||
@import 'components-plugin/newsletter-types';
|
@import 'components-plugin/newsletter-types';
|
||||||
@import 'components-plugin/newsletter-template-styles';
|
@import 'components-plugin/newsletter-template-styles';
|
||||||
@import 'components-plugin/welcome-wizard';
|
@import 'components-plugin/welcome-wizard';
|
||||||
|
@import 'components-plugin/feature-announcement';
|
||||||
@import 'components-plugin/newsletter-congratulate';
|
@import 'components-plugin/newsletter-congratulate';
|
||||||
@import 'components-plugin/discounts';
|
@import 'components-plugin/discounts';
|
||||||
@import 'components-plugin/review-request';
|
@import 'components-plugin/review-request';
|
||||||
|
@ -53,7 +53,7 @@ function exportMixpanel() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
window.mailpoet_analytics_enabled &&
|
window.mailpoet_analytics_enabled &&
|
||||||
window.mailpoet_3rd_party_libs_enabled
|
window.MailPoet.libs3rdPartyEnabled
|
||||||
) {
|
) {
|
||||||
window.MailPoet.trackEvent = track;
|
window.MailPoet.trackEvent = track;
|
||||||
} else {
|
} else {
|
||||||
@ -104,30 +104,11 @@ function cacheEvent(forced, name, data, options, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initializeMixpanelWhenLoaded() {
|
export function initializeMixpanelWhenLoaded() {
|
||||||
const MAX_RETRY = 5;
|
if (typeof window.mixpanel === 'object') {
|
||||||
let intervalId;
|
|
||||||
let retryCount = 0;
|
|
||||||
|
|
||||||
const setupMixpanel = () => {
|
|
||||||
exportMixpanel();
|
exportMixpanel();
|
||||||
trackCachedEvents();
|
trackCachedEvents();
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof window.mixpanel === 'object') {
|
|
||||||
setupMixpanel();
|
|
||||||
} else {
|
} else {
|
||||||
intervalId = setInterval(() => {
|
setTimeout(initializeMixpanelWhenLoaded, 100);
|
||||||
if (typeof window.mixpanel === 'object') {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
setupMixpanel();
|
|
||||||
} else {
|
|
||||||
retryCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retryCount > MAX_RETRY) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
import { MailPoet } from 'mailpoet';
|
||||||
|
import { withFeatureAnnouncement } from './with-feature-announcement';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasNews: boolean;
|
||||||
|
onBeamerClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function FeatureAnnouncementComponent({ hasNews, onBeamerClick }: Props) {
|
||||||
|
const buttonClasses = classnames(
|
||||||
|
'button mailpoet_feature_announcement_button',
|
||||||
|
hasNews ? 'mailpoet_feature_announcement_dot' : '',
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="mailpoet_feature_announcement">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBeamerClick}
|
||||||
|
className={buttonClasses}
|
||||||
|
title={MailPoet.I18n.t('whatsNew')}
|
||||||
|
>
|
||||||
|
<span className="mailpoet_feature_announcement_icon dashicons dashicons-carrot" />
|
||||||
|
</button>
|
||||||
|
<span id="beamer-empty-element" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureAnnouncement = withFeatureAnnouncement(
|
||||||
|
FeatureAnnouncementComponent,
|
||||||
|
);
|
||||||
|
export { FeatureAnnouncement };
|
@ -0,0 +1,111 @@
|
|||||||
|
import { ComponentType, FC } from 'react';
|
||||||
|
import { MailPoet } from 'mailpoet';
|
||||||
|
import ReactStringReplace from 'react-string-replace';
|
||||||
|
import jQuery from 'jquery';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
|
interface FeatureAnnouncementWindow extends Window {
|
||||||
|
Beamer: {
|
||||||
|
show: () => void;
|
||||||
|
};
|
||||||
|
mailpoet_feature_announcement_has_news: boolean;
|
||||||
|
mailpoet_update_available: boolean;
|
||||||
|
beamer_config: {
|
||||||
|
product_id: string;
|
||||||
|
selector: string;
|
||||||
|
language: string;
|
||||||
|
callback: () => void;
|
||||||
|
filter?: string;
|
||||||
|
};
|
||||||
|
mailpoet_user_locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let window: FeatureAnnouncementWindow;
|
||||||
|
|
||||||
|
export const withFeatureAnnouncement = <P extends Record<string, unknown>>(
|
||||||
|
Component: ComponentType<P>,
|
||||||
|
): FC<Omit<P, 'hasNews' | 'onBeamerClick'>> => {
|
||||||
|
const isBeamerInitialized = () => typeof window.Beamer !== 'undefined';
|
||||||
|
let showDot = window.mailpoet_feature_announcement_has_news;
|
||||||
|
let beamerCallback;
|
||||||
|
|
||||||
|
function showPluginUpdateNotice() {
|
||||||
|
if (
|
||||||
|
!window.mailpoet_update_available ||
|
||||||
|
document.getElementById('mailpoet_update_notice')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updateMailPoetNotice = ReactStringReplace(
|
||||||
|
MailPoet.I18n.t('updateMailPoetNotice'),
|
||||||
|
/\[link\](.*?)\[\/link\]/,
|
||||||
|
(match) => `<a href="update-core.php">${match}</a>`,
|
||||||
|
).join('');
|
||||||
|
jQuery('#beamerOverlay').append(
|
||||||
|
`<p id="mailpoet_update_notice" class="mailpoet_in_beamer_update_notice">${updateMailPoetNotice}</p>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLastAnnouncementSeenValue() {
|
||||||
|
const data = { last_announcement_seen: Math.floor(Date.now() / 1000) };
|
||||||
|
void MailPoet.Ajax.post({
|
||||||
|
api_version: MailPoet.apiVersion,
|
||||||
|
endpoint: 'user_flags',
|
||||||
|
action: 'set',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBeamer() {
|
||||||
|
window.beamer_config = {
|
||||||
|
product_id: 'VvHbhYWy7118',
|
||||||
|
selector: '#beamer-empty-element',
|
||||||
|
language: window.mailpoet_user_locale,
|
||||||
|
callback: beamerCallback,
|
||||||
|
};
|
||||||
|
if (MailPoet.isWoocommerceActive) {
|
||||||
|
window.beamer_config.filter = 'woocommerce';
|
||||||
|
}
|
||||||
|
MailPoet.Modal.loading(true);
|
||||||
|
window.mailpoet_feature_announcement_has_news = false;
|
||||||
|
const s = document.createElement('script');
|
||||||
|
s.type = 'text/javascript';
|
||||||
|
s.src = 'https://app.getbeamer.com/js/beamer-embed.js';
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBeamer(event = null) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (!isBeamerInitialized()) {
|
||||||
|
loadBeamer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showDot = false;
|
||||||
|
beamerCallback = noop; // We show Beamer panel only on first callback after initialization
|
||||||
|
MailPoet.Modal.loading(false);
|
||||||
|
window.Beamer.show();
|
||||||
|
updateLastAnnouncementSeenValue();
|
||||||
|
showPluginUpdateNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
beamerCallback = () => {
|
||||||
|
if (!isBeamerInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showBeamer();
|
||||||
|
};
|
||||||
|
|
||||||
|
return function withFeatureAnnouncementRenderer({
|
||||||
|
...props
|
||||||
|
}: Omit<P, 'hasNews' | 'onBeamerClick'>) {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...(props as P)}
|
||||||
|
onBeamerClick={(e) => showBeamer(e)}
|
||||||
|
hasNews={showDot}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
@ -16,11 +16,7 @@ export type ApiError = {
|
|||||||
|
|
||||||
export const initializeApi = () => {
|
export const initializeApi = () => {
|
||||||
apiFetch.use((options, next) => {
|
apiFetch.use((options, next) => {
|
||||||
if (
|
if (options.path && options.path.startsWith('/wc-analytics/')) {
|
||||||
options.path &&
|
|
||||||
(options.path.startsWith('/wc-analytics/') ||
|
|
||||||
options.path.startsWith('/wp/v2/'))
|
|
||||||
) {
|
|
||||||
return apiFetch.createRootURLMiddleware(`${api.root}/`)(options, next);
|
return apiFetch.createRootURLMiddleware(`${api.root}/`)(options, next);
|
||||||
}
|
}
|
||||||
return apiFetch.createRootURLMiddleware(apiUrl)(options, next);
|
return apiFetch.createRootURLMiddleware(apiUrl)(options, next);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
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 { SlotFillProvider } from '@wordpress/components';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { registerTranslations } from 'common';
|
import { registerTranslations } from 'common';
|
||||||
@ -72,7 +72,7 @@ function Automations(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBarWithBoundary />
|
<TopBarWithBeamer />
|
||||||
<GlobalNotices />
|
<GlobalNotices />
|
||||||
<Notices />
|
<Notices />
|
||||||
<MssAccessNotices />
|
<MssAccessNotices />
|
||||||
|
@ -143,7 +143,6 @@ export function* activate() {
|
|||||||
return {
|
return {
|
||||||
type: 'ACTIVATE',
|
type: 'ACTIVATE',
|
||||||
automation: data?.data ?? automation,
|
automation: data?.data ?? automation,
|
||||||
saved: !!data?.data,
|
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,15 +41,11 @@ export function reducer(state: State, action): State {
|
|||||||
savedState: 'saved',
|
savedState: 'saved',
|
||||||
};
|
};
|
||||||
case 'ACTIVATE':
|
case 'ACTIVATE':
|
||||||
return action.saved
|
return {
|
||||||
? {
|
...state,
|
||||||
...state,
|
automationData: action.automation,
|
||||||
automationData: action.automation,
|
savedState: 'saved',
|
||||||
savedState: 'saved',
|
};
|
||||||
}
|
|
||||||
: {
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
case 'DEACTIVATE':
|
case 'DEACTIVATE':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -67,9 +67,7 @@ export function StatisticSeparator({
|
|||||||
// in an empty if/else branch. To calculate the total we need to subtract
|
// in an empty if/else branch. To calculate the total we need to subtract
|
||||||
// totalEntered of the sibling step from totalEntered of previousStep
|
// totalEntered of the sibling step from totalEntered of previousStep
|
||||||
const siblingStep = previousStep.next_steps.find((step) => step.id);
|
const siblingStep = previousStep.next_steps.find((step) => step.id);
|
||||||
const totalEnteredSibling = siblingStep
|
const totalEnteredSibling = calculateTotals(siblingStep.id);
|
||||||
? calculateTotals(siblingStep.id)
|
|
||||||
: 0;
|
|
||||||
const totalEnteredPrevious = completed[previousStep.id] ?? 0;
|
const totalEnteredPrevious = completed[previousStep.id] ?? 0;
|
||||||
totalEntered = totalEnteredPrevious - totalEnteredSibling;
|
totalEntered = totalEnteredPrevious - totalEnteredSibling;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { dispatch, select, useSelect } from '@wordpress/data';
|
import { dispatch, select, useSelect } from '@wordpress/data';
|
||||||
import { __ } from '@wordpress/i18n';
|
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 { Notices } from '../../../listing/components/notices';
|
||||||
import { Header } from './components/header';
|
import { Header } from './components/header';
|
||||||
import { Overview } from './components/overview';
|
import { Overview } from './components/overview';
|
||||||
@ -49,7 +49,7 @@ function TopBarWithBreadcrumb(): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopBarWithBoundary>
|
<TopBarWithBeamer>
|
||||||
<p className="mailpoet-automation-analytics-title">
|
<p className="mailpoet-automation-analytics-title">
|
||||||
<a href={MailPoet.urls.automationListing}>
|
<a href={MailPoet.urls.automationListing}>
|
||||||
{__('Automations', 'mailpoet')}
|
{__('Automations', 'mailpoet')}
|
||||||
@ -57,7 +57,7 @@ function TopBarWithBreadcrumb(): JSX.Element {
|
|||||||
› <strong>{automation.name}</strong>
|
› <strong>{automation.name}</strong>
|
||||||
<AutomationStatus status={automation.status} />
|
<AutomationStatus status={automation.status} />
|
||||||
</p>
|
</p>
|
||||||
</TopBarWithBoundary>
|
</TopBarWithBeamer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { Step } from '../../../../../editor/components/automation/types';
|
|||||||
import { storeName } from '../../../../../editor/store';
|
import { storeName } from '../../../../../editor/store';
|
||||||
|
|
||||||
const transactionalTriggers = [
|
const transactionalTriggers = [
|
||||||
'mailpoet:custom-trigger',
|
|
||||||
'woocommerce:order-status-changed',
|
'woocommerce:order-status-changed',
|
||||||
'woocommerce:order-created',
|
'woocommerce:order-created',
|
||||||
'woocommerce:order-completed',
|
'woocommerce:order-completed',
|
||||||
@ -16,9 +15,6 @@ const transactionalTriggers = [
|
|||||||
'woocommerce-subscriptions:subscription-status-changed',
|
'woocommerce-subscriptions:subscription-status-changed',
|
||||||
'woocommerce-subscriptions:trial-ended',
|
'woocommerce-subscriptions:trial-ended',
|
||||||
'woocommerce-subscriptions:trial-started',
|
'woocommerce-subscriptions:trial-started',
|
||||||
'woocommerce:buys-from-a-tag',
|
|
||||||
'woocommerce:buys-from-a-category',
|
|
||||||
'woocommerce:buys-a-product',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isTransactional(step: Step): boolean {
|
export function isTransactional(step: Step): boolean {
|
||||||
|
@ -7,7 +7,6 @@ import { step as AbandonedCartTrigger } from './steps/abandoned-cart';
|
|||||||
import { MailPoet } from '../../../mailpoet';
|
import { MailPoet } from '../../../mailpoet';
|
||||||
import { step as BuysAProductTrigger } from './steps/buys-a-product';
|
import { step as BuysAProductTrigger } from './steps/buys-a-product';
|
||||||
import { step as BuysFromACategory } from './steps/buys-from-a-category';
|
import { step as BuysFromACategory } from './steps/buys-from-a-category';
|
||||||
import { step as BuysFromATag } from './steps/buys-from-a-tag';
|
|
||||||
import { step as MadeAReview } from './steps/made-a-review';
|
import { step as MadeAReview } from './steps/made-a-review';
|
||||||
// Insert new imports here
|
// Insert new imports here
|
||||||
|
|
||||||
@ -22,7 +21,6 @@ export const initialize = (): void => {
|
|||||||
registerStepType(AbandonedCartTrigger);
|
registerStepType(AbandonedCartTrigger);
|
||||||
registerStepType(BuysAProductTrigger);
|
registerStepType(BuysAProductTrigger);
|
||||||
registerStepType(BuysFromACategory);
|
registerStepType(BuysFromACategory);
|
||||||
registerStepType(BuysFromATag);
|
|
||||||
registerStepType(MadeAReview);
|
registerStepType(MadeAReview);
|
||||||
// Insert new steps here
|
// Insert new steps here
|
||||||
};
|
};
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Search } from '@woocommerce/components';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { PanelBody } from '@wordpress/components';
|
|
||||||
import apiFetch from '@wordpress/api-fetch';
|
|
||||||
import { addQueryArgs } from '@wordpress/url';
|
|
||||||
import { dispatch, useSelect } from '@wordpress/data';
|
|
||||||
import { PlainBodyTitle } from '../../../../../editor/components';
|
|
||||||
import { storeName } from '../../../../../editor/store';
|
|
||||||
import { OrderStatusPanel } from '../../order-status-changed/edit/order-status-panel';
|
|
||||||
import autocompleter from './tag-autocompleter';
|
|
||||||
|
|
||||||
type Tag = {
|
|
||||||
key: string | number;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchTags(include: number[], callback: (tags: Tag[]) => void) {
|
|
||||||
const path = addQueryArgs('/wp/v2/product_tag/', { include });
|
|
||||||
const data: { id: number; name: string }[] = await apiFetch({
|
|
||||||
path,
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
callback(data.map((item) => ({ key: item?.id, label: item?.name })));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Edit(): JSX.Element {
|
|
||||||
const [current, setCurrent] = useState<Tag[]>([]);
|
|
||||||
const { selectedStep } = useSelect((select) => ({
|
|
||||||
selectedStep: select(storeName).getSelectedStep(),
|
|
||||||
}));
|
|
||||||
const tagIds: number[] = useMemo(
|
|
||||||
() => (selectedStep.args?.tag_ids as number[]) ?? [],
|
|
||||||
[selectedStep],
|
|
||||||
);
|
|
||||||
const [isBusy, setIsBusy] = useState(tagIds.length > 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isBusy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void fetchTags(tagIds, (tags: Tag[]) => {
|
|
||||||
setCurrent(tags);
|
|
||||||
setIsBusy(false);
|
|
||||||
});
|
|
||||||
}, [isBusy, tagIds]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PanelBody opened>
|
|
||||||
<PlainBodyTitle title={__('Tags', 'mailpoet')} />
|
|
||||||
<Search
|
|
||||||
disabled={isBusy}
|
|
||||||
type="custom"
|
|
||||||
autocompleter={autocompleter}
|
|
||||||
className={`mailpoet-product-search ${isBusy ? 'is-busy' : ''}`}
|
|
||||||
placeholder={__('Search for a tag', 'mailpoet')}
|
|
||||||
selected={current}
|
|
||||||
onChange={(items: Tag[]) => {
|
|
||||||
setCurrent(items);
|
|
||||||
void dispatch(storeName).updateStepArgs(
|
|
||||||
selectedStep.id,
|
|
||||||
'tag_ids',
|
|
||||||
items.map((item) => item.key),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
multiple
|
|
||||||
inlineTags
|
|
||||||
/>
|
|
||||||
</PanelBody>
|
|
||||||
|
|
||||||
<OrderStatusPanel
|
|
||||||
label={__('Order settings', 'mailpoet')}
|
|
||||||
showFrom={false}
|
|
||||||
showTo
|
|
||||||
toLabel={__('Order status', 'mailpoet')}
|
|
||||||
onChange={(status, property) => {
|
|
||||||
void dispatch(storeName).updateStepArgs(
|
|
||||||
selectedStep.id,
|
|
||||||
property,
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { addQueryArgs } from '@wordpress/url';
|
|
||||||
import apiFetch from '@wordpress/api-fetch';
|
|
||||||
import { AutoCompleter } from '@woocommerce/components/build-types/search/autocompleters';
|
|
||||||
|
|
||||||
const tagAutoCompleter: AutoCompleter = {
|
|
||||||
name: 'tags',
|
|
||||||
className: 'woocommerce-search__product-result',
|
|
||||||
options(search) {
|
|
||||||
const query = search
|
|
||||||
? {
|
|
||||||
search,
|
|
||||||
per_page: 10,
|
|
||||||
orderby: 'count',
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
return apiFetch({
|
|
||||||
path: addQueryArgs('/wp/v2/product_tag', query),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isDebounced: true,
|
|
||||||
getOptionIdentifier(tag) {
|
|
||||||
return tag.id as number;
|
|
||||||
},
|
|
||||||
getOptionKeywords(tag) {
|
|
||||||
return [tag.name] as string[];
|
|
||||||
},
|
|
||||||
getFreeTextOptions(query) {
|
|
||||||
const label = (
|
|
||||||
<span key="name" className="woocommerce-search__result-name">
|
|
||||||
{__('Search results', 'mailpoet')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
const titleOption = {
|
|
||||||
key: 'title',
|
|
||||||
label,
|
|
||||||
value: { id: query, name: query },
|
|
||||||
};
|
|
||||||
|
|
||||||
return [titleOption];
|
|
||||||
},
|
|
||||||
getOptionLabel(tag) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key="name"
|
|
||||||
className="woocommerce-search__result-name"
|
|
||||||
aria-label={tag.name}
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// This is slightly different than gutenberg/Autocomplete, we don't support different methods
|
|
||||||
// of replace/insertion, so we can just return the value.
|
|
||||||
getOptionCompletion(tag) {
|
|
||||||
const value = {
|
|
||||||
key: tag.id,
|
|
||||||
label: tag.name,
|
|
||||||
};
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tagAutoCompleter;
|
|
@ -1,26 +0,0 @@
|
|||||||
export function Icon(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M5.73173 4.00134C5.8081 3.03517 6.61579 2.27551 7.60166 2.27551C8.58753 2.27551 9.39522 3.03517 9.47159 4.00134H10.1904C10.7479 4.00134 11.2033 4.45681 11.2033 5.01426V10.1917C11.2033 10.7492 10.7479 11.2047 10.1904 11.2047H5.01292C4.45547 11.2047 4 10.7492 4 10.1917V5.01426C4 4.45681 4.45547 4.00134 5.01292 4.00134H5.73173ZM7.60166 3.43843C7.36389 3.43843 7.15161 3.55664 7.02145 3.73861C6.96586 3.81633 6.92553 3.90533 6.90474 4.00134H8.29858C8.27779 3.90533 8.23746 3.81633 8.18187 3.73861C8.05171 3.55664 7.83943 3.43843 7.60166 3.43843ZM5.16291 5.16426V9.65285C5.16291 9.86763 5.33703 10.0417 5.55181 10.0417H9.65151C9.86629 10.0417 10.0404 9.86763 10.0404 9.65285V5.16426H9.47749V5.87717C9.47749 6.19732 9.21618 6.45863 8.89603 6.45863C8.57589 6.45863 8.31458 6.19732 8.31458 5.87717V5.16426H6.88875V5.87717C6.88875 6.19732 6.62743 6.45863 6.30729 6.45863C5.98714 6.45863 5.72583 6.19732 5.72583 5.87717V5.16426H5.16291Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M13.5858 4.58722C13.9609 4.21215 14.4696 4.00143 15 4.00143H18C18.5304 4.00143 19.0391 4.21215 19.4142 4.58722C19.7893 4.96229 20 5.471 20 6.00143V9.00143C20 9.53187 19.7893 10.0406 19.4142 10.4156C19.0391 10.7907 18.5304 11.0014 18 11.0014H15C14.4696 11.0014 13.9609 10.7907 13.5858 10.4156C13.2107 10.0406 13 9.53187 13 9.00143V6.00143C13 5.471 13.2107 4.96229 13.5858 4.58722ZM15 5.50143H18C18.1326 5.50143 18.2598 5.55411 18.3536 5.64788C18.4473 5.74165 18.5 5.86883 18.5 6.00143V9.00143C18.5 9.13404 18.4473 9.26122 18.3536 9.35499C18.2598 9.44876 18.1326 9.50143 18 9.50143H15C14.8674 9.50143 14.7402 9.44876 14.6464 9.35499C14.5527 9.26122 14.5 9.13404 14.5 9.00143V6.00143C14.5 5.86883 14.5527 5.74165 14.6464 5.64788C14.7402 5.55411 14.8674 5.50143 15 5.50143Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M13.5858 13.5844C13.9609 13.2093 14.4696 12.9986 15 12.9986H18C18.5304 12.9986 19.0391 13.2093 19.4142 13.5844C19.7893 13.9594 20 14.4681 20 14.9986V17.9986C20 18.529 19.7893 19.0377 19.4142 19.4128C19.0391 19.7879 18.5304 19.9986 18 19.9986H15C14.4696 19.9986 13.9609 19.7879 13.5858 19.4128C13.2107 19.0377 13 18.529 13 17.9986V14.9986C13 14.4681 13.2107 13.9594 13.5858 13.5844ZM15 14.4986H18C18.1326 14.4986 18.2598 14.5513 18.3536 14.645C18.4473 14.7388 18.5 14.866 18.5 14.9986V17.9986C18.5 18.1312 18.4473 18.2584 18.3536 18.3521C18.2598 18.4459 18.1326 18.4986 18 18.4986H15C14.8674 18.4986 14.7402 18.4459 14.6464 18.3521C14.5527 18.2584 14.5 18.1312 14.5 17.9986V14.9986C14.5 14.866 14.5527 14.7388 14.6464 14.645C14.7402 14.5513 14.8674 14.4986 15 14.4986Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M4.58579 13.5844C4.21071 13.9594 4 14.4681 4 14.9986V17.9986C4 18.529 4.21071 19.0377 4.58579 19.4128C4.96086 19.7879 5.46957 19.9986 6 19.9986H9C9.53043 19.9986 10.0391 19.7879 10.4142 19.4128C10.7893 19.0377 11 18.529 11 17.9986V14.9986C11 14.4681 10.7893 13.9594 10.4142 13.5844C10.0391 13.2093 9.53043 12.9986 9 12.9986H6C5.46957 12.9986 4.96086 13.2093 4.58579 13.5844ZM9 14.4986H6C5.86739 14.4986 5.74021 14.5513 5.64645 14.645C5.55268 14.7388 5.5 14.866 5.5 14.9986V17.9986C5.5 18.1312 5.55268 18.2584 5.64645 18.3521C5.74021 18.4459 5.86739 18.4986 6 18.4986H9C9.13261 18.4986 9.25979 18.4459 9.35355 18.3521C9.44732 18.2584 9.5 18.1312 9.5 17.9986V14.9986C9.5 14.866 9.44732 14.7388 9.35355 14.645C9.25979 14.5513 9.13261 14.4986 9 14.4986Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { StepType } from '../../../../editor/store';
|
|
||||||
import { Edit } from './edit';
|
|
||||||
import { Icon } from './icon';
|
|
||||||
|
|
||||||
const keywords = [
|
|
||||||
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('tag', 'mailpoet'),
|
|
||||||
// translators: verb, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('buy', 'mailpoet'),
|
|
||||||
// translators: verb, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('purchase', 'mailpoet'),
|
|
||||||
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('ecommerce', 'mailpoet'),
|
|
||||||
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('woocommerce', 'mailpoet'),
|
|
||||||
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('product', 'mailpoet'),
|
|
||||||
// translators: noun, used as a search keyword for "Customer buys from a tag" trigger
|
|
||||||
__('order', 'mailpoet'),
|
|
||||||
];
|
|
||||||
export const step: StepType = {
|
|
||||||
key: 'woocommerce:buys-from-a-tag',
|
|
||||||
group: 'triggers',
|
|
||||||
title: () => __('Customer buys from a tag', 'mailpoet'),
|
|
||||||
description: () =>
|
|
||||||
__(
|
|
||||||
'Start the automation when a customer buys a product from a tag.',
|
|
||||||
'mailpoet',
|
|
||||||
),
|
|
||||||
|
|
||||||
subtitle: () => __('Trigger', 'mailpoet'),
|
|
||||||
keywords,
|
|
||||||
foreground: '#2271b1',
|
|
||||||
background: '#f0f6fc',
|
|
||||||
icon: () => <Icon />,
|
|
||||||
edit: () => <Edit />,
|
|
||||||
} as const;
|
|
@ -4,27 +4,27 @@ import { Icon } from './icon';
|
|||||||
import { PremiumModalForStepEdit } from '../../../../components/premium-modal-steps-edit';
|
import { PremiumModalForStepEdit } from '../../../../components/premium-modal-steps-edit';
|
||||||
|
|
||||||
const keywords = [
|
const keywords = [
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('review', 'mailpoet'),
|
__('review', 'mailpoet'),
|
||||||
// translators: verb, used as a search keyword for "Customer posts a review" trigger
|
// translators: verb, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('buy', 'mailpoet'),
|
__('buy', 'mailpoet'),
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('comment', 'mailpoet'),
|
__('comment', 'mailpoet'),
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('ecommerce', 'mailpoet'),
|
__('ecommerce', 'mailpoet'),
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('woocommerce', 'mailpoet'),
|
__('woocommerce', 'mailpoet'),
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('product', 'mailpoet'),
|
__('product', 'mailpoet'),
|
||||||
// translators: noun, used as a search keyword for "Customer posts a review" trigger
|
// translators: noun, used as a search keyword for "Customer makes a review" trigger
|
||||||
__('order', 'mailpoet'),
|
__('order', 'mailpoet'),
|
||||||
];
|
];
|
||||||
export const step: StepType = {
|
export const step: StepType = {
|
||||||
key: 'woocommerce:made-a-review',
|
key: 'woocommerce:made-a-review',
|
||||||
group: 'triggers',
|
group: 'triggers',
|
||||||
title: () => __('Customer posts a review', 'mailpoet'),
|
title: () => __('Customer makes a review', 'mailpoet'),
|
||||||
description: () =>
|
description: () =>
|
||||||
__('Start the automation when a customer posts a review.', 'mailpoet'),
|
__('Start the automation when a customer makes a review.', 'mailpoet'),
|
||||||
|
|
||||||
subtitle: () => __('Trigger', 'mailpoet'),
|
subtitle: () => __('Trigger', 'mailpoet'),
|
||||||
keywords,
|
keywords,
|
||||||
|
@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
|
|||||||
import { registerTranslations } from 'common';
|
import { registerTranslations } from 'common';
|
||||||
import { automationTemplateCategories, automationTemplates } from './config';
|
import { automationTemplateCategories, automationTemplates } from './config';
|
||||||
import { initializeApi } from '../api';
|
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 { FromScratchButton } from './components/from-scratch';
|
||||||
import { BackButton, PageHeader } from '../../common/page-header';
|
import { BackButton, PageHeader } from '../../common/page-header';
|
||||||
import { MailPoet } from '../../mailpoet';
|
import { MailPoet } from '../../mailpoet';
|
||||||
@ -50,7 +50,7 @@ function Templates(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mailpoet-main-container">
|
<div className="mailpoet-main-container">
|
||||||
<TopBarWithBoundary />
|
<TopBarWithBeamer />
|
||||||
<PageHeader
|
<PageHeader
|
||||||
heading={__('Start with a template', 'mailpoet')}
|
heading={__('Start with a template', 'mailpoet')}
|
||||||
headingPrefix={
|
headingPrefix={
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { PlacesType } from 'react-tooltip';
|
import { PlacesType } from 'react-tooltip';
|
||||||
import { Badge } from './badge';
|
import { Badge } from './badge';
|
||||||
|
|
||||||
@ -15,48 +15,48 @@ const getStats = () => ({
|
|||||||
badgeRanges: [30, 10, 0],
|
badgeRanges: [30, 10, 0],
|
||||||
badgeTypes: ['excellent', 'good', 'critical'],
|
badgeTypes: ['excellent', 'good', 'critical'],
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Excellent open rate
|
||||||
excellent: sprintf(__('above %s%%', 'mailpoet'), 30),
|
excellent: __('above 30%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Good open rate
|
||||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 10, 30),
|
good: __('between 10 and 30%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Critical open rate
|
||||||
critical: sprintf(__('below %s%%', 'mailpoet'), 10),
|
critical: __('under 10%', 'mailpoet'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
badgeRanges: [3, 1, 0],
|
badgeRanges: [3, 1, 0],
|
||||||
badgeTypes: ['excellent', 'good', 'critical'],
|
badgeTypes: ['excellent', 'good', 'critical'],
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Excellent click rate
|
||||||
excellent: sprintf(__('above %s%%', 'mailpoet'), 3),
|
excellent: __('above 3%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Good click rate
|
||||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 1, 3),
|
good: __('between 1 and 3%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Critical click rate
|
||||||
critical: sprintf(__('below %s%%', 'mailpoet'), 1),
|
critical: __('under 1%', 'mailpoet'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bounced: {
|
bounced: {
|
||||||
badgeRanges: [1.5, 0.5, 0],
|
badgeRanges: [1.5, 0.5, 0],
|
||||||
badgeTypes: ['critical', 'good', 'excellent'],
|
badgeTypes: ['critical', 'good', 'excellent'],
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Excellent bounce rate
|
||||||
excellent: sprintf(__('below %s%%', 'mailpoet'), 0.5),
|
excellent: __('below 0.5%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Good bounce rate
|
||||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 0.5, 1.5),
|
good: __('between 0.5% and 1.5%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Critical bounce rate
|
||||||
critical: sprintf(__('above %s%%', 'mailpoet'), 1.5),
|
critical: __('above 1.5%', 'mailpoet'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unsubscribed: {
|
unsubscribed: {
|
||||||
badgeRanges: [0.7, 0.3, 0],
|
badgeRanges: [0.7, 0.3, 0],
|
||||||
badgeTypes: ['critical', 'good', 'excellent'],
|
badgeTypes: ['critical', 'good', 'excellent'],
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
// translators: Shows a percentage range, "below 10%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Excellent unsubscribe rate
|
||||||
excellent: sprintf(__('below %s%%', 'mailpoet'), 0.3),
|
excellent: __('Below 0.3%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "between 10% and 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Good unsubscribe rate
|
||||||
good: sprintf(__('between %s%% and %s%%', 'mailpoet'), 0.3, 0.7),
|
good: __('between 0.3% and 0.7%', 'mailpoet'),
|
||||||
// translators: Shows a percentage range, "above 30%". Used in contexts like open, click, bounce, or unsubscribe rates.
|
// translators: Critical unsubscribe rate
|
||||||
critical: sprintf(__('above %s%%', 'mailpoet'), 0.7),
|
critical: __('above 0.7%', 'mailpoet'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,40 +1,21 @@
|
|||||||
import { _x } from '@wordpress/i18n';
|
import { Input } from 'common/index';
|
||||||
import { Button, Input } from 'common/index';
|
|
||||||
import { useAction, useSelector } from 'settings/store/hooks';
|
import { useAction, useSelector } from 'settings/store/hooks';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type KeyInputPropType = {
|
type KeyInputPropType = {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isFullWidth?: boolean;
|
isFullWidth?: boolean;
|
||||||
forceRevealed?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KeyInput({
|
export function KeyInput({
|
||||||
placeholder,
|
placeholder,
|
||||||
isFullWidth = false,
|
isFullWidth = false,
|
||||||
forceRevealed = false,
|
|
||||||
}: KeyInputPropType) {
|
}: KeyInputPropType) {
|
||||||
const state = useSelector('getKeyActivationState')();
|
const state = useSelector('getKeyActivationState')();
|
||||||
const setState = useAction('updateKeyActivationState');
|
const setState = useAction('updateKeyActivationState');
|
||||||
const [isRevealed, setIsRevealed] = useState(false);
|
|
||||||
const inputType = forceRevealed || isRevealed ? 'text' : 'password';
|
|
||||||
const toggleButton = !forceRevealed && (
|
|
||||||
<Button
|
|
||||||
className="mailpoet-premium-key-toggle"
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={() => setIsRevealed(!isRevealed)}
|
|
||||||
>
|
|
||||||
{isRevealed
|
|
||||||
? // translators: Used as a button to show or hide the premium key
|
|
||||||
_x('Hide', 'verb', 'mailpoet')
|
|
||||||
: // translators: Used as a button to show or hide the premium key
|
|
||||||
_x('Show', 'verb', 'mailpoet')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type={inputType}
|
type="text"
|
||||||
id="mailpoet_premium_key"
|
id="mailpoet_premium_key"
|
||||||
name="premium[premium_key]"
|
name="premium[premium_key]"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@ -48,7 +29,6 @@ export function KeyInput({
|
|||||||
key: event.target.value.trim() || null,
|
key: event.target.value.trim() || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
iconEnd={toggleButton}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { action } from '_storybook/action';
|
||||||
import { TopBar } from '../top-bar';
|
import { TopBar } from '../top-bar';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -16,7 +17,7 @@ export function TopBarWithoutChildren() {
|
|||||||
left: '0px',
|
left: '0px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopBar />
|
<TopBar hasNews={false} onBeamerClick={action('beamer click')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -33,7 +34,7 @@ export function TopBarWithoutChildrenWithNews() {
|
|||||||
left: '0px',
|
left: '0px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopBar />
|
<TopBar hasNews onBeamerClick={action('beamer click')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { action } from '_storybook/action';
|
||||||
import { TopBar } from '../top-bar';
|
import { TopBar } from '../top-bar';
|
||||||
import { Button } from '../../button/button';
|
import { Button } from '../../button/button';
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ export function TopBarWithChildren() {
|
|||||||
left: '0px',
|
left: '0px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopBar>
|
<TopBar hasNews={false} onBeamerClick={action('beamer click')}>
|
||||||
<Button>Button</Button>
|
<Button>Button</Button>
|
||||||
</TopBar>
|
</TopBar>
|
||||||
</div>
|
</div>
|
||||||
|
22
mailpoet/assets/js/src/common/top-bar/beamer-icon.tsx
Normal file
22
mailpoet/assets/js/src/common/top-bar/beamer-icon.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export function BeamerIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M6 17V14.5002L6.00101 14.4997C7.10049 13.9995 7.90003 12.8998 8 11.7002V9.50016C8 7.96615 8.78223 6.62769 10.0309 5.95845C10.3288 5.79879 10.6533 5.67721 11 5.60016C10.9 5.50016 10.8 5.30016 10.8 5.10016C10.8 5.01146 10.8109 4.92493 10.8315 4.84189C10.95 4.36325 11.3887 4.00017 11.9 4.00017C11.9334 4.00017 11.9672 3.99696 12 4C12.0332 3.99688 12.0661 4.00004 12.1 4.00004C12.6113 4.00004 13.05 4.36313 13.1685 4.84177C13.1891 4.92481 13.2 5.01133 13.2 5.10004C13.2 5.30004 13.1 5.50004 13 5.60004C13.3467 5.67709 13.6712 5.79867 13.9691 5.95833C15.2178 6.62757 16 7.96602 16 9.50004V11.7C16.1 12.9 16.9 14.0002 18 14.5002V17.0002L6 17ZM17 16H7V15.148C8.2117 14.4742 8.98499 13.2218 9.09655 11.8831L9.1 9.50004C9.1 8.15301 9.99683 6.94434 11.3676 6.56356L11.7403 6.46006L12.0003 6.40228L12.2553 6.45896L12.6324 6.56368C14.0032 6.94446 14.9 8.15313 14.9 9.50016L14.9035 11.8832C15.015 13.2219 15.7883 14.4743 17 15.1481V16ZM12.0001 5.3601C12.221 5.3601 12.4001 5.18102 12.4001 4.9601C12.4001 4.73919 12.221 4.5601 12.0001 4.5601C11.7792 4.5601 11.6001 4.73919 11.6001 4.9601C11.6001 5.18102 11.7792 5.3601 12.0001 5.3601Z"
|
||||||
|
fill="#2C3338"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.8303 19C14.0017 18.7054 14.1 18.3637 14.1 18L9.9 18.0002C9.9 18.3638 9.99834 18.7055 10.1697 19.0002C10.5169 19.5968 11.1636 20.0002 11.9 20.0002C11.9339 20.0002 11.9677 19.9993 12.0012 19.9976C12.034 19.9992 12.0669 20 12.1 20C12.8364 20 13.4831 19.5967 13.8303 19Z"
|
||||||
|
fill="#2C3338"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -1,25 +1,62 @@
|
|||||||
import { ReactNode } from 'react';
|
import { 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 { HideScreenOptions } from 'common/hide-screen-options/hide-screen-options';
|
||||||
import { MailPoetLogoResponsive } from './mailpoet-logo-responsive';
|
import { MailPoetLogoResponsive } from './mailpoet-logo-responsive';
|
||||||
|
import { BeamerIcon } from './beamer-icon';
|
||||||
import { ScreenOptionsFix } from './screen-options-fix';
|
import { ScreenOptionsFix } from './screen-options-fix';
|
||||||
import { withBoundary } from '../error-boundary';
|
import { withBoundary } from '../error-boundary';
|
||||||
|
import { MailPoet } from '../../mailpoet';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
hasNews?: boolean;
|
||||||
|
onBeamerClick?: () => void;
|
||||||
logoWithLink?: boolean;
|
logoWithLink?: boolean;
|
||||||
hideScreenOptions?: boolean;
|
hideScreenOptions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
children,
|
children,
|
||||||
|
hasNews,
|
||||||
|
onBeamerClick,
|
||||||
logoWithLink = true,
|
logoWithLink = true,
|
||||||
hideScreenOptions = false,
|
hideScreenOptions = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const buttonClasses = classnames(
|
||||||
|
'mailpoet-top-bar-beamer',
|
||||||
|
hasNews ? 'mailpoet-top-bar-beamer-dot' : '',
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="mailpoet-top-bar">
|
<div className="mailpoet-top-bar">
|
||||||
<MailPoetLogoResponsive withLink={logoWithLink} />
|
<MailPoetLogoResponsive withLink={logoWithLink} />
|
||||||
<div className="mailpoet-top-bar-children">{children}</div>
|
<div className="mailpoet-top-bar-children">{children}</div>
|
||||||
<div className="mailpoet-flex-grow" />
|
<div className="mailpoet-flex-grow" />
|
||||||
|
{onBeamerClick && MailPoet.libs3rdPartyEnabled && (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={onBeamerClick}
|
||||||
|
className={buttonClasses}
|
||||||
|
title={__('What’s new', 'mailpoet')}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (
|
||||||
|
['keydown', 'keypress'].includes(event.type) &&
|
||||||
|
['Enter', ' '].includes(event.key)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
onBeamerClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BeamerIcon />
|
||||||
|
<span>{__('Updates', 'mailpoet')}</span>
|
||||||
|
</a>
|
||||||
|
<span id="beamer-empty-element" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ScreenOptionsFix />
|
<ScreenOptionsFix />
|
||||||
{hideScreenOptions && <HideScreenOptions />}
|
{hideScreenOptions && <HideScreenOptions />}
|
||||||
</div>
|
</div>
|
||||||
@ -27,4 +64,4 @@ export function TopBar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
TopBar.displayName = 'TopBar';
|
TopBar.displayName = 'TopBar';
|
||||||
export const TopBarWithBoundary = withBoundary(TopBar);
|
export const TopBarWithBeamer = withFeatureAnnouncement(withBoundary(TopBar));
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"inserter": false,
|
"inserter": false,
|
||||||
"lock": false
|
"lock": false
|
||||||
},
|
},
|
||||||
"apiVersion": 3,
|
"apiVersion": 2,
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"logo": {
|
"logo": {
|
@ -2,14 +2,10 @@ import { registerBlockType } from '@wordpress/blocks';
|
|||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
||||||
import { PanelBody, RadioControl, Icon } from '@wordpress/components';
|
import { PanelBody, RadioControl, Icon } from '@wordpress/components';
|
||||||
import { useState } from 'react';
|
import { useSelect } from '@wordpress/data';
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
|
import { storeName } from '../../engine/store/constants';
|
||||||
import MailPoetIcon from './mailpoet-icon';
|
import MailPoetIcon from './mailpoet-icon';
|
||||||
import { PremiumModal } from '../../common/premium-modal';
|
|
||||||
import './style.scss';
|
|
||||||
|
|
||||||
const getCdnUrl = () => window.mailpoet_cdn_url;
|
|
||||||
const getPremiumPluginStatus = () => window.mailpoet_premium_active;
|
|
||||||
|
|
||||||
function LogoImage({
|
function LogoImage({
|
||||||
logoSrc,
|
logoSrc,
|
||||||
@ -18,31 +14,8 @@ function LogoImage({
|
|||||||
logoSrc: string;
|
logoSrc: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [isModalOpened, setIsModalOpened] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<img src={logoSrc} style={style} alt="Powered by MailPoet" width="100px" />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mailpoet-email-footer-credit"
|
|
||||||
onClick={() => setIsModalOpened(true)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={logoSrc}
|
|
||||||
style={style}
|
|
||||||
alt="Powered by MailPoet"
|
|
||||||
width="100px"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{!!isModalOpened && (
|
|
||||||
<PremiumModal onRequestClose={() => setIsModalOpened(false)}>
|
|
||||||
{__(
|
|
||||||
'A MailPoet logo will appear in the footer of all emails sent with the free version of MailPoet.',
|
|
||||||
'mailpoet',
|
|
||||||
)}
|
|
||||||
</PremiumModal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +27,13 @@ function Edit({
|
|||||||
setAttributes: (value: { logo: string }) => void;
|
setAttributes: (value: { logo: string }) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const blockProps = useBlockProps();
|
const blockProps = useBlockProps();
|
||||||
|
const { cdnUrl, isPremiumPluginActive } = useSelect(
|
||||||
const cdnUrl = getCdnUrl();
|
(select) => ({
|
||||||
const isPremiumPluginActive = getPremiumPluginStatus();
|
cdnUrl: select(storeName).getCdnUrl(),
|
||||||
|
isPremiumPluginActive: select(storeName).isPremiumPluginActive(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
if (isPremiumPluginActive) {
|
if (isPremiumPluginActive) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -91,6 +67,15 @@ function Edit({
|
|||||||
) as unknown as string,
|
) as unknown as string,
|
||||||
value: 'light',
|
value: 'light',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<LogoImage
|
||||||
|
logoSrc={`${cdnUrl}email-editor/logo-dark.png`}
|
||||||
|
style={{ background: '#000000' }}
|
||||||
|
/>
|
||||||
|
) as unknown as string,
|
||||||
|
value: 'dark',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setAttributes({
|
setAttributes({
|
@ -0,0 +1,21 @@
|
|||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables Styles for button
|
||||||
|
* Currently we are not able to read these styles in renderer
|
||||||
|
*/
|
||||||
|
function enhanceButtonBlock() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/change-button',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/button') {
|
||||||
|
return { ...settings, styles: [] };
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enhanceButtonBlock };
|
@ -0,0 +1,28 @@
|
|||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch layout to reduced flex email layout
|
||||||
|
* Email render engine can't handle full flex layout se we need to switch to reduced flex layout
|
||||||
|
*/
|
||||||
|
function enhanceButtonsBlock() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/change-buttons',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/buttons') {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
layout: false, // disable block editor's layouts
|
||||||
|
__experimentalEmailFlexLayout: true, // enable MailPoet's reduced flex email layout
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enhanceButtonsBlock };
|
@ -0,0 +1,25 @@
|
|||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
function enhanceColumnBlock() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/change-column',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/column') {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
background: {
|
||||||
|
backgroundImage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enhanceColumnBlock };
|
@ -0,0 +1,87 @@
|
|||||||
|
import { InspectorControls } from '@wordpress/block-editor';
|
||||||
|
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
|
||||||
|
const columnsEditCallback = createHigherOrderComponent(
|
||||||
|
(BlockEdit) =>
|
||||||
|
function alterBlocksEdits(props) {
|
||||||
|
if (props.name !== 'core/columns') {
|
||||||
|
return <BlockEdit {...props} />;
|
||||||
|
}
|
||||||
|
// CSS sets opacity by the class is-disabled by the toggle component from the Gutenberg package
|
||||||
|
// To deactivating the input we use CSS pointer-events because we want to avoid JavaScript hacks
|
||||||
|
const deactivateToggleCss = `
|
||||||
|
.components-panel__body .components-toggle-control .components-form-toggle { opacity: 0.3; }
|
||||||
|
.components-panel__body .components-toggle-control .components-form-toggle__input { pointer-events: none; }
|
||||||
|
.components-panel__body .components-toggle-control label { pointer-events: none; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BlockEdit {...props} />
|
||||||
|
<InspectorControls>
|
||||||
|
<style>{deactivateToggleCss}</style>
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'columnsEditCallback',
|
||||||
|
);
|
||||||
|
|
||||||
|
function deactivateStackOnMobile() {
|
||||||
|
addFilter(
|
||||||
|
'editor.BlockEdit',
|
||||||
|
'mailpoet-email-editor/deactivate-stack-on-mobile',
|
||||||
|
columnsEditCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables layout support for columns and column blocks because
|
||||||
|
* the default layout `flex` add gaps between columns that it is not possible to support in emails.
|
||||||
|
*/
|
||||||
|
function disableColumnsLayout() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/disable-columns-layout',
|
||||||
|
(settings, name) => {
|
||||||
|
if (name === 'core/columns' || name === 'core/column') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
layout: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceColumnsBlock() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/change-columns',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/columns') {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
background: {
|
||||||
|
backgroundImage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { deactivateStackOnMobile, disableColumnsLayout, enhanceColumnsBlock };
|
@ -1,11 +1,8 @@
|
|||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { addFilter } from '@wordpress/hooks';
|
import { addFilter } from '@wordpress/hooks';
|
||||||
import {
|
import {
|
||||||
Block as WPBlock,
|
Block as WPBlock,
|
||||||
BlockSupports as WPBlockSupports,
|
BlockSupports as WPBlockSupports,
|
||||||
} from '@wordpress/blocks/index';
|
} from '@wordpress/blocks';
|
||||||
|
|
||||||
// Extend the BlockSupports type to include shadow
|
// Extend the BlockSupports type to include shadow
|
||||||
// The shadow is not included in WP6.4 but it is in WP6.5
|
// The shadow is not included in WP6.4 but it is in WP6.5
|
||||||
@ -18,19 +15,19 @@ type Block = WPBlock & { supports?: BlockSupports };
|
|||||||
* Currently we are not able to read these styles in renderer
|
* Currently we are not able to read these styles in renderer
|
||||||
*/
|
*/
|
||||||
function alterSupportConfiguration() {
|
function alterSupportConfiguration() {
|
||||||
addFilter(
|
addFilter(
|
||||||
'blocks.registerBlockType',
|
'blocks.registerBlockType',
|
||||||
'mailpoet-email-editor/block-support',
|
'mailpoet-email-editor/block-support',
|
||||||
( settings: Block ) => {
|
(settings: Block) => {
|
||||||
if ( settings.supports?.shadow ) {
|
if (settings.supports?.shadow) {
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
supports: { ...settings.supports, shadow: false },
|
supports: { ...settings.supports, shadow: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return settings;
|
return settings;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { alterSupportConfiguration };
|
export { alterSupportConfiguration };
|
@ -0,0 +1,30 @@
|
|||||||
|
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',
|
||||||
|
),
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
layout: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { disableGroupVariations };
|
@ -0,0 +1,61 @@
|
|||||||
|
import { InspectorControls } from '@wordpress/block-editor';
|
||||||
|
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
const imageEditCallback = createHigherOrderComponent(
|
||||||
|
(BlockEdit) =>
|
||||||
|
function alterBlocksEdits(props) {
|
||||||
|
if (props.name !== 'core/image') {
|
||||||
|
return <BlockEdit {...props} />;
|
||||||
|
}
|
||||||
|
// Because we cannot support displaying the modal with image after clicking in the email we have to hide the toggle
|
||||||
|
const deactivateToggleCss = `
|
||||||
|
.components-tools-panel .components-toggle-control { display: none; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BlockEdit {...props} />
|
||||||
|
<InspectorControls>
|
||||||
|
<style>{deactivateToggleCss}</style>
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'imageEditCallback',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because CSS property filter is not supported in almost 50% of email clients we have to disable it
|
||||||
|
*/
|
||||||
|
function disableImageFilter() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/deactivate-image-filter',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/image') {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
supports: {
|
||||||
|
...settings.supports,
|
||||||
|
filter: {
|
||||||
|
duetone: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideExpandOnClick() {
|
||||||
|
addFilter(
|
||||||
|
'editor.BlockEdit',
|
||||||
|
'mailpoet-email-editor/hide-expand-on-click',
|
||||||
|
imageEditCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { hideExpandOnClick, disableImageFilter };
|
@ -0,0 +1,58 @@
|
|||||||
|
import { addFilter } from '@wordpress/hooks';
|
||||||
|
import { Block } from '@wordpress/blocks';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
function Placeholder({ layoutClassNames }) {
|
||||||
|
const blockProps = useBlockProps({ className: layoutClassNames });
|
||||||
|
return (
|
||||||
|
<div {...blockProps}>
|
||||||
|
<p>{__('This is the Content block.', 'mailpoet')}</p>
|
||||||
|
<p>
|
||||||
|
{__(
|
||||||
|
'It will display all the blocks in the email content, which might be only simple text paragraphs. You can enrich your message with images, incorporate data through tables, explore different layout designs with columns, or use any other block type.',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curried function to add a custom placeholder to the post content block, or just use the original Edit component.
|
||||||
|
function PostContentEdit(OriginalEditComponent) {
|
||||||
|
return function Edit({
|
||||||
|
context,
|
||||||
|
__unstableLayoutClassNames: layoutClassNames,
|
||||||
|
}) {
|
||||||
|
const { postId: contextPostId, postType: contextPostType } = context;
|
||||||
|
const hasContent = contextPostId && contextPostType;
|
||||||
|
|
||||||
|
if (hasContent) {
|
||||||
|
return (
|
||||||
|
<OriginalEditComponent
|
||||||
|
{...{ context, __unstableLayoutClassNames: layoutClassNames }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Placeholder layoutClassNames={layoutClassNames} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhancePostContentBlock() {
|
||||||
|
addFilter(
|
||||||
|
'blocks.registerBlockType',
|
||||||
|
'mailpoet-email-editor/change-post-content',
|
||||||
|
(settings: Block, name) => {
|
||||||
|
if (name === 'core/post-content') {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
edit: PostContentEdit(settings.edit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enhancePostContentBlock };
|
@ -0,0 +1,18 @@
|
|||||||
|
import { unregisterFormatType } from '@wordpress/rich-text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable Rich text formats we currently cannot support
|
||||||
|
* 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 code - Not well formatted
|
||||||
|
unregisterFormatType('core/code');
|
||||||
|
|
||||||
|
// remove support for Language - Not supported for now
|
||||||
|
unregisterFormatType('core/language');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { disableCertainRichTextFormats };
|
30
mailpoet/assets/js/src/email-editor/engine/blocks/index.ts
Normal file
30
mailpoet/assets/js/src/email-editor/engine/blocks/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { registerCoreBlocks } from '@wordpress/block-library';
|
||||||
|
import { enhanceColumnBlock } from './core/column';
|
||||||
|
import {
|
||||||
|
disableColumnsLayout,
|
||||||
|
deactivateStackOnMobile,
|
||||||
|
enhanceColumnsBlock,
|
||||||
|
} from './core/columns';
|
||||||
|
import { enhancePostContentBlock } from './core/post-content';
|
||||||
|
import { disableGroupVariations } from './core/group';
|
||||||
|
import { disableImageFilter, hideExpandOnClick } from './core/image';
|
||||||
|
import { disableCertainRichTextFormats } from './core/rich-text';
|
||||||
|
import { enhanceButtonBlock } from './core/button';
|
||||||
|
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();
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is simplified version of the original one from @wordpress/editor package.
|
||||||
|
* The original component can be found here: https://github.com/WordPress/gutenberg/blob/46446b853d740c309c0675c7bf2ca4170a618c42/packages/editor/src/components/autosave-monitor/index.js
|
||||||
|
* The main reason for the own solution is that the original component needs to initialize the @wordpress/editor 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 { saveEditedEmail } = useDispatch(storeName);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let autosaveTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
if (hasEdits && autosaveInterval > 0) {
|
||||||
|
autosaveTimer = setTimeout(() => {
|
||||||
|
void saveEditedEmail();
|
||||||
|
}, autosaveInterval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autosaveTimer) {
|
||||||
|
clearTimeout(autosaveTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [hasEdits, autosaveInterval, saveEditedEmail]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* WordPress dependencies
|
||||||
|
*/
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ErrorBoundary,
|
||||||
|
PostLockedModal,
|
||||||
|
EditorProvider,
|
||||||
|
} from '@wordpress/editor';
|
||||||
|
import { useMemo } from '@wordpress/element';
|
||||||
|
import { SlotFillProvider, Spinner } from '@wordpress/components';
|
||||||
|
import { Post, store as coreStore } from '@wordpress/core-data';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
import { unlock } from '../../../lock-unlock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { Layout } from './layout';
|
||||||
|
import { useNavigateToEntityRecord } from '../../hooks/use-navigate-to-entity-record';
|
||||||
|
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need to fetch patterns ourselves. Automatic fetching of patterns is currently a private functionality
|
||||||
|
* that is not available in EditorProvider but only in the ExperimentalBlockEditorProvider which
|
||||||
|
* is not exported in the public components nor private components.
|
||||||
|
*/
|
||||||
|
const blockPatterns = useSelect(
|
||||||
|
(select) => {
|
||||||
|
const { hasFinishedResolution, getBlockPatternsForPostType } = unlock(
|
||||||
|
select(coreStore),
|
||||||
|
);
|
||||||
|
const patterns = getBlockPatternsForPostType(
|
||||||
|
currentPost.postType,
|
||||||
|
) as Post[];
|
||||||
|
return hasFinishedResolution('getBlockPatterns') ? patterns : undefined;
|
||||||
|
},
|
||||||
|
[currentPost.postType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const editorSettings = useMemo(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
() => ({
|
||||||
|
...settings,
|
||||||
|
onNavigateToEntityRecord,
|
||||||
|
onNavigateToPreviousEntityRecord,
|
||||||
|
defaultRenderingMode: 'template-locked',
|
||||||
|
supportsTemplateMode: true,
|
||||||
|
__experimentalBlockPatterns: blockPatterns,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
settings,
|
||||||
|
onNavigateToEntityRecord,
|
||||||
|
onNavigateToPreviousEntityRecord,
|
||||||
|
blockPatterns,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!post || (currentPost.postType !== 'wp_template' && !template)) {
|
||||||
|
return (
|
||||||
|
<div className="spinner-container">
|
||||||
|
<Spinner style={{ width: '80px', height: '80px' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlotFillProvider>
|
||||||
|
<EditorProvider
|
||||||
|
settings={editorSettings}
|
||||||
|
post={post}
|
||||||
|
initialEdits={initialEdits}
|
||||||
|
useSubRegistry={false}
|
||||||
|
// @ts-expect-error __unstableTemplate is not in the EditorProvider props in the installed version of packages
|
||||||
|
__unstableTemplate={template}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* @ts-expect-error ErrorBoundary type is incorrect there is no onError */}
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Layout />
|
||||||
|
<PostLockedModal />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</EditorProvider>
|
||||||
|
</SlotFillProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
@import '~@wordpress/base-styles/colors';
|
||||||
|
|
||||||
|
#mailpoet-email-editor {
|
||||||
|
.editor-header__toolbar {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.editor-header__center {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific styles for the component EmailTypeInfo
|
||||||
|
// Styles are based on the Block Card component from Gutenberg block editor
|
||||||
|
.mailpoet-email-sidebar__email-type-info {
|
||||||
|
.components-panel__row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-type-info__icon {
|
||||||
|
flex: 0 0 24px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-type-info__content {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-editor__settings-panel {
|
||||||
|
.mailpoet-settings-panel__subject .components-base-control__label {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.components-external-link {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-settings-panel__help {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.components-text {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-settings-panel__preview-text .components-base-control__label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-settings-panel__preview-text-length {
|
||||||
|
color: $black;
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-settings-panel__preview-text-length-warning {
|
||||||
|
color: $alert-yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-settings-panel__preview-text-length-error {
|
||||||
|
color: $alert-red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-post-visual-editor {
|
||||||
|
line-height: 1.4; /* Recommended line-height that is also used in the email editor */
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
|
||||||
|
-ms-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */
|
||||||
|
word-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-editor__email_layout_wrapper {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-editor__email_content_wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix for mobile preview height
|
||||||
|
&.is-mobile-preview {
|
||||||
|
> div {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.editor-styles-wrapper {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the advanced settings in the sidebar. This panel is not used in the email editor at this moment.
|
||||||
|
.block-editor-block-inspector__advanced {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export * from './editor';
|
export * from './editor';
|
||||||
export * from './layout';
|
export * from './layout';
|
@ -0,0 +1,128 @@
|
|||||||
|
import { BlockSelectionClearer } from '@wordpress/block-editor';
|
||||||
|
import { UnsavedChangesWarning } 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 { useRef } from '@wordpress/element';
|
||||||
|
|
||||||
|
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 { VisualEditor } from './visual-editor/visual-editor';
|
||||||
|
|
||||||
|
import { TemplateSelection } from '../template-select';
|
||||||
|
|
||||||
|
export function Layout() {
|
||||||
|
const {
|
||||||
|
isFullscreenActive,
|
||||||
|
isSidebarOpened,
|
||||||
|
initialSettings,
|
||||||
|
previewDeviceType,
|
||||||
|
isInserterSidebarOpened,
|
||||||
|
isListviewSidebarOpened,
|
||||||
|
canUserEditMedia,
|
||||||
|
hasFixedToolbar,
|
||||||
|
focusMode,
|
||||||
|
styles,
|
||||||
|
} = 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).getDeviceType(),
|
||||||
|
canUserEditMedia: select(coreStore).canUser('create', 'media'),
|
||||||
|
hasFixedToolbar: select(storeName).isFeatureActive('fixedToolbar'),
|
||||||
|
focusMode: select(storeName).isFeatureActive('focusMode'),
|
||||||
|
styles: select(storeName).getStyles(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { toggleInserterSidebar } = useDispatch(storeName);
|
||||||
|
|
||||||
|
const [emailCss] = useEmailCss();
|
||||||
|
const className = classnames('edit-post-layout', {
|
||||||
|
'is-sidebar-opened': isSidebarOpened,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
|
||||||
|
// 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="visual-editor__email_content_wrapper">
|
||||||
|
<VisualEditor
|
||||||
|
disableIframe={false}
|
||||||
|
styles={[...settings.styles, ...emailCss]}
|
||||||
|
className="has-global-padding"
|
||||||
|
contentRef={contentRef}
|
||||||
|
iframeProps={{}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BlockSelectionClearer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
sidebar={<ComplementaryArea.Slot scope={storeName} />}
|
||||||
|
secondarySidebar={
|
||||||
|
(isInserterSidebarOpened && <InserterSidebar />) ||
|
||||||
|
(isListviewSidebarOpened && <ListviewSidebar />)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* Rendering Warning component here ensures that the warning is displayed under the border configuration. */}
|
||||||
|
<BlockCompatibilityWarnings />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* WordPress dependencies
|
||||||
|
*/
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
|
import { store as editorStore } from '@wordpress/editor';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that:
|
||||||
|
*
|
||||||
|
* - Displays a 'Edit your template to edit this block' dialog when the user
|
||||||
|
* is focusing on editing email content and double clicks on a disabled
|
||||||
|
* template block.
|
||||||
|
*
|
||||||
|
* @see https://github.com/WordPress/gutenberg/blob/c754c783a9004db678fcfebd9a21a22820f2115c/packages/editor/src/components/visual-editor/edit-template-blocks-notification.js
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {import('react').RefObject<HTMLElement>} props.contentRef Ref to the block
|
||||||
|
* editor iframe canvas.
|
||||||
|
*/
|
||||||
|
export default function EditTemplateBlocksNotification({ contentRef }) {
|
||||||
|
const { onNavigateToEntityRecord, templateId } = useSelect((select) => {
|
||||||
|
// @ts-expect-error getCurrentTemplateId is missing in types.
|
||||||
|
const { getEditorSettings, getCurrentTemplateId } = select(editorStore);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// @ts-expect-error onNavigateToEntityRecord is missing in EditorSettings.
|
||||||
|
onNavigateToEntityRecord: getEditorSettings().onNavigateToEntityRecord,
|
||||||
|
templateId: getCurrentTemplateId(),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDblClick = (event) => {
|
||||||
|
if (!event.target.classList.contains('is-root-container')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canvas = contentRef.current;
|
||||||
|
canvas?.addEventListener('dblclick', handleDblClick);
|
||||||
|
return () => {
|
||||||
|
canvas?.removeEventListener('dblclick', handleDblClick);
|
||||||
|
};
|
||||||
|
}, [contentRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isDialogOpen}
|
||||||
|
confirmButtonText={__('Edit template')}
|
||||||
|
onConfirm={() => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
onNavigateToEntityRecord({
|
||||||
|
postId: templateId,
|
||||||
|
postType: 'wp_template',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsDialogOpen(false)}
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{__(
|
||||||
|
'You’ve tried to select a block that is part of a template, which may be used on other emails. Would you like to edit the template?',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* WordPress dependencies
|
||||||
|
*/
|
||||||
|
import { useRefEffect } from '@wordpress/compose';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../../../../lock-unlock';
|
||||||
|
|
||||||
|
const DISTANCE_THRESHOLD = 500;
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceFromRect(x: number, y: number, rect) {
|
||||||
|
const dx = x - clamp(x, Number(rect.left), Number(rect.right));
|
||||||
|
const dy = y - clamp(y, Number(rect.top), Number(rect.bottom));
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSelectNearestEditableBlock({
|
||||||
|
isEnabled = true,
|
||||||
|
} = {}) {
|
||||||
|
const { getEnabledClientIdsTree, getBlockName, getBlockOrder } = unlock(
|
||||||
|
useSelect(blockEditorStore),
|
||||||
|
);
|
||||||
|
const { selectBlock } = useDispatch(blockEditorStore);
|
||||||
|
|
||||||
|
return useRefEffect(
|
||||||
|
(element: Element) => {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectNearestEditableBlock = (x, y) => {
|
||||||
|
const editableBlockClientIds = getEnabledClientIdsTree().flatMap(
|
||||||
|
({ clientId }) => {
|
||||||
|
const blockName = getBlockName(clientId);
|
||||||
|
if (blockName === 'core/template-part') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (blockName === 'core/post-content') {
|
||||||
|
const innerBlocks = getBlockOrder(clientId);
|
||||||
|
if (innerBlocks.length) {
|
||||||
|
return innerBlocks as string[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [clientId as string];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract the nearest client ID
|
||||||
|
const nearestClientIdData = editableBlockClientIds.reduce(
|
||||||
|
(acc: { clientId: string; distance: number }, clientId: string) => {
|
||||||
|
const block = element.querySelector(`[data-block="${clientId}"]`);
|
||||||
|
if (!block) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const rect = block.getBoundingClientRect();
|
||||||
|
const distance = distanceFromRect(Number(x), Number(y), rect);
|
||||||
|
|
||||||
|
if (distance < acc.distance && distance < DISTANCE_THRESHOLD) {
|
||||||
|
return { clientId, distance };
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ clientId: null, distance: Number.POSITIVE_INFINITY },
|
||||||
|
);
|
||||||
|
|
||||||
|
const nearestClientId = nearestClientIdData?.clientId || '';
|
||||||
|
if (nearestClientId) {
|
||||||
|
void selectBlock(nearestClientId as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
const shouldSelect =
|
||||||
|
event.target === element ||
|
||||||
|
event.target.classList.contains('is-root-container');
|
||||||
|
if (shouldSelect) {
|
||||||
|
selectNearestEditableBlock(event.clientX, event.clientY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener('click', handleClick);
|
||||||
|
return () => element.removeEventListener('click', handleClick);
|
||||||
|
},
|
||||||
|
[isEnabled],
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* WordPress dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import {
|
||||||
|
BlockList,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
__unstableUseTypewriter as useTypewriter,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
RecursionProvider,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
privateApis as blockEditorPrivateApis,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
__experimentalUseResizeCanvas as useResizeCanvas,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { useRef } from '@wordpress/element';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useMergeRefs } from '@wordpress/compose';
|
||||||
|
import { store as editorStore } from '@wordpress/editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import EditTemplateBlocksNotification from './edit-template-blocks-notification';
|
||||||
|
import useSelectNearestEditableBlock from './use-select-nearest-editable-block';
|
||||||
|
import { unlock } from '../../../../lock-unlock';
|
||||||
|
|
||||||
|
export const TEMPLATE_POST_TYPE = 'wp_template';
|
||||||
|
export const TEMPLATE_PART_POST_TYPE = 'wp_template_part';
|
||||||
|
export const PATTERN_POST_TYPE = 'wp_block';
|
||||||
|
export const NAVIGATION_POST_TYPE = 'wp_navigation';
|
||||||
|
|
||||||
|
const { ExperimentalBlockCanvas: BlockCanvas, useFlashEditableBlocks } = unlock(
|
||||||
|
blockEditorPrivateApis,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These post types have a special editor where they don't allow you to fill the title
|
||||||
|
* and they don't apply the layout styles.
|
||||||
|
*/
|
||||||
|
const DESIGN_POST_TYPES = [
|
||||||
|
PATTERN_POST_TYPE,
|
||||||
|
TEMPLATE_POST_TYPE,
|
||||||
|
NAVIGATION_POST_TYPE,
|
||||||
|
TEMPLATE_PART_POST_TYPE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied and simplified from https://github.com/WordPress/gutenberg/blob/c754c783a9004db678fcfebd9a21a22820f2115c/packages/editor/src/components/visual-editor/index.js
|
||||||
|
* Simplifications:
|
||||||
|
* - doesn't support post-only mode for no design post types. We use the post-only mode only for templates
|
||||||
|
* - removed logic for layout styles. We currently pass them among all styles in styles property
|
||||||
|
* - removed support for post title
|
||||||
|
* - removed support for zooming
|
||||||
|
* - removed support for resizing
|
||||||
|
* @todo Need to fix layout so that we support Align settings properly
|
||||||
|
*/
|
||||||
|
export function VisualEditor({
|
||||||
|
// Ideally as we unify post and site editors, we won't need these props
|
||||||
|
styles,
|
||||||
|
disableIframe = false,
|
||||||
|
iframeProps,
|
||||||
|
contentRef,
|
||||||
|
className,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
renderingMode,
|
||||||
|
wrapperBlockName,
|
||||||
|
wrapperUniqueId,
|
||||||
|
deviceType,
|
||||||
|
isFocusedEntity,
|
||||||
|
layout,
|
||||||
|
} = useSelect((select) => {
|
||||||
|
const {
|
||||||
|
getCurrentPostId,
|
||||||
|
getCurrentPostType,
|
||||||
|
getEditorSettings,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
getRenderingMode,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
getDeviceType,
|
||||||
|
} = select(editorStore);
|
||||||
|
const postTypeSlug = getCurrentPostType();
|
||||||
|
const checkRenderingMode = getRenderingMode();
|
||||||
|
let checkWrapperBlockName;
|
||||||
|
|
||||||
|
if (postTypeSlug === PATTERN_POST_TYPE) {
|
||||||
|
checkWrapperBlockName = 'core/block';
|
||||||
|
} else if (checkRenderingMode === 'post-only') {
|
||||||
|
checkWrapperBlockName = 'core/post-content';
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorSettings = getEditorSettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderingMode: checkRenderingMode,
|
||||||
|
isDesignPostType: DESIGN_POST_TYPES.includes(postTypeSlug),
|
||||||
|
// Post template fetch returns a 404 on classic themes, which
|
||||||
|
// messes with e2e tests, so check it's a block theme first.
|
||||||
|
wrapperBlockName: checkWrapperBlockName,
|
||||||
|
wrapperUniqueId: getCurrentPostId(),
|
||||||
|
deviceType: getDeviceType() as string,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
isFocusedEntity: !!editorSettings.onNavigateToPreviousEntityRecord,
|
||||||
|
postType: postTypeSlug,
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
isPreview: editorSettings.__unstableIsPreviewMode,
|
||||||
|
// @ts-expect-error There are no types for the experimental features settings.
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
layout: editorSettings.__experimentalFeatures.layout,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deviceStyles = useResizeCanvas(deviceType);
|
||||||
|
|
||||||
|
// We want to use the same layout.
|
||||||
|
const blockListLayout = layout;
|
||||||
|
|
||||||
|
const localRef = useRef();
|
||||||
|
const typewriterRef = useTypewriter();
|
||||||
|
const newContentRef = useMergeRefs([
|
||||||
|
localRef,
|
||||||
|
contentRef,
|
||||||
|
renderingMode === 'post-only' ? typewriterRef : null,
|
||||||
|
useFlashEditableBlocks({
|
||||||
|
isEnabled: renderingMode === 'template-locked',
|
||||||
|
}),
|
||||||
|
useSelectNearestEditableBlock({
|
||||||
|
isEnabled: renderingMode === 'template-locked',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const shouldIframe =
|
||||||
|
!disableIframe || ['Tablet', 'Mobile'].includes(deviceType);
|
||||||
|
const containerWidth =
|
||||||
|
deviceType === 'Desktop' ? (layout.contentSize as string) : '100%';
|
||||||
|
|
||||||
|
const iframeStyles = [
|
||||||
|
...((styles as string[]) ?? []),
|
||||||
|
{
|
||||||
|
css: `.is-root-container{display:flow-root; width:${containerWidth}; margin: 0 auto;box-sizing: border-box;}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
'editor-visual-editor',
|
||||||
|
// this class is here for backward compatibility reasons.
|
||||||
|
'edit-post-visual-editor',
|
||||||
|
className as string,
|
||||||
|
{
|
||||||
|
'has-padding': isFocusedEntity,
|
||||||
|
'is-iframed': shouldIframe,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BlockCanvas
|
||||||
|
shouldIframe={shouldIframe}
|
||||||
|
contentRef={newContentRef}
|
||||||
|
styles={iframeStyles}
|
||||||
|
height="100%"
|
||||||
|
iframeProps={{
|
||||||
|
...iframeProps,
|
||||||
|
style: {
|
||||||
|
...iframeProps?.style,
|
||||||
|
...deviceStyles,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecursionProvider
|
||||||
|
blockName={wrapperBlockName}
|
||||||
|
uniqueId={wrapperUniqueId}
|
||||||
|
>
|
||||||
|
<BlockList
|
||||||
|
className={classnames(
|
||||||
|
`is-${deviceType.toLowerCase()}-preview`,
|
||||||
|
'has-global-padding', // Ensures that padding is applied at the top level
|
||||||
|
)}
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
layout={blockListLayout}
|
||||||
|
dropZoneElement={
|
||||||
|
// When iframed, pass in the html element of the iframe to
|
||||||
|
// ensure the drop zone extends to the edges of the iframe.
|
||||||
|
// @ts-expect-error No types for this exist yet.
|
||||||
|
disableIframe ? localRef.current : localRef.current?.parentNode
|
||||||
|
}
|
||||||
|
__unstableDisableDropZone={
|
||||||
|
// In template preview mode, disable drop zones at the root of the template.
|
||||||
|
renderingMode === 'template-locked'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{renderingMode === 'template-locked' && (
|
||||||
|
<EditTemplateBlocksNotification contentRef={localRef} />
|
||||||
|
)}
|
||||||
|
</RecursionProvider>
|
||||||
|
</BlockCanvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import { useRef } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
VisuallyHidden,
|
||||||
|
__experimentalText as Text,
|
||||||
|
TextControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { chevronDown } from '@wordpress/icons';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
// @see https://github.com/WordPress/gutenberg/blob/5e0ffdbc36cb2e967dfa6a6b812a10a2e56a598f/packages/edit-post/src/components/header/document-actions/index.js
|
||||||
|
|
||||||
|
export function CampaignName() {
|
||||||
|
const { showIconLabels } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
showIconLabels: select(storeName).isFeatureActive('showIconLabels'),
|
||||||
|
postId: select(storeName).getEmailPostId(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [emailTitle = '', setTitle] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'title',
|
||||||
|
);
|
||||||
|
|
||||||
|
const titleRef = useRef(null);
|
||||||
|
return (
|
||||||
|
<div ref={titleRef} className="mailpoet-email-editor-campaign-name">
|
||||||
|
<Dropdown
|
||||||
|
popoverProps={{
|
||||||
|
placement: 'bottom',
|
||||||
|
anchor: titleRef.current,
|
||||||
|
}}
|
||||||
|
contentClassName="mailpoet-email-editor-campaign-name__dropdown"
|
||||||
|
renderToggle={({ isOpen, onToggle }) => (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="mailpoet-email-campaign-name__link"
|
||||||
|
>
|
||||||
|
<Text size="body" as="h1">
|
||||||
|
<VisuallyHidden as="span">
|
||||||
|
{__('Editing email:', 'mailpoet')}
|
||||||
|
</VisuallyHidden>
|
||||||
|
{emailTitle}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="mailpoet-email-campaign-name__toggle"
|
||||||
|
icon={chevronDown}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={onToggle}
|
||||||
|
label={__('Change campaign name', 'mailpoet')}
|
||||||
|
>
|
||||||
|
{showIconLabels && __('Rename', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
renderContent={() => (
|
||||||
|
<div className="mailpoet-email-editor-email-title-edit">
|
||||||
|
<TextControl
|
||||||
|
label={__('Campaign name', 'mailpoet')}
|
||||||
|
value={emailTitle}
|
||||||
|
onChange={(newTitle) => {
|
||||||
|
setTitle(newTitle);
|
||||||
|
}}
|
||||||
|
name="campaign_name"
|
||||||
|
help={__(
|
||||||
|
`Name your email campaign to indicate its purpose. This would only be visible to you and not shown to your subscribers.`,
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
import { useRef, useState } from '@wordpress/element';
|
||||||
|
import { PinnedItems } from '@wordpress/interface';
|
||||||
|
import { Button, ToolbarItem as WpToolbarItem } from '@wordpress/components';
|
||||||
|
import {
|
||||||
|
NavigableToolbar,
|
||||||
|
BlockToolbar as WPBlockToolbar,
|
||||||
|
store as blockEditorStore,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { store as coreDataStore } from '@wordpress/core-data';
|
||||||
|
// @ts-expect-error DocumentBar types are not available
|
||||||
|
import { DocumentBar, store as editorStore } from '@wordpress/editor';
|
||||||
|
import { store as preferencesStore } from '@wordpress/preferences';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { plus, listView, undo, redo, next, previous } from '@wordpress/icons';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
import { MoreMenu } from './more-menu';
|
||||||
|
import { PreviewDropdown } from '../preview';
|
||||||
|
import { SaveButton } from './save-button';
|
||||||
|
import { CampaignName } from './campaign-name';
|
||||||
|
import { SendButton } from './send-button';
|
||||||
|
|
||||||
|
import { unlock } from '../../../lock-unlock';
|
||||||
|
|
||||||
|
// Build type for ToolbarItem contains only "as" and "children" properties but it takes all props from
|
||||||
|
// component passed to "as" property (in this case Button). So as fix for TS errors we need to pass all props from Button to ToolbarItem.
|
||||||
|
// We should be able to remove this fix when ToolbarItem will be fixed in Gutenberg.
|
||||||
|
const ToolbarItem = WpToolbarItem as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentProps<typeof WpToolbarItem> &
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Definition of BlockToolbar in currently installed Gutenberg packages (wp-6.4) is missing hideDragHandle prop
|
||||||
|
// After updating to newer version of Gutenberg we should be able to remove this fix
|
||||||
|
const BlockToolbar = WPBlockToolbar as React.FC<
|
||||||
|
React.ComponentProps<typeof WPBlockToolbar> & {
|
||||||
|
hideDragHandle?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const inserterButton = useRef();
|
||||||
|
const listviewButton = useRef();
|
||||||
|
const undoButton = useRef();
|
||||||
|
const redoButton = useRef();
|
||||||
|
|
||||||
|
const [isBlockToolsCollapsed, setIsBlockToolsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const { toggleInserterSidebar, toggleListviewSidebar } =
|
||||||
|
useDispatch(storeName);
|
||||||
|
const { undo: undoAction, redo: redoAction } = useDispatch(coreDataStore);
|
||||||
|
const {
|
||||||
|
isInserterSidebarOpened,
|
||||||
|
isListviewSidebarOpened,
|
||||||
|
isFixedToolbarActive,
|
||||||
|
isBlockSelected,
|
||||||
|
hasUndo,
|
||||||
|
hasRedo,
|
||||||
|
hasDocumentNavigationHistory,
|
||||||
|
} = useSelect((select) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { getEditorSettings: _getEditorSettings } = unlock(
|
||||||
|
select(editorStore),
|
||||||
|
);
|
||||||
|
const editorSettings = _getEditorSettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInserterSidebarOpened: select(storeName).isInserterSidebarOpened(),
|
||||||
|
isListviewSidebarOpened: select(storeName).isListviewSidebarOpened(),
|
||||||
|
isFixedToolbarActive: select(preferencesStore).get(
|
||||||
|
'core',
|
||||||
|
'fixedToolbar',
|
||||||
|
),
|
||||||
|
isBlockSelected: !!select(blockEditorStore).getBlockSelectionStart(),
|
||||||
|
hasUndo: select(coreDataStore).hasUndo(),
|
||||||
|
hasRedo: select(coreDataStore).hasRedo(),
|
||||||
|
hasDocumentNavigationHistory:
|
||||||
|
!!editorSettings.onNavigateToPreviousEntityRecord,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const preventDefault = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const shortLabelInserter = !isInserterSidebarOpened
|
||||||
|
? __('Add', 'mailpoet')
|
||||||
|
: __('Close', 'mailpoet');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-header edit-post-header">
|
||||||
|
<div className="editor-header__toolbar">
|
||||||
|
<NavigableToolbar
|
||||||
|
className="editor-document-tools edit-post-header-toolbar is-unstyled"
|
||||||
|
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('editor-collapsible-block-toolbar', {
|
||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// Document actions - Component in header for displaying email/campaign title edit popup
|
||||||
|
.mailpoet-email-editor-campaign-name {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.components-dropdown {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-button {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-campaign-name__link {
|
||||||
|
display: inline-flex;
|
||||||
|
height: fit-content;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document actions - Popup for editing email/campaign title
|
||||||
|
.mailpoet-email-editor-campaign-name__dropdown {
|
||||||
|
.components-popover__content {
|
||||||
|
min-width: 280px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-editor-email-title-edit {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mailpoet-email-editor-save-button__dropdown {
|
||||||
|
.components-popover__content {
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export * from './header';
|
export * from './header';
|
@ -0,0 +1,113 @@
|
|||||||
|
import { MenuGroup, MenuItem, DropdownMenu } from '@wordpress/components';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import { displayShortcut } from '@wordpress/keycodes';
|
||||||
|
import { moreVertical } from '@wordpress/icons';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { __, _x } from '@wordpress/i18n';
|
||||||
|
import { PreferenceToggleMenuItem } from '@wordpress/preferences';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
import { TrashModal } from './trash-modal';
|
||||||
|
|
||||||
|
// See:
|
||||||
|
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/header/more-menu/index.js
|
||||||
|
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/more-menu/index.js
|
||||||
|
|
||||||
|
export function MoreMenu(): JSX.Element {
|
||||||
|
const [showTrashModal, setShowTrashModal] = useState(false);
|
||||||
|
const { urls, postId } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
urls: select(storeName).getUrls(),
|
||||||
|
postId: select(storeName).getEmailPostId(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [status, setStatus] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'status',
|
||||||
|
);
|
||||||
|
const { saveEditedEmail, updateEmailMailPoetProperty } =
|
||||||
|
useDispatch(storeName);
|
||||||
|
const goToListings = () => {
|
||||||
|
window.location.href = urls.listings;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu
|
||||||
|
className="edit-site-more-menu"
|
||||||
|
popoverProps={{
|
||||||
|
className: 'edit-site-more-menu__content',
|
||||||
|
}}
|
||||||
|
icon={moreVertical}
|
||||||
|
label={__('More', 'mailpoet')}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<MenuGroup label={_x('View', 'noun', 'mailpoet')}>
|
||||||
|
<PreferenceToggleMenuItem
|
||||||
|
scope="core"
|
||||||
|
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="core"
|
||||||
|
name="focusMode"
|
||||||
|
label={__('Spotlight mode', 'mailpoet')}
|
||||||
|
info={__('Focus at one block at a time', 'mailpoet')}
|
||||||
|
messageActivated={__('Spotlight mode activated', 'mailpoet')}
|
||||||
|
messageDeactivated={__(
|
||||||
|
'Spotlight mode deactivated',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<PreferenceToggleMenuItem
|
||||||
|
scope={storeName}
|
||||||
|
name="fullscreenMode"
|
||||||
|
label={__('Fullscreen mode', 'mailpoet')}
|
||||||
|
info={__('Work without distraction', 'mailpoet')}
|
||||||
|
messageActivated={__('Fullscreen mode activated', 'mailpoet')}
|
||||||
|
messageDeactivated={__(
|
||||||
|
'Fullscreen mode deactivated',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
shortcut={displayShortcut.secondary('f')}
|
||||||
|
/>
|
||||||
|
</MenuGroup>
|
||||||
|
<MenuGroup>
|
||||||
|
{status === 'trash' ? (
|
||||||
|
<MenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await setStatus('draft');
|
||||||
|
await updateEmailMailPoetProperty('deleted_at', '');
|
||||||
|
await saveEditedEmail();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{__('Restore from trash', 'mailpoet')}
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={() => setShowTrashModal(true)} isDestructive>
|
||||||
|
{__('Move to trash', 'mailpoet')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</MenuGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
{showTrashModal && (
|
||||||
|
<TrashModal
|
||||||
|
onClose={() => setShowTrashModal(false)}
|
||||||
|
onRemove={goToListings}
|
||||||
|
postId={postId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { useRef } from '@wordpress/element';
|
||||||
|
import { Button, Dropdown } from '@wordpress/components';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error No types available for useEntitiesSavedStatesIsDirty
|
||||||
|
useEntitiesSavedStatesIsDirty,
|
||||||
|
// @ts-expect-error Our current version of packages doesn't have EntitiesSavedStates export
|
||||||
|
EntitiesSavedStates,
|
||||||
|
} from '@wordpress/editor';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { check, cloud, Icon } from '@wordpress/icons';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
export function SaveButton() {
|
||||||
|
const { saveEditedEmail } = useDispatch(storeName);
|
||||||
|
|
||||||
|
const { dirtyEntityRecords } = useEntitiesSavedStatesIsDirty();
|
||||||
|
|
||||||
|
const { hasEdits, isEmpty, isSaving } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
hasEdits: select(storeName).hasEdits(),
|
||||||
|
isEmpty: select(storeName).isEmpty(),
|
||||||
|
isSaving: select(storeName).isSaving(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonRef = useRef(null);
|
||||||
|
|
||||||
|
const hasNonEmailEdits = dirtyEntityRecords.some(
|
||||||
|
(entity) => entity.name !== 'mailpoet_email',
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSaved = !isEmpty && !isSaving && !hasEdits;
|
||||||
|
const isDisabled = isEmpty || isSaving || isSaved;
|
||||||
|
|
||||||
|
let label = __('Save Draft', 'mailpoet');
|
||||||
|
if (isSaved) {
|
||||||
|
label = __('Saved', 'mailpoet');
|
||||||
|
} else if (isSaving) {
|
||||||
|
label = __('Saving', 'mailpoet');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasNonEmailEdits ? (
|
||||||
|
<div ref={buttonRef}>
|
||||||
|
<Dropdown
|
||||||
|
popoverProps={{
|
||||||
|
placement: 'bottom',
|
||||||
|
anchor: buttonRef.current,
|
||||||
|
}}
|
||||||
|
contentClassName="mailpoet-email-editor-save-button__dropdown"
|
||||||
|
renderToggle={({ onToggle }) => (
|
||||||
|
<Button onClick={onToggle} variant="tertiary">
|
||||||
|
{hasEdits
|
||||||
|
? __('Save email & template', 'mailpoet')
|
||||||
|
: __('Save template', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
renderContent={({ onToggle }) => (
|
||||||
|
<EntitiesSavedStates close={onToggle} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button variant="tertiary" disabled={isDisabled} onClick={saveEditedEmail}>
|
||||||
|
{isSaving && <Icon icon={cloud} />}
|
||||||
|
{isSaved && <Icon icon={check} />}
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import {
|
||||||
|
store as editorStore,
|
||||||
|
// @ts-expect-error No types available for useEntitiesSavedStatesIsDirty
|
||||||
|
useEntitiesSavedStatesIsDirty,
|
||||||
|
} from '@wordpress/editor';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { MailPoetEmailData, storeName } from 'email-editor/engine/store';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useContentValidation } from 'email-editor/engine/hooks';
|
||||||
|
|
||||||
|
export function SendButton() {
|
||||||
|
const [mailpoetEmail] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'mailpoet_data',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isDirty } = useEntitiesSavedStatesIsDirty();
|
||||||
|
|
||||||
|
const { validateContent, isValid } = useContentValidation();
|
||||||
|
const { hasEmptyContent, isEmailSent, isEditingTemplate } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
hasEmptyContent: select(storeName).hasEmptyContent(),
|
||||||
|
isEmailSent: select(storeName).isEmailSent(),
|
||||||
|
isEditingTemplate:
|
||||||
|
select(editorStore).getCurrentPostType() === 'wp_template',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
isEditingTemplate || hasEmptyContent || isEmailSent || isValid || isDirty;
|
||||||
|
|
||||||
|
const mailpoetEmailData: MailPoetEmailData = mailpoetEmail;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (validateContent()) {
|
||||||
|
window.location.href = `admin.php?page=mailpoet-newsletters#/send/${mailpoetEmailData.id}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{__('Send', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Button, Modal } from '@wordpress/components';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { store as coreStore } from '@wordpress/core-data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
|
||||||
|
export function TrashModal({
|
||||||
|
onClose,
|
||||||
|
onRemove,
|
||||||
|
postId,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
postId: number;
|
||||||
|
}) {
|
||||||
|
const { getLastEntityDeleteError } = useSelect(coreStore);
|
||||||
|
const { deleteEntityRecord } = useDispatch(coreStore);
|
||||||
|
const { createErrorNotice } = useDispatch(noticesStore);
|
||||||
|
const closeCallback = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
const trashCallback = async () => {
|
||||||
|
const success = await deleteEntityRecord(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
postId as unknown as string,
|
||||||
|
{},
|
||||||
|
{ throwOnError: false },
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
onRemove();
|
||||||
|
} else {
|
||||||
|
const lastError = getLastEntityDeleteError(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
postId,
|
||||||
|
);
|
||||||
|
// Already deleted.
|
||||||
|
if (lastError?.code === 410) {
|
||||||
|
onRemove();
|
||||||
|
} else {
|
||||||
|
const errorMessage = lastError?.message
|
||||||
|
? (lastError.message as string)
|
||||||
|
: __(
|
||||||
|
'An error occurred while moving the email to the trash.',
|
||||||
|
'mailpoet',
|
||||||
|
);
|
||||||
|
await createErrorNotice(errorMessage, {
|
||||||
|
type: 'snackbar',
|
||||||
|
isDismissible: true,
|
||||||
|
context: 'email-editor',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="mailpoet-move-to-trash-modal"
|
||||||
|
title={__('Move to trash', 'mailpoet')}
|
||||||
|
onRequestClose={closeCallback}
|
||||||
|
focusOnMount="firstContentElement"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{__('Are you sure you want to move this email to trash?', 'mailpoet')}
|
||||||
|
</p>
|
||||||
|
<div className="mailpoet-send-preview-modal-footer">
|
||||||
|
<Button variant="tertiary" onClick={closeCallback}>
|
||||||
|
{__('Cancel', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={trashCallback}>
|
||||||
|
{__('Move to trash', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrashModal;
|
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
__experimentalLibrary as Library,
|
||||||
|
store as blockEditorStore,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { store as editorStore } from '@wordpress/editor';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toggleInserterSidebar } = useDispatch(storeName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-inserter-sidebar">
|
||||||
|
<div className="editor-inserter-sidebar__content">
|
||||||
|
<Library
|
||||||
|
showMostUsedBlocks
|
||||||
|
showInserterHelpPanel={false}
|
||||||
|
// In the email content mode we insert primarily into the post content block.
|
||||||
|
rootClientId={isEditingEmailContent ? postContentId : null}
|
||||||
|
onClose={toggleInserterSidebar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
useShortcut,
|
||||||
|
store as keyboardShortcutsStore,
|
||||||
|
} from '@wordpress/keyboard-shortcuts';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
// See:
|
||||||
|
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/keyboard-shortcuts/index.js
|
||||||
|
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/keyboard-shortcuts/index.js
|
||||||
|
|
||||||
|
export function KeyboardShortcuts(): null {
|
||||||
|
const { isSidebarOpened, hasEdits, isSaving } = useSelect((select) => ({
|
||||||
|
isSidebarOpened: select(storeName).isSidebarOpened(),
|
||||||
|
isSaving: select(storeName).isSaving(),
|
||||||
|
hasEdits: select(storeName).hasEdits(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { openSidebar, closeSidebar, saveEditedEmail, toggleFeature } =
|
||||||
|
useDispatch(storeName);
|
||||||
|
|
||||||
|
const { registerShortcut } = useDispatch(keyboardShortcutsStore);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void registerShortcut({
|
||||||
|
name: 'mailpoet/email-editor/toggle-fullscreen',
|
||||||
|
category: 'global',
|
||||||
|
description: __('Toggle fullscreen mode.', 'mailpoet'),
|
||||||
|
keyCombination: {
|
||||||
|
modifier: 'secondary',
|
||||||
|
character: 'f',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
void registerShortcut({
|
||||||
|
name: 'mailpoet/email-editor/toggle-sidebar',
|
||||||
|
category: 'global',
|
||||||
|
description: __('Show or hide the settings sidebar.', 'mailpoet'),
|
||||||
|
keyCombination: {
|
||||||
|
modifier: 'primaryShift',
|
||||||
|
character: ',',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
void registerShortcut({
|
||||||
|
name: 'mailpoet/email-editor/save',
|
||||||
|
category: 'global',
|
||||||
|
description: __('Save your changes.', 'mailpoet'),
|
||||||
|
keyCombination: {
|
||||||
|
modifier: 'primary',
|
||||||
|
character: 's',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [registerShortcut]);
|
||||||
|
|
||||||
|
useShortcut('mailpoet/email-editor/toggle-fullscreen', () => {
|
||||||
|
void toggleFeature('fullscreenMode');
|
||||||
|
});
|
||||||
|
|
||||||
|
useShortcut('mailpoet/email-editor/toggle-sidebar', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isSidebarOpened) {
|
||||||
|
void closeSidebar();
|
||||||
|
} else {
|
||||||
|
void openSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useShortcut('mailpoet/email-editor/save', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!hasEdits || isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void saveEditedEmail();
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { __experimentalListView as ListView } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
export function ListviewSidebar() {
|
||||||
|
return (
|
||||||
|
<div className="editor-list-view-sidebar">
|
||||||
|
<ListView />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
.components-notice.mailpoet-email-editor-validation-errors {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc outside;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
button.components-button.is-link {
|
||||||
|
color: #1e1e1e;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export { EditorNotices } from './notices';
|
export { EditorNotices } from './notices';
|
||||||
export { EditorSnackbars } from './snackbars';
|
export { EditorSnackbars } from './snackbars';
|
||||||
export { SentEmailNotice } from './sent-email-notice';
|
export { SentEmailNotice } from './sent-email-notice';
|
@ -0,0 +1,43 @@
|
|||||||
|
import { NoticeList } from '@wordpress/components';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
import { ValidationNotices } from './validation-notices';
|
||||||
|
import { EditorSnackbars } from './snackbars';
|
||||||
|
|
||||||
|
// See: https://github.com/WordPress/gutenberg/blob/5be0ec4153c3adf9f0f2513239f4f7a358ba7948/packages/editor/src/components/editor-notices/index.js
|
||||||
|
|
||||||
|
export function EditorNotices() {
|
||||||
|
const { notices } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
notices: select(noticesStore).getNotices('email-editor'),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { removeNotice } = useDispatch(noticesStore);
|
||||||
|
|
||||||
|
const dismissibleNotices = notices.filter(
|
||||||
|
({ isDismissible, type }) => isDismissible && type === 'default',
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonDismissibleNotices = notices.filter(
|
||||||
|
({ isDismissible, type }) => !isDismissible && type === 'default',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NoticeList
|
||||||
|
notices={nonDismissibleNotices}
|
||||||
|
className="components-editor-notices__pinned"
|
||||||
|
/>
|
||||||
|
<NoticeList
|
||||||
|
notices={dismissibleNotices}
|
||||||
|
className="components-editor-notices__dismissible"
|
||||||
|
onRemove={(id) => removeNotice(id, 'email-editor')}
|
||||||
|
/>
|
||||||
|
<ValidationNotices />
|
||||||
|
<EditorSnackbars context="global" />
|
||||||
|
<EditorSnackbars context="email-editor" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import { dispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
export function SentEmailNotice() {
|
||||||
|
const { isEmailSent } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
isEmailSent: select(storeName).isEmailSent(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmailSent) {
|
||||||
|
void dispatch(noticesStore).createNotice(
|
||||||
|
'warning',
|
||||||
|
__(
|
||||||
|
'This email has already been sent. It can be edited, but not sent again. Duplicate this email if you want to send it again.',
|
||||||
|
'mailpoet',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: 'email-sent',
|
||||||
|
isDismissible: false,
|
||||||
|
context: 'email-editor',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [isEmailSent]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { SnackbarList } from '@wordpress/components';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
|
||||||
|
// See: https://github.com/WordPress/gutenberg/blob/2788a9cf8b8149be3ee52dd15ce91fa55815f36a/packages/editor/src/components/editor-snackbars/index.js
|
||||||
|
|
||||||
|
export function EditorSnackbars({ context = 'email-editor' }) {
|
||||||
|
const { notices } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
notices: select(noticesStore).getNotices(context),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { removeNotice } = useDispatch(noticesStore);
|
||||||
|
|
||||||
|
const snackbarNotices = notices.filter(({ type }) => type === 'snackbar');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SnackbarList
|
||||||
|
notices={snackbarNotices}
|
||||||
|
className="components-editor-notices__snackbar"
|
||||||
|
onRemove={(id) => removeNotice(id, context)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Notice, Button } from '@wordpress/components';
|
||||||
|
import { useValidationNotices } from 'email-editor/engine/hooks';
|
||||||
|
|
||||||
|
export function ValidationNotices() {
|
||||||
|
const { notices } = useValidationNotices();
|
||||||
|
|
||||||
|
if (notices.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notice
|
||||||
|
status="error"
|
||||||
|
className="mailpoet-email-editor-validation-errors components-editor-notices__pinned"
|
||||||
|
isDismissible={false}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<strong>{__('Fix errors to continue:', 'mailpoet')}</strong>
|
||||||
|
<ul>
|
||||||
|
{notices.map(({ id, content, actions }) => (
|
||||||
|
<li key={id}>
|
||||||
|
{content}
|
||||||
|
{actions.length > 0
|
||||||
|
? actions.map(({ label, onClick }) => (
|
||||||
|
<Button key={label} onClick={onClick} variant="link">
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
.components-modal__frame.mailpoet-send-preview-email {
|
||||||
|
max-width: 456px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-send-preview-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-send-preview-modal-notice-error {
|
||||||
|
background-color: #fcf0f1;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 0 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-send-preview-modal-notice-success {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export * from './preview-dropdown';
|
export * from './preview-dropdown';
|
||||||
export * from './send-preview-email';
|
export * from './send-preview-email';
|
@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
MenuGroup,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon, external, check, mobile, desktop } from '@wordpress/icons';
|
||||||
|
import { SendPreviewEmail } from './send-preview-email';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
export function PreviewDropdown() {
|
||||||
|
const [mailpoetEmailData] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'mailpoet_data',
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewDeviceType = useSelect(
|
||||||
|
(select) => select(storeName).getDeviceType(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { changePreviewDeviceType, togglePreviewModal } =
|
||||||
|
useDispatch(storeName);
|
||||||
|
const newsletterPreviewUrl: string = mailpoetEmailData?.preview_url || '';
|
||||||
|
|
||||||
|
const changeDeviceType = (newDeviceType: string) => {
|
||||||
|
void changePreviewDeviceType(newDeviceType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openInNewTab = (url: string) => {
|
||||||
|
window.open(url, '_blank', 'noreferrer');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deviceIcons = {
|
||||||
|
mobile,
|
||||||
|
desktop,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu
|
||||||
|
className="mailpoet-preview-dropdown"
|
||||||
|
label={__('Preview', 'mailpoet')}
|
||||||
|
icon={deviceIcons[previewDeviceType.toLowerCase()]}
|
||||||
|
>
|
||||||
|
{({ onClose }) => (
|
||||||
|
<>
|
||||||
|
<MenuGroup>
|
||||||
|
<MenuItem
|
||||||
|
className="block-editor-post-preview__button-resize"
|
||||||
|
onClick={() => changeDeviceType('Desktop')}
|
||||||
|
icon={previewDeviceType === 'Desktop' && check}
|
||||||
|
>
|
||||||
|
{__('Desktop', 'mailpoet')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="block-editor-post-preview__button-resize"
|
||||||
|
onClick={() => changeDeviceType('Mobile')}
|
||||||
|
icon={previewDeviceType === 'Mobile' && check}
|
||||||
|
>
|
||||||
|
{__('Mobile', 'mailpoet')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuGroup>
|
||||||
|
<MenuGroup>
|
||||||
|
<MenuItem
|
||||||
|
className="block-editor-post-preview__button-resize"
|
||||||
|
onClick={() => {
|
||||||
|
void togglePreviewModal(true);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{__('Send a test email', 'mailpoet')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuGroup>
|
||||||
|
{newsletterPreviewUrl ? (
|
||||||
|
<MenuGroup>
|
||||||
|
<div className="edit-post-header-preview__grouping-external">
|
||||||
|
<Button
|
||||||
|
className="edit-post-header-preview__button-external components-menu-item__button"
|
||||||
|
onClick={() => {
|
||||||
|
openInNewTab(newsletterPreviewUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{__('Preview in new tab', 'mailpoet')}
|
||||||
|
<Icon icon={external} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</MenuGroup>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
<SendPreviewEmail />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { check, Icon } from '@wordpress/icons';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
createInterpolateElement,
|
||||||
|
} from '@wordpress/element';
|
||||||
|
import { ENTER } from '@wordpress/keycodes';
|
||||||
|
import { isEmail } from '@wordpress/url';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import {
|
||||||
|
MailPoetEmailData,
|
||||||
|
SendingPreviewStatus,
|
||||||
|
storeName,
|
||||||
|
} from '../../store';
|
||||||
|
|
||||||
|
export function SendPreviewEmail() {
|
||||||
|
const sendToRef = useRef(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
requestSendingNewsletterPreview,
|
||||||
|
togglePreviewModal,
|
||||||
|
updateSendPreviewEmail,
|
||||||
|
} = useDispatch(storeName);
|
||||||
|
|
||||||
|
const {
|
||||||
|
toEmail: previewToEmail,
|
||||||
|
isSendingPreviewEmail,
|
||||||
|
sendingPreviewStatus,
|
||||||
|
isModalOpened,
|
||||||
|
} = useSelect((select) => select(storeName).getPreviewState(), []);
|
||||||
|
|
||||||
|
const [mailpoetEmailData] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'mailpoet_data',
|
||||||
|
) as [MailPoetEmailData, unknown, unknown];
|
||||||
|
|
||||||
|
const handleSendPreviewEmail = () => {
|
||||||
|
void requestSendingNewsletterPreview(mailpoetEmailData.id, previewToEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCallback = () => togglePreviewModal(false);
|
||||||
|
|
||||||
|
// We use this effect to focus on the input field when the modal is opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
sendToRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isModalOpened]);
|
||||||
|
|
||||||
|
if (!isModalOpened) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="mailpoet-send-preview-email"
|
||||||
|
title={__('Send a test email', 'mailpoet')}
|
||||||
|
onRequestClose={closeCallback}
|
||||||
|
focusOnMount={false}
|
||||||
|
>
|
||||||
|
{sendingPreviewStatus === SendingPreviewStatus.ERROR ? (
|
||||||
|
<div className="mailpoet-send-preview-modal-notice-error">
|
||||||
|
{__('Sorry, we were unable to send this email.', 'mailpoet')}
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Please check your <link>sending method configuration</link> with your hosting provider.',
|
||||||
|
'mailpoet',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
link: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href="admin.php?page=mailpoet-settings#mta"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Or, sign up for MailPoet Sending Service to easily send emails. <link>Sign up for free</link>',
|
||||||
|
'mailpoet',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
link: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href={new URL(
|
||||||
|
'free-plan',
|
||||||
|
'https://www.mailpoet.com/',
|
||||||
|
).toString()}
|
||||||
|
key="sign-up-for-free"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<p>
|
||||||
|
{createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Send yourself a test email to test how your email would look like in different email apps. You can also test your spam score by sending a test email to <link1>{$serviceName}</link1>. <link2>Learn more</link2>.',
|
||||||
|
'mailpoet',
|
||||||
|
).replace('{$serviceName}', 'Mail Tester'),
|
||||||
|
{
|
||||||
|
link1: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href="https://www.mail-tester.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
link2: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href="https://kb.mailpoet.com/article/147-test-your-spam-score-with-mail-tester"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<TextControl
|
||||||
|
label={__('Send to', 'mailpoet')}
|
||||||
|
onChange={(email) => {
|
||||||
|
void updateSendPreviewEmail(email);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
const { keyCode } = event;
|
||||||
|
if (keyCode === ENTER) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSendPreviewEmail();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={previewToEmail}
|
||||||
|
type="email"
|
||||||
|
ref={sendToRef}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{sendingPreviewStatus === SendingPreviewStatus.SUCCESS ? (
|
||||||
|
<p className="mailpoet-send-preview-modal-notice-success">
|
||||||
|
<Icon icon={check} style={{ fill: '#4AB866' }} />
|
||||||
|
{__('Test email sent successfully!', 'mailpoet')}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<div className="mailpoet-send-preview-modal-footer">
|
||||||
|
<Button variant="tertiary" onClick={closeCallback}>
|
||||||
|
{__('Close', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSendPreviewEmail}
|
||||||
|
disabled={isSendingPreviewEmail || !isEmail(previewToEmail)}
|
||||||
|
>
|
||||||
|
{isSendingPreviewEmail
|
||||||
|
? __('Sending...', 'mailpoet')
|
||||||
|
: __('Send test email', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import { hasBlockSupport, getBlockSupport } from '@wordpress/blocks';
|
||||||
|
import { Fill, Notice } from '@wordpress/components';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
export const hasBackgroundImageSupport = (nameOrType: string) => {
|
||||||
|
// @ts-expect-error not yet supported in the types
|
||||||
|
const backgroundSupport = getBlockSupport(nameOrType, 'background') as Record<
|
||||||
|
string,
|
||||||
|
boolean
|
||||||
|
>;
|
||||||
|
|
||||||
|
return backgroundSupport && backgroundSupport?.backgroundImage !== false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BlockCompatibilityWarnings(): JSX.Element {
|
||||||
|
// Select the currently selected block
|
||||||
|
const selectedBlock = useSelect(
|
||||||
|
(sel) => sel('core/block-editor').getSelectedBlock(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the selected block has enabled border configuration
|
||||||
|
const hasBorderSupport = hasBlockSupport(
|
||||||
|
selectedBlock?.name,
|
||||||
|
// @ts-expect-error Border is not yet supported in the types
|
||||||
|
'__experimentalBorder',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasBorderSupport && (
|
||||||
|
<Fill name="InspectorControlsBorder">
|
||||||
|
<Notice
|
||||||
|
className="mailpoet__grid-full-width"
|
||||||
|
status="warning"
|
||||||
|
isDismissible={false}
|
||||||
|
>
|
||||||
|
{__(
|
||||||
|
'Border display may vary or be unsupported in some email clients.',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
</Notice>
|
||||||
|
</Fill>
|
||||||
|
)}
|
||||||
|
{hasBackgroundImageSupport(selectedBlock?.name) && (
|
||||||
|
<Fill name="InspectorControlsBackground">
|
||||||
|
<Notice
|
||||||
|
className="mailpoet__grid-full-width"
|
||||||
|
status="warning"
|
||||||
|
isDismissible={false}
|
||||||
|
>
|
||||||
|
{__(
|
||||||
|
'Select a background color for email clients that do not support background images.',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
</Notice>
|
||||||
|
</Fill>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
__experimentalText as Text,
|
||||||
|
ExternalLink,
|
||||||
|
PanelBody,
|
||||||
|
TextareaControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { createInterpolateElement } from '@wordpress/element';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
const previewTextMaxLength = 150;
|
||||||
|
const previewTextRecommendedLength = 80;
|
||||||
|
|
||||||
|
export function DetailsPanel() {
|
||||||
|
const [mailpoetEmailData] = useEntityProp(
|
||||||
|
'postType',
|
||||||
|
'mailpoet_email',
|
||||||
|
'mailpoet_data',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { updateEmailMailPoetProperty } = useDispatch(storeName);
|
||||||
|
const subjectHelp = createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Use shortcodes to personalize your email, or learn more about <bestPracticeLink>best practices</bestPracticeLink> and using <emojiLink>emoji in subject lines</emojiLink>.',
|
||||||
|
'mailpoet',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
bestPracticeLink: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href="https://www.mailpoet.com/blog/17-email-subject-line-best-practices-to-boost-engagement/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
emojiLink: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href="https://www.mailpoet.com/blog/tips-using-emojis-in-subject-lines/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const subjectLabel = (
|
||||||
|
<>
|
||||||
|
<span>{__('Subject', 'mailpoet')}</span>
|
||||||
|
<ExternalLink href="https://kb.mailpoet.com/article/215-personalize-newsletter-with-shortcodes#list">
|
||||||
|
{__('Shortcode guide', 'mailpoet')}
|
||||||
|
</ExternalLink>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewTextLength = mailpoetEmailData?.preheader?.length ?? 0;
|
||||||
|
|
||||||
|
const preheaderLabel = (
|
||||||
|
<>
|
||||||
|
<span>{__('Preview text', 'mailpoet')}</span>
|
||||||
|
<span
|
||||||
|
className={classnames('mailpoet-settings-panel__preview-text-length', {
|
||||||
|
'mailpoet-settings-panel__preview-text-length-warning':
|
||||||
|
previewTextLength > previewTextRecommendedLength,
|
||||||
|
'mailpoet-settings-panel__preview-text-length-error':
|
||||||
|
previewTextLength > previewTextMaxLength,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{previewTextLength}/{previewTextMaxLength}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelBody
|
||||||
|
title={__('Details', 'mailpoet')}
|
||||||
|
className="mailpoet-email-editor__settings-panel"
|
||||||
|
>
|
||||||
|
<TextareaControl
|
||||||
|
className="mailpoet-settings-panel__subject"
|
||||||
|
label={subjectLabel}
|
||||||
|
placeholder={__('Eg. The summer sale is here!', 'mailpoet')}
|
||||||
|
value={mailpoetEmailData?.subject ?? ''}
|
||||||
|
onChange={(value) => updateEmailMailPoetProperty('subject', value)}
|
||||||
|
data-automation-id="email_subject"
|
||||||
|
/>
|
||||||
|
<div className="mailpoet-settings-panel__help">
|
||||||
|
<Text>{subjectHelp}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextareaControl
|
||||||
|
className="mailpoet-settings-panel__preview-text"
|
||||||
|
label={preheaderLabel}
|
||||||
|
placeholder={__(
|
||||||
|
"Add a preview text to capture subscribers' attention and increase open rates.",
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
value={mailpoetEmailData?.preheader ?? ''}
|
||||||
|
onChange={(value) => updateEmailMailPoetProperty('preheader', value)}
|
||||||
|
data-automation-id="email_preview_text"
|
||||||
|
/>
|
||||||
|
<div className="mailpoet-settings-panel__help">
|
||||||
|
<Text>
|
||||||
|
{createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'<link>This text</link> will appear in the inbox, underneath the subject line.',
|
||||||
|
'mailpoet',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
link: (
|
||||||
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
|
||||||
|
<a
|
||||||
|
href={new URL(
|
||||||
|
'article/418-preview-text',
|
||||||
|
'https://kb.mailpoet.com/',
|
||||||
|
).toString()}
|
||||||
|
key="preview-text-kb"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</PanelBody>
|
||||||
|
);
|
||||||
|
}
|
@ -1,19 +1,14 @@
|
|||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { Panel } from '@wordpress/components';
|
import { Panel } from '@wordpress/components';
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { DetailsPanel } from './details-panel';
|
import { DetailsPanel } from './details-panel';
|
||||||
import { EmailTypeInfo } from './email-type-info';
|
import { EmailTypeInfo } from './email-type-info';
|
||||||
|
import { TemplatesPanel } from './templates-panel';
|
||||||
|
|
||||||
export function EmailSettings() {
|
export function EmailSettings() {
|
||||||
return (
|
return (
|
||||||
<Panel>
|
<Panel>
|
||||||
<EmailTypeInfo />
|
<EmailTypeInfo />
|
||||||
<DetailsPanel />
|
<TemplatesPanel />
|
||||||
</Panel>
|
<DetailsPanel />
|
||||||
);
|
</Panel>
|
||||||
|
);
|
||||||
}
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Panel, PanelBody, PanelRow } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon, megaphone } from '@wordpress/icons';
|
||||||
|
|
||||||
|
export function EmailTypeInfo() {
|
||||||
|
return (
|
||||||
|
<Panel className="mailpoet-email-sidebar__email-type-info">
|
||||||
|
<PanelBody>
|
||||||
|
<PanelRow>
|
||||||
|
<span className="mailpoet-email-type-info__icon">
|
||||||
|
<Icon icon={megaphone} />
|
||||||
|
</span>
|
||||||
|
<div className="mailpoet-email-type-info__content">
|
||||||
|
<h2>{__('Newsletter', 'mailpoet')}</h2>
|
||||||
|
<span>
|
||||||
|
{__(
|
||||||
|
'Send or schedule a newsletter to connect with your subscribers.',
|
||||||
|
'mailpoet',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PanelRow>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import * as React from '@wordpress/element';
|
||||||
|
import { privateApis as componentsPrivateApis } from '@wordpress/components';
|
||||||
|
import { mainSidebarEmailTab, mainSidebarBlockTab } from '../../store';
|
||||||
|
import { unlock } from '../../../lock-unlock';
|
||||||
|
|
||||||
|
const { Tabs } = unlock(componentsPrivateApis);
|
||||||
|
|
||||||
|
export function HeaderTabs(_, ref) {
|
||||||
|
return (
|
||||||
|
<Tabs.TabList ref={ref}>
|
||||||
|
<Tabs.Tab tabId={mainSidebarEmailTab}>{__('Email', 'mailpoet')}</Tabs.Tab>
|
||||||
|
<Tabs.Tab tabId={mainSidebarBlockTab}>{__('Block')}</Tabs.Tab>
|
||||||
|
</Tabs.TabList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Header = React.forwardRef(HeaderTabs);
|
@ -0,0 +1,17 @@
|
|||||||
|
@import '~@wordpress/base-styles/colors';
|
||||||
|
|
||||||
|
.background-block-support-panel .mailpoet__grid-full-width,
|
||||||
|
.border-block-support-panel .mailpoet__grid-full-width {
|
||||||
|
// setting the grid to 100% width because border block contains two grid columns
|
||||||
|
grid-column: 1/3;
|
||||||
|
// resetting margin fits the notice to the full column width
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable focus outline for buttons in the sidebar
|
||||||
|
.edit-post-sidebar {
|
||||||
|
.components-button:focus:not(:disabled) {
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export * from './block-compatibility-warnings';
|
export * from './block-compatibility-warnings';
|
||||||
export * from './sidebar';
|
export * from './sidebar';
|
@ -0,0 +1,97 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useContext, useRef, useEffect } from '@wordpress/element';
|
||||||
|
import { privateApis as componentsPrivateApis } from '@wordpress/components';
|
||||||
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
BlockInspector,
|
||||||
|
store as blockEditorStore,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { ComplementaryArea } from '@wordpress/interface';
|
||||||
|
import { drawerRight } from '@wordpress/icons';
|
||||||
|
import { store as editorStore } from '@wordpress/editor';
|
||||||
|
import {
|
||||||
|
storeName,
|
||||||
|
mainSidebarEmailTab,
|
||||||
|
mainSidebarBlockTab,
|
||||||
|
mainSidebarId,
|
||||||
|
} from '../../store';
|
||||||
|
import { Header } from './header';
|
||||||
|
import { EmailSettings } from './email-settings';
|
||||||
|
import { TemplateSettings } from './template-settings';
|
||||||
|
import { unlock } from '../../../lock-unlock';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const { Tabs } = unlock(componentsPrivateApis);
|
||||||
|
|
||||||
|
type Props = React.ComponentProps<typeof ComplementaryArea>;
|
||||||
|
|
||||||
|
function SidebarContent(props: Props) {
|
||||||
|
const { isEditingTemplate } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
isEditingTemplate:
|
||||||
|
select(editorStore).getCurrentPostType() === 'wp_template',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabListRef = useRef(null);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
const tabsContextValue = useContext(Tabs.Context);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComplementaryArea
|
||||||
|
identifier={mainSidebarId}
|
||||||
|
headerClassName="editor-sidebar__panel-tabs"
|
||||||
|
className="edit-post-sidebar"
|
||||||
|
header={
|
||||||
|
<Tabs.Context.Provider value={tabsContextValue}>
|
||||||
|
<Header ref={tabListRef} />
|
||||||
|
</Tabs.Context.Provider>
|
||||||
|
}
|
||||||
|
icon={drawerRight}
|
||||||
|
scope={storeName}
|
||||||
|
smallScreenTitle={__('No title', 'mailpoet')}
|
||||||
|
isActiveByDefault
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Tabs.Context.Provider value={tabsContextValue}>
|
||||||
|
<Tabs.TabPanel tabId={mainSidebarEmailTab}>
|
||||||
|
{isEditingTemplate ? <TemplateSettings /> : <EmailSettings />}
|
||||||
|
</Tabs.TabPanel>
|
||||||
|
<Tabs.TabPanel tabId={mainSidebarBlockTab}>
|
||||||
|
<BlockInspector />
|
||||||
|
</Tabs.TabPanel>
|
||||||
|
</Tabs.Context.Provider>
|
||||||
|
</ComplementaryArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar(props: Props) {
|
||||||
|
const { toggleSettingsSidebarActiveTab } = useDispatch(storeName);
|
||||||
|
const { activeTab, selectedBlockId } = useSelect(
|
||||||
|
(select) => ({
|
||||||
|
activeTab: select(storeName).getSettingsSidebarActiveTab(),
|
||||||
|
selectedBlockId: select(blockEditorStore).getSelectedBlockClientId(),
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch tab based on selected block.
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedBlockId) {
|
||||||
|
void toggleSettingsSidebarActiveTab(mainSidebarBlockTab);
|
||||||
|
} else {
|
||||||
|
void toggleSettingsSidebarActiveTab(mainSidebarEmailTab);
|
||||||
|
}
|
||||||
|
}, [selectedBlockId, toggleSettingsSidebarActiveTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
selectedTabId={activeTab || mainSidebarEmailTab}
|
||||||
|
onSelect={(key) => toggleSettingsSidebarActiveTab(key as string)}
|
||||||
|
>
|
||||||
|
<SidebarContent {...props} />
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Panel, PanelBody, PanelRow } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { Icon, layout } from '@wordpress/icons';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
|
||||||
|
export function TemplateInfo() {
|
||||||
|
const template = useSelect(
|
||||||
|
(select) => select(storeName).getCurrentTemplate(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel className="mailpoet-email-sidebar__email-type-info">
|
||||||
|
<PanelBody>
|
||||||
|
<PanelRow>
|
||||||
|
<span className="mailpoet-email-type-info__icon">
|
||||||
|
<Icon icon={layout} />
|
||||||
|
</span>
|
||||||
|
<div className="mailpoet-email-type-info__content">
|
||||||
|
{/* @ts-expect-error Todo template type is not defined */}
|
||||||
|
<h2>{template?.title || __('Template', 'mailpoet')}</h2>
|
||||||
|
{/* @ts-expect-error Todo template type is not defined */}
|
||||||
|
<span>{template?.description || ''}</span>
|
||||||
|
</div>
|
||||||
|
</PanelRow>
|
||||||
|
</PanelBody>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Panel } from '@wordpress/components';
|
||||||
|
import { TemplateInfo } from './template-info';
|
||||||
|
import { TemplatesPanel } from './templates-panel';
|
||||||
|
|
||||||
|
export function TemplateSettings() {
|
||||||
|
return (
|
||||||
|
<Panel>
|
||||||
|
<TemplateInfo />
|
||||||
|
<TemplatesPanel />
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
import { PanelBody, Button } from '@wordpress/components';
|
||||||
|
import { useSelect, useDispatch, dispatch, select } from '@wordpress/data';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { store as editorStore } from '@wordpress/editor';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
import { store as coreStore } from '@wordpress/core-data';
|
||||||
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
|
import {
|
||||||
|
parse,
|
||||||
|
// @ts-expect-error No types available for this yet.
|
||||||
|
__unstableSerializeAndClean,
|
||||||
|
BlockInstance,
|
||||||
|
} from '@wordpress/blocks';
|
||||||
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
|
import { storeName } from '../../store';
|
||||||
|
import { unlock } from '../../../lock-unlock';
|
||||||
|
|
||||||
|
// Todo: This is not available yet. Replace when possible.
|
||||||
|
async function revertTemplate(template) {
|
||||||
|
const templateEntityConfig = select(coreStore).getEntityConfig(
|
||||||
|
'postType',
|
||||||
|
template.type as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileTemplatePath = addQueryArgs(
|
||||||
|
`${templateEntityConfig.baseURL as string}/${template.id as string}`,
|
||||||
|
{ context: 'edit', source: 'theme' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileTemplate = await apiFetch({ path: fileTemplatePath });
|
||||||
|
|
||||||
|
const serializeBlocks = ({ blocks: blocksForSerialization = [] }) =>
|
||||||
|
__unstableSerializeAndClean(blocksForSerialization) as BlockInstance[];
|
||||||
|
|
||||||
|
// @ts-expect-error template type is not defined
|
||||||
|
const blocks = parse(fileTemplate?.content?.raw as string);
|
||||||
|
void dispatch(coreStore).editEntityRecord(
|
||||||
|
'postType',
|
||||||
|
template.type as string,
|
||||||
|
// @ts-expect-error template type is not defined
|
||||||
|
fileTemplate.id as string,
|
||||||
|
{
|
||||||
|
content: serializeBlocks,
|
||||||
|
blocks,
|
||||||
|
source: 'theme',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplatesPanel() {
|
||||||
|
const { onNavigateToEntityRecord, template, hasHistory } = useSelect(
|
||||||
|
(sel) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { getEditorSettings: _getEditorSettings } = unlock(
|
||||||
|
sel(editorStore),
|
||||||
|
);
|
||||||
|
const editorSettings = _getEditorSettings();
|
||||||
|
return {
|
||||||
|
onNavigateToEntityRecord: editorSettings.onNavigateToEntityRecord,
|
||||||
|
hasHistory: !!editorSettings.onNavigateToPreviousEntityRecord,
|
||||||
|
template: sel(storeName).getEditedPostTemplate(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { saveEditedEntityRecord } = useDispatch(coreStore);
|
||||||
|
const { createSuccessNotice, createErrorNotice } = useDispatch(noticesStore);
|
||||||
|
async function revertAndSaveTemplate() {
|
||||||
|
try {
|
||||||
|
await revertTemplate(template);
|
||||||
|
await saveEditedEntityRecord('postType', template.type, template.id, {});
|
||||||
|
void createSuccessNotice(
|
||||||
|
sprintf(
|
||||||
|
/* translators: The template/part's name. */
|
||||||
|
__('"%s" reset.', 'mailpoet'),
|
||||||
|
decodeEntities(template.title),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: 'snackbar',
|
||||||
|
id: 'edit-site-template-reverted',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
void createErrorNotice(
|
||||||
|
__('An error occurred while reverting the template.', 'mailpoet'),
|
||||||
|
{
|
||||||
|
type: 'snackbar',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelBody
|
||||||
|
title={__('Templates Experiment', 'mailpoet')}
|
||||||
|
className="mailpoet-email-editor__settings-panel"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Components from this Panel will be placed in different areas of the UI.
|
||||||
|
They are place here in one place just to simplify the experiment.
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<h3>Edit template toggle</h3>
|
||||||
|
{template && !hasHistory && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
onNavigateToEntityRecord({
|
||||||
|
postId: template.id,
|
||||||
|
postType: 'wp_template',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!template.id}
|
||||||
|
>
|
||||||
|
{__('Edit template', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<hr />
|
||||||
|
<h3>Revert Template</h3>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
void revertAndSaveTemplate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{__('Revert customizations', 'mailpoet')}
|
||||||
|
</Button>
|
||||||
|
</PanelBody>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
.mailpoet-email-editor__styles-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.components-panel,
|
||||||
|
.components-navigator-provider,
|
||||||
|
.components-navigator-screen {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-navigator-screen {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-navigator-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-editor__styles-header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailpoet-email-editor__styles-header-description {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.edit-site-global-styles-subtitle {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-column {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-site-global-styles-screen-typography__indicator {
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 2em;
|
||||||
|
line-height: 2em;
|
||||||
|
padding: 0;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-site-typography-preview {
|
||||||
|
align-items: center;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
|
import './index.scss';
|
||||||
|
|
||||||
export * from './styles-sidebar';
|
export * from './styles-sidebar';
|
@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
// We can remove the ts-expect-error comments once the types are available.
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalSpacingSizesControl as SpacingSizesControl,
|
||||||
|
useSettings,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import {
|
||||||
|
__experimentalToolsPanel as ToolsPanel,
|
||||||
|
__experimentalToolsPanelItem as ToolsPanelItem,
|
||||||
|
__experimentalUseCustomUnits as useCustomUnits,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { useEmailStyles } from '../../../hooks';
|
||||||
|
|
||||||
|
export function DimensionsPanel() {
|
||||||
|
const [availableUnits] = useSettings('spacing.units') as [string[]];
|
||||||
|
const units = useCustomUnits({
|
||||||
|
availableUnits,
|
||||||
|
});
|
||||||
|
const { styles, defaultStyles, updateStyleProp } = useEmailStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolsPanel
|
||||||
|
label={__('Dimensions', 'mailpoet')}
|
||||||
|
resetAll={() => {
|
||||||
|
updateStyleProp(['spacing'], defaultStyles.spacing);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToolsPanelItem
|
||||||
|
isShownByDefault
|
||||||
|
hasValue={() =>
|
||||||
|
!isEqual(styles.spacing.padding, defaultStyles.spacing.padding)
|
||||||
|
}
|
||||||
|
label={__('Padding', 'mailpoet')}
|
||||||
|
onDeselect={() =>
|
||||||
|
updateStyleProp(['spacing', 'padding'], defaultStyles.spacing.padding)
|
||||||
|
}
|
||||||
|
className="tools-panel-item-spacing"
|
||||||
|
>
|
||||||
|
<SpacingSizesControl
|
||||||
|
allowReset
|
||||||
|
values={styles.spacing.padding}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateStyleProp(['spacing', 'padding'], value);
|
||||||
|
}}
|
||||||
|
label={__('Padding', 'mailpoet')}
|
||||||
|
sides={['horizontal', 'vertical', 'top', 'left', 'right', 'bottom']}
|
||||||
|
units={units}
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
<ToolsPanelItem
|
||||||
|
isShownByDefault
|
||||||
|
label={__('Block spacing', 'mailpoet')}
|
||||||
|
hasValue={() =>
|
||||||
|
styles.spacing.blockGap !== defaultStyles.spacing.blockGap
|
||||||
|
}
|
||||||
|
onDeselect={() =>
|
||||||
|
updateStyleProp(
|
||||||
|
['spacing', 'blockGap'],
|
||||||
|
defaultStyles.spacing.blockGap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="tools-panel-item-spacing"
|
||||||
|
>
|
||||||
|
<SpacingSizesControl
|
||||||
|
label={__('Block spacing', 'mailpoet')}
|
||||||
|
min={0}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateStyleProp(['spacing', 'blockGap'], value.top);
|
||||||
|
}}
|
||||||
|
showSideInLabel={false}
|
||||||
|
sides={['top']} // Use 'top' as the shorthand property in non-axial configurations.
|
||||||
|
values={{ top: styles.spacing.blockGap }}
|
||||||
|
allowReset
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
</ToolsPanel>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useCallback } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
useSettings,
|
||||||
|
// We can remove the ts-expect-error comments once the types are available.
|
||||||
|
// @see packages/block-editor/src/components/index.js
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalFontAppearanceControl as FontAppearanceControl,
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalLetterSpacingControl as LetterSpacingControl,
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalFontFamilyControl as FontFamilyControl,
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
LineHeightControl,
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalTextDecorationControl as TextDecorationControl,
|
||||||
|
// @ts-expect-error TS7016: Could not find a declaration file for module '@wordpress/block-editor'.
|
||||||
|
__experimentalTextTransformControl as TextTransformControl,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import {
|
||||||
|
FontSizePicker,
|
||||||
|
__experimentalToolsPanel as ToolsPanel,
|
||||||
|
__experimentalToolsPanelItem as ToolsPanelItem,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { useEmailStyles } from '../../../hooks';
|
||||||
|
import { getElementStyles } from '../utils';
|
||||||
|
|
||||||
|
export const DEFAULT_CONTROLS = {
|
||||||
|
fontFamily: true,
|
||||||
|
fontSize: true,
|
||||||
|
fontAppearance: true,
|
||||||
|
lineHeight: true,
|
||||||
|
letterSpacing: false,
|
||||||
|
textTransform: false,
|
||||||
|
textDecoration: false,
|
||||||
|
writingMode: true,
|
||||||
|
textColumns: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TypographyElementPanel({
|
||||||
|
element,
|
||||||
|
headingLevel,
|
||||||
|
defaultControls = DEFAULT_CONTROLS,
|
||||||
|
}: {
|
||||||
|
element: string;
|
||||||
|
headingLevel: string;
|
||||||
|
defaultControls?: typeof DEFAULT_CONTROLS;
|
||||||
|
}) {
|
||||||
|
const [fontSizes, blockLevelFontFamilies] = useSettings(
|
||||||
|
'typography.fontSizes',
|
||||||
|
'typography.fontFamilies',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ref: https://github.com/WordPress/gutenberg/issues/59778
|
||||||
|
const fontFamilies = blockLevelFontFamilies?.default || [];
|
||||||
|
const { styles, defaultStyles, updateStyleProp } = useEmailStyles();
|
||||||
|
const elementStyles = getElementStyles(styles, element, headingLevel);
|
||||||
|
const defaultElementStyles = getElementStyles(
|
||||||
|
defaultStyles,
|
||||||
|
element,
|
||||||
|
headingLevel,
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
lineHeight,
|
||||||
|
letterSpacing,
|
||||||
|
textDecoration,
|
||||||
|
textTransform,
|
||||||
|
} = elementStyles.typography;
|
||||||
|
|
||||||
|
const {
|
||||||
|
fontFamily: defaultFontFamily,
|
||||||
|
fontSize: defaultFontSize,
|
||||||
|
fontStyle: defaultFontStyle,
|
||||||
|
fontWeight: defaultFontWeight,
|
||||||
|
lineHeight: defaultLineHeight,
|
||||||
|
letterSpacing: defaultLetterSpacing,
|
||||||
|
textDecoration: defaultTextDecoration,
|
||||||
|
textTransform: defaultTextTransform,
|
||||||
|
} = defaultElementStyles.typography;
|
||||||
|
|
||||||
|
const hasFontFamily = () => fontFamily !== defaultFontFamily;
|
||||||
|
const hasFontSize = () => fontSize !== defaultFontSize;
|
||||||
|
const hasFontAppearance = () =>
|
||||||
|
fontWeight !== defaultFontWeight || fontStyle !== defaultFontStyle;
|
||||||
|
const hasLineHeight = () => lineHeight !== defaultLineHeight;
|
||||||
|
const hasLetterSpacing = () => letterSpacing !== defaultLetterSpacing;
|
||||||
|
const hasTextDecoration = () => textDecoration !== defaultTextDecoration;
|
||||||
|
const hasTextTransform = () => textTransform !== defaultTextTransform;
|
||||||
|
const showToolFontSize = element !== 'heading' || headingLevel !== 'heading';
|
||||||
|
|
||||||
|
const updateElementStyleProp = useCallback(
|
||||||
|
(path, newValue) => {
|
||||||
|
if (element === 'heading') {
|
||||||
|
updateStyleProp(['elements', headingLevel, ...path], newValue);
|
||||||
|
} else if (element === 'text') {
|
||||||
|
updateStyleProp([...path], newValue);
|
||||||
|
} else {
|
||||||
|
updateStyleProp(['elements', element, ...path], newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[element, updateStyleProp, headingLevel],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setLetterSpacing = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'letterSpacing'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLineHeight = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'lineHeight'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFontSize = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'fontSize'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFontFamily = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'fontFamily'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTextDecoration = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'textDecoration'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTextTransform = (newValue) => {
|
||||||
|
updateElementStyleProp(['typography', 'textTransform'], newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFontAppearance = ({
|
||||||
|
fontStyle: newFontStyle,
|
||||||
|
fontWeight: newFontWeight,
|
||||||
|
}) => {
|
||||||
|
updateElementStyleProp(['typography', 'fontStyle'], newFontStyle);
|
||||||
|
updateElementStyleProp(['typography', 'fontWeight'], newFontWeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAll = () => {
|
||||||
|
updateElementStyleProp(['typography'], defaultElementStyles.typography);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolsPanel label={__('Typography', 'mailpoet')} resetAll={resetAll}>
|
||||||
|
<ToolsPanelItem
|
||||||
|
label={__('Font family', 'mailpoet')}
|
||||||
|
hasValue={hasFontFamily}
|
||||||
|
onDeselect={() => setFontFamily(defaultFontFamily)}
|
||||||
|
isShownByDefault={defaultControls.fontFamily}
|
||||||
|
>
|
||||||
|
<FontFamilyControl
|
||||||
|
value={fontFamily}
|
||||||
|
onChange={setFontFamily}
|
||||||
|
size="__unstable-large"
|
||||||
|
fontFamilies={fontFamilies}
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
{showToolFontSize && (
|
||||||
|
<ToolsPanelItem
|
||||||
|
label={__('Font size', 'mailpoet')}
|
||||||
|
hasValue={hasFontSize}
|
||||||
|
onDeselect={() => setFontSize(defaultFontSize)}
|
||||||
|
isShownByDefault={defaultControls.fontSize}
|
||||||
|
>
|
||||||
|
<FontSizePicker
|
||||||
|
value={fontSize}
|
||||||
|
onChange={setFontSize}
|
||||||
|
fontSizes={fontSizes}
|
||||||
|
disableCustomFontSizes={false}
|
||||||
|
withReset={false}
|
||||||
|
withSlider
|
||||||
|
size="__unstable-large"
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
)}
|
||||||
|
<ToolsPanelItem
|
||||||
|
className="single-column"
|
||||||
|
label={__('Appearance', 'mailpoet')}
|
||||||
|
hasValue={hasFontAppearance}
|
||||||
|
onDeselect={() => {
|
||||||
|
setFontAppearance({
|
||||||
|
fontStyle: defaultFontStyle,
|
||||||
|
fontWeight: defaultFontWeight,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isShownByDefault={defaultControls.fontAppearance}
|
||||||
|
>
|
||||||
|
<FontAppearanceControl
|
||||||
|
value={{
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
}}
|
||||||
|
onChange={setFontAppearance}
|
||||||
|
hasFontStyles
|
||||||
|
hasFontWeights
|
||||||
|
size="__unstable-large"
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
<ToolsPanelItem
|
||||||
|
className="single-column"
|
||||||
|
label={__('Line height', 'mailpoet')}
|
||||||
|
hasValue={hasLineHeight}
|
||||||
|
onDeselect={() => setLineHeight(defaultLineHeight)}
|
||||||
|
isShownByDefault={defaultControls.lineHeight}
|
||||||
|
>
|
||||||
|
<LineHeightControl
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
__unstableInputWidth="auto"
|
||||||
|
value={lineHeight}
|
||||||
|
onChange={setLineHeight}
|
||||||
|
size="__unstable-large"
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
<ToolsPanelItem
|
||||||
|
className="single-column"
|
||||||
|
label={__('Letter spacing', 'mailpoet')}
|
||||||
|
hasValue={hasLetterSpacing}
|
||||||
|
onDeselect={() => setLetterSpacing(defaultLetterSpacing)}
|
||||||
|
isShownByDefault={defaultControls.letterSpacing}
|
||||||
|
>
|
||||||
|
<LetterSpacingControl
|
||||||
|
value={letterSpacing}
|
||||||
|
onChange={setLetterSpacing}
|
||||||
|
size="__unstable-large"
|
||||||
|
__unstableInputWidth="auto"
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
<ToolsPanelItem
|
||||||
|
className="single-column"
|
||||||
|
label={__('Text decoration', 'mailpoet')}
|
||||||
|
hasValue={hasTextDecoration}
|
||||||
|
onDeselect={() => setTextDecoration(defaultTextDecoration)}
|
||||||
|
isShownByDefault={defaultControls.textDecoration}
|
||||||
|
>
|
||||||
|
<TextDecorationControl
|
||||||
|
value={textDecoration}
|
||||||
|
onChange={setTextDecoration}
|
||||||
|
size="__unstable-large"
|
||||||
|
__unstableInputWidth="auto"
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
<ToolsPanelItem
|
||||||
|
label={__('Letter case', 'mailpoet')}
|
||||||
|
hasValue={hasTextTransform}
|
||||||
|
onDeselect={() => setTextTransform(defaultTextTransform)}
|
||||||
|
isShownByDefault={defaultControls.textTransform}
|
||||||
|
>
|
||||||
|
<TextTransformControl
|
||||||
|
value={textTransform}
|
||||||
|
onChange={setTextTransform}
|
||||||
|
showNone
|
||||||
|
isBlock
|
||||||
|
size="__unstable-large"
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
</ToolsPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypographyElementPanel;
|
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* WordPress dependencies
|
||||||
|
*/
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
__experimentalItemGroup as ItemGroup,
|
||||||
|
__experimentalItem as Item,
|
||||||
|
__experimentalVStack as VStack,
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
__experimentalHeading as Heading,
|
||||||
|
__experimentalNavigatorButton as NavigatorButton,
|
||||||
|
FlexItem,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { useEmailStyles } from '../../../hooks';
|
||||||
|
import { getElementStyles } from '../utils';
|
||||||
|
|
||||||
|
function ElementItem({ element, label }: { element: string; label: string }) {
|
||||||
|
const { styles } = useEmailStyles();
|
||||||
|
const elementStyles = getElementStyles(styles, element, null, true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
letterSpacing,
|
||||||
|
textDecoration,
|
||||||
|
textTransform,
|
||||||
|
} = elementStyles.typography;
|
||||||
|
|
||||||
|
const textColor = elementStyles.color?.text || 'inherit';
|
||||||
|
const background = elementStyles.color?.background || '#f0f0f0';
|
||||||
|
const navigationButtonLabel = sprintf(
|
||||||
|
// translators: %s: is a subset of Typography, e.g., 'text' or 'links'.
|
||||||
|
__('Typography %s styles', 'mailpoet'),
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item>
|
||||||
|
<NavigatorButton
|
||||||
|
path={`/typography/${element}`}
|
||||||
|
aria-label={navigationButtonLabel}
|
||||||
|
>
|
||||||
|
<HStack justify="flex-start">
|
||||||
|
<FlexItem
|
||||||
|
className="edit-site-global-styles-screen-typography__indicator"
|
||||||
|
style={{
|
||||||
|
fontFamily: fontFamily ?? 'serif',
|
||||||
|
background,
|
||||||
|
color: textColor,
|
||||||
|
fontStyle: fontStyle ?? 'normal',
|
||||||
|
fontWeight: fontWeight ?? 'normal',
|
||||||
|
letterSpacing: letterSpacing ?? 'normal',
|
||||||
|
textDecoration:
|
||||||
|
textDecoration ?? (element === 'link' ? 'underline' : 'none'),
|
||||||
|
textTransform: textTransform ?? 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aa
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem>{label}</FlexItem>
|
||||||
|
</HStack>
|
||||||
|
</NavigatorButton>
|
||||||
|
</Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypographyPanel() {
|
||||||
|
return (
|
||||||
|
<Card size="small" variant="primary" isBorderless>
|
||||||
|
<CardBody>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Heading level={3} className="edit-site-global-styles-subtitle">
|
||||||
|
{__('Elements', 'mailpoet')}
|
||||||
|
</Heading>
|
||||||
|
<ItemGroup isBordered isSeparated size="small">
|
||||||
|
<ElementItem element="text" label={__('Text', 'mailpoet')} />
|
||||||
|
<ElementItem element="link" label={__('Links', 'mailpoet')} />
|
||||||
|
<ElementItem element="heading" label={__('Headings', 'mailpoet')} />
|
||||||
|
<ElementItem element="button" label={__('Buttons', 'mailpoet')} />
|
||||||
|
</ItemGroup>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypographyPanel;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user