Autoformat files with prettier

[MAILPOET-4075]
This commit is contained in:
Jan Jakes
2022-04-08 14:44:12 +02:00
committed by Veljko V
parent 2506ff5490
commit ab27eaee2d
592 changed files with 17992 additions and 12047 deletions

View File

@ -116,7 +116,7 @@ jobs:
- checkout: - checkout:
path: /home/circleci/mailpoet path: /home/circleci/mailpoet
- run: - run:
name: "Compute checksum for prefixer" name: 'Compute checksum for prefixer'
command: find prefixer -type f -not -path 'prefixer/build/*' -not -path 'prefixer/vendor/*' | sort | xargs cat | sha512sum > prefixer-checksum command: find prefixer -type f -not -path 'prefixer/build/*' -not -path 'prefixer/vendor/*' | sort | xargs cat | sha512sum > prefixer-checksum
- restore_cache: - restore_cache:
key: tools-{{ checksum "tools/install.php" }} key: tools-{{ checksum "tools/install.php" }}
@ -133,7 +133,7 @@ jobs:
- npm-{{ checksum "package-lock.json" }} - npm-{{ checksum "package-lock.json" }}
- npm- # fallback to most recent npm-* if not found by checksum - npm- # fallback to most recent npm-* if not found by checksum
- run: - run:
name: "Set up test environment" name: 'Set up test environment'
command: | command: |
# install plugin dependencies # install plugin dependencies
COMPOSER_DEV_MODE=1 php tools/install.php COMPOSER_DEV_MODE=1 php tools/install.php
@ -197,7 +197,7 @@ jobs:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- add_ssh_keys - add_ssh_keys
- run: - run:
name: "Install Premium plugin" name: 'Install Premium plugin'
command: | command: |
# Add GitHub to known_hosts because there is no checkout step in this job # Add GitHub to known_hosts because there is no checkout step in this job
echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" >> ~/.ssh/known_hosts echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" >> ~/.ssh/known_hosts
@ -207,7 +207,7 @@ jobs:
- restore_cache: - restore_cache:
key: premium-composer-{{ checksum "mailpoet-premium/composer.json" }}-{{ checksum "mailpoet-premium/composer.lock" }} key: premium-composer-{{ checksum "mailpoet-premium/composer.json" }}-{{ checksum "mailpoet-premium/composer.lock" }}
- run: - run:
name: "Set up test environment" name: 'Set up test environment'
command: | command: |
# install Premium dependencies # install Premium dependencies
MAILPOET_FREE_PATH=$(pwd)/mailpoet MAILPOET_FREE_PATH=$(pwd)/mailpoet
@ -241,7 +241,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Static analysis" name: 'Static analysis'
command: ./do qa:phpstan --php-version=<< parameters.php_version >> command: ./do qa:phpstan --php-version=<< parameters.php_version >>
qa_js: qa_js:
executor: wpcli_php_latest executor: wpcli_php_latest
@ -250,7 +250,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "QA Frontend Assets" name: 'QA Frontend Assets'
command: ./do qa:frontend-assets command: ./do qa:frontend-assets
qa_php: qa_php:
executor: wpcli_php_latest executor: wpcli_php_latest
@ -259,7 +259,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "QA PHP" name: 'QA PHP'
command: ./do qa:php command: ./do qa:php
qa_php_oldest: qa_php_oldest:
executor: wpcli_php_oldest executor: wpcli_php_oldest
@ -268,7 +268,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "QA PHP" name: 'QA PHP'
command: ./do qa:php command: ./do qa:php
qa_php_max_wporg: qa_php_max_wporg:
executor: wpcli_php_max_wporg executor: wpcli_php_max_wporg
@ -276,7 +276,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "QA PHP" name: 'QA PHP'
command: ./do qa:php-max-wporg command: ./do qa:php-max-wporg
js_tests: js_tests:
executor: wpcli_php_latest executor: wpcli_php_latest
@ -285,15 +285,15 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Preparing test results folder" name: 'Preparing test results folder'
command: mkdir test-results command: mkdir test-results
- run: - run:
name: "JS Newsletter Editor Tests" name: 'JS Newsletter Editor Tests'
command: | command: |
mkdir test-results/mocha mkdir test-results/mocha
./do t:newsletter-editor test-results/mocha/newsletter_editor_junit.xml ./do t:newsletter-editor test-results/mocha/newsletter_editor_junit.xml
- run: - run:
name: "JS Tests" name: 'JS Tests'
command: | command: |
./do t:j test-results/mocha/junit.xml ./do t:j test-results/mocha/junit.xml
- store_test_results: - store_test_results:
@ -343,10 +343,10 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Set up virtual host" name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run: - run:
name: "Pull acceptance test docker images" name: 'Pull acceptance test docker images'
# Pull docker images with 3 retries # Pull docker images with 3 retries
command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done command: i='0';while ! docker-compose -f tests/docker/docker-compose.yml pull && ((i < 3)); do sleep 3 && i=$[$i+1]; done
- when: - when:
@ -417,16 +417,16 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Set up virtual host" name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run: - run:
name: "Prepare example.com for testing" name: 'Prepare example.com for testing'
command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts
- run: - run:
name: "Set up test environment" name: 'Set up test environment'
command: source ../.circleci/setup.bash && setup php7 command: source ../.circleci/setup.bash && setup php7
- run: - run:
name: "PHP Unit tests" name: 'PHP Unit tests'
command: | command: |
./do t:u --xml ./do t:u --xml
- store_test_results: - store_test_results:
@ -453,16 +453,16 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Set up virtual host" name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run: - run:
name: "Prepare example.com for testing" name: 'Prepare example.com for testing'
command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts
- run: - run:
name: "Set up test environment" name: 'Set up test environment'
command: << parameters.setup_command >> command: << parameters.setup_command >>
- run: - run:
name: "PHP Integration tests" name: 'PHP Integration tests'
command: << parameters.run_command >> command: << parameters.run_command >>
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
@ -479,7 +479,7 @@ jobs:
- attach_workspace: - attach_workspace:
at: /home/circleci/mailpoet at: /home/circleci/mailpoet
- run: - run:
name: "Set up environment" name: 'Set up environment'
command: | command: |
source ../.circleci/setup.bash && setup php7 source ../.circleci/setup.bash && setup php7
sudo apt-get update sudo apt-get update
@ -487,7 +487,7 @@ jobs:
sed -i 's/^WP_ROOT=.*$/WP_ROOT=\/home\/circleci\/mailpoet\/wordpress/g' .env sed -i 's/^WP_ROOT=.*$/WP_ROOT=\/home\/circleci\/mailpoet\/wordpress/g' .env
echo ${CIRCLE_BUILD_NUM} > release_zip_build_number.txt echo ${CIRCLE_BUILD_NUM} > release_zip_build_number.txt
- run: - run:
name: "Build" name: 'Build'
command: ./build.sh command: ./build.sh
- store_artifacts: - store_artifacts:
path: /home/circleci/mailpoet/mailpoet/mailpoet.zip path: /home/circleci/mailpoet/mailpoet/mailpoet.zip
@ -580,7 +580,7 @@ workflows:
nightly: nightly:
triggers: triggers:
- schedule: - schedule:
cron: "0 22 * * 1-5" cron: '0 22 * * 1-5'
filters: filters:
branches: branches:
only: only:

View File

@ -4,7 +4,6 @@ about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To reproduce** **To reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to ... 1. Go to ...
2. Click on ... 2. Click on ...
3. Scroll down to ... 3. Scroll down to ...
@ -26,6 +26,7 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Versions (please complete the following information):** **Versions (please complete the following information):**
- WordPress version: [e.g: 5.3.2] - WordPress version: [e.g: 5.3.2]
- PHP version: [e.g: 7.4.2] - PHP version: [e.g: 7.4.2]
- MailPoet version: [e.g: 3.46.13] - MailPoet version: [e.g: 3.46.13]

View File

@ -4,7 +4,6 @@ about: https://feedback.mailpoet.com/
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
Please use https://feedback.mailpoet.com/ for feature requests. Please use https://feedback.mailpoet.com/ for feature requests.

22
.github/SECURITY.md vendored
View File

@ -4,7 +4,7 @@ Full details of the Automattic Security Policy can be found on [automattic.com/s
## Supported Versions ## Supported Versions
Generally, *only the latest version of MailPoet has continued support*. If a critical vulnerability is found in the current version of MailPoet, we may opt to backport any patches to previous versions. Generally, _only the latest version of MailPoet has continued support_. If a critical vulnerability is found in the current version of MailPoet, we may opt to backport any patches to previous versions.
## Reporting a Vulnerability ## Reporting a Vulnerability
@ -14,9 +14,9 @@ Generally, *only the latest version of MailPoet has continued support*. If a cri
Our most critical targets are: Our most critical targets are:
* MailPoet plugin (this repository) - MailPoet plugin (this repository)
* MailPoet Premium - MailPoet Premium
* mailpoet.com -- the primary site, and all of it subdomains, e.g. [account.mailpoet.com](https://account.mailpoet.com/) - mailpoet.com -- the primary site, and all of it subdomains, e.g. [account.mailpoet.com](https://account.mailpoet.com/)
For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic). For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic).
@ -26,12 +26,12 @@ _Please note that the **WordPress software is a separate entity** from Automatti
We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines: We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). - Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production: - Pen-testing Production:
* Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). - Please **setup a local environment** instead whenever possible. Most of our code is open source (see above).
* If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. - If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC.
* **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. - **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels.
* To be eligible for a bounty, please follow all of these guidelines. - To be eligible for a bounty, please follow all of these guidelines.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. - Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties. We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties.

View File

@ -3,7 +3,7 @@
# #
# You may wish to alter this file to override the set of languages analyzed, # You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic. # or to provide custom queries or build logic.
name: "CodeQL" name: 'CodeQL'
on: on:
push: push:

View File

@ -1,6 +1,7 @@
# Contributing # Contributing
## PHP Code ## PHP Code
- Two spaces indentation. - Two spaces indentation.
- Space between keyword (if, for, switch...) and left bracket - Space between keyword (if, for, switch...) and left bracket
- CamelCase for classes. - CamelCase for classes.
@ -15,20 +16,24 @@
- Cover your code in tests. - Cover your code in tests.
## SCSS Code ## SCSS Code
- camelCase for file name - camelCase for file name
- Components files are prefixed with underscore, to indicate, that they aren't compiled separately. - Components files are prefixed with underscore, to indicate, that they aren't compiled separately.
## JS Code ## JS Code
- Javascript code should follow the [Airbnb style guide](https://github.com/airbnb/javascript). - Javascript code should follow the [Airbnb style guide](https://github.com/airbnb/javascript).
- Prefer named export before default export in JS and TS files - Prefer named export before default export in JS and TS files
## Disabling linting rules ## Disabling linting rules
- we want to avoid using `eslint-disable` - we want to avoid using `eslint-disable`
- if we have to use it we need to use a comment explaining why do we need it: - if we have to use it we need to use a comment explaining why do we need it:
`/* eslint-disable no-new -- this class has a side-effect in the constructor and it's a library's. */` `/* eslint-disable no-new -- this class has a side-effect in the constructor and it's a library's. */`
- for PHP we do the same with the exception `// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps` which for now doesnt require an explanation - for PHP we do the same with the exception `// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps` which for now doesnt require an explanation
## Git flow ## Git flow
- Do not commit to master. - Do not commit to master.
- Open a short-living feature branch. - Open a short-living feature branch.
- Open a pull request. - Open a pull request.
@ -39,12 +44,13 @@
- Wait for review from another developer. - Wait for review from another developer.
## Issues creation ## Issues creation
- Issues are managed on Jira. - Issues are managed on Jira.
- Discuss issues on public Slack chats, discuss code in pull requests. - Discuss issues on public Slack chats, discuss code in pull requests.
- Open a small Jira issue only when it has been discussed. - Open a small Jira issue only when it has been discussed.
## Migration from IdiORM to Doctrine ## Migration from IdiORM to Doctrine
MailPoet used to use [IdiORM](https://github.com/j4mie/idiorm) as its object-relational mapper (ORM), but the project was abandoned a while ago, so we started a migration to [Doctrine](https://www.doctrine-project.org/). This is a significant effort that has been going on for quite some time. Although you will still see parts of the code that use IdioORM, we ask that all new code be added using Doctrine instead. MailPoet used to use [IdiORM](https://github.com/j4mie/idiorm) as its object-relational mapper (ORM), but the project was abandoned a while ago, so we started a migration to [Doctrine](https://www.doctrine-project.org/). This is a significant effort that has been going on for quite some time. Although you will still see parts of the code that use IdioORM, we ask that all new code be added using Doctrine instead.
All IdioORM models live in [mailpoet/lib/Models](https://github.com/mailpoet/mailpoet/tree/master/mailpoet/lib/Models), should be considered deprecated and shouldn't be used by new code. We are moving everything to Doctrine entities and some auxiliary code when needed. You can find Doctrine entities in [mailpoet/lib/Entities](https://github.com/mailpoet/mailpoet/tree/master/mailpoet/lib/Entities). All IdioORM models live in [mailpoet/lib/Models](https://github.com/mailpoet/mailpoet/tree/master/mailpoet/lib/Models), should be considered deprecated and shouldn't be used by new code. We are moving everything to Doctrine entities and some auxiliary code when needed. You can find Doctrine entities in [mailpoet/lib/Entities](https://github.com/mailpoet/mailpoet/tree/master/mailpoet/lib/Entities).

View File

@ -1,17 +1,19 @@
# MailPoet # MailPoet
The **MailPoet** plugin monorepo. The **MailPoet** plugin monorepo.
To use our Docker-based development environment (recommended), continue with the steps below. To use our Docker-based development environment (recommended), continue with the steps below.
If you'd like to use the plugin code directly, see details in [the plugin's readme](mailpoet/README.md). If you'd like to use the plugin code directly, see details in [the plugin's readme](mailpoet/README.md).
## 🔌 Initial setup ## 🔌 Initial setup
1) Run `./do setup` to pull everything and install necessary dependencies.
2) Add secrets to `.env` files in `mailpoet` and `mailpoet-premium` directories. Go to the Secret Store and look for "MailPoet: plugin .env"
3) Run `./do start` to start the stack.
4) Go to http://localhost:8888 to see the dashboard of the dev environment.
1. Run `./do setup` to pull everything and install necessary dependencies.
2. Add secrets to `.env` files in `mailpoet` and `mailpoet-premium` directories. Go to the Secret Store and look for "MailPoet: plugin .env"
3. Run `./do start` to start the stack.
4. Go to http://localhost:8888 to see the dashboard of the dev environment.
## 🔍 PHPStorm setup for XDebug ## 🔍 PHPStorm setup for XDebug
In `Languages & Preferences > PHP > Servers` set path mappings: In `Languages & Preferences > PHP > Servers` set path mappings:
```shell ```shell
@ -28,6 +30,7 @@ To use XDebug inside the **cron**, you need to pass a URL argument `&XDEBUG_TRIG
Alternatively, you can add `XDEBUG_TRIGGER: yes` to the `wordpress` service in `docker-compose.yml` and restart it (which will run XDebug also for all other requests). Alternatively, you can add `XDEBUG_TRIGGER: yes` to the `wordpress` service in `docker-compose.yml` and restart it (which will run XDebug also for all other requests).
## Xdebug develop mode ## Xdebug develop mode
[Xdebug develop mode](https://xdebug.org/docs/develop) is disabled by default because it causes performance issues due to conflicts with the DI container. [Xdebug develop mode](https://xdebug.org/docs/develop) is disabled by default because it causes performance issues due to conflicts with the DI container.
It can be enabled when needed using the `XDEBUG_MODE` environment variable. For example, it is possible to enable it by adding the following to `docker-compose.override.yml`: It can be enabled when needed using the `XDEBUG_MODE` environment variable. For example, it is possible to enable it by adding the following to `docker-compose.override.yml`:
@ -38,12 +41,15 @@ environment:
``` ```
## 💾 NFS volume sharing for Mac ## 💾 NFS volume sharing for Mac
NFS volumes can bring more stability and performance on Docker for Mac. To setup NFS volume sharing run: NFS volumes can bring more stability and performance on Docker for Mac. To setup NFS volume sharing run:
```shell ```shell
sudo sh dev/mac-nfs-setup.sh sudo sh dev/mac-nfs-setup.sh
``` ```
Then create a Docker Compose override file with NFS settings and restart containers: Then create a Docker Compose override file with NFS settings and restart containers:
```shell ```shell
cp docker-compose.override.macos-sample.yml docker-compose.override.yml cp docker-compose.override.macos-sample.yml docker-compose.override.yml
@ -55,18 +61,22 @@ docker-compose up -d
outside your `Documents` folder, otherwise you may run into [file permission issues](https://objekt.click/2019/11/docker-the-problem-with-macos-catalina/). outside your `Documents` folder, otherwise you may run into [file permission issues](https://objekt.click/2019/11/docker-the-problem-with-macos-catalina/).
# 🐶 Husky # 🐶 Husky
We use [Husky](https://github.com/typicode/husky) to run automated checks in pre-commit hooks. We use [Husky](https://github.com/typicode/husky) to run automated checks in pre-commit hooks.
In case you're using [NVM](https://github.com/nvm-sh/nvm) for Node version management you may In case you're using [NVM](https://github.com/nvm-sh/nvm) for Node version management you may
need to create or update your `~/.huskyrc` file with: need to create or update your `~/.huskyrc` file with:
```sh ```sh
# This loads nvm.sh and sets the correct PATH before running the hooks: # This loads nvm.sh and sets the correct PATH before running the hooks:
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
``` ```
Without it, you may experience errors in some Git clients. Without it, you may experience errors in some Git clients.
## 🕹 Commands ## 🕹 Commands
The `./do` script define aliases for most of the commands you will need while working on plugins: The `./do` script define aliases for most of the commands you will need while working on plugins:
```shell ```shell
@ -88,11 +98,14 @@ Options:
You can access this help in your command line running `./do` without parameters. You can access this help in your command line running `./do` without parameters.
## ✉️ Adding new templates to the plugin ## ✉️ Adding new templates to the plugin
[Read the article.](https://mailpoet.atlassian.net/wiki/spaces/MAILPOET/pages/629374977/Adding+new+templates+to+the+plugin) [Read the article.](https://mailpoet.atlassian.net/wiki/spaces/MAILPOET/pages/629374977/Adding+new+templates+to+the+plugin)
## 🚥 Testing with PHP 7.4 or PHP 8.0 ## 🚥 Testing with PHP 7.4 or PHP 8.0
To switch the environment to PHP 7.4/8.0: To switch the environment to PHP 7.4/8.0:
1) Configure the `wordpress` service in `docker-compose.override.yml` to build from the php74 Dockerfile:
1. Configure the `wordpress` service in `docker-compose.override.yml` to build from the php74 Dockerfile:
```yaml ```yaml
wordpress: wordpress:
@ -100,11 +113,13 @@ To switch the environment to PHP 7.4/8.0:
context: . context: .
dockerfile: docker/php74/Dockerfile # OR docker/php80/Dockerfile dockerfile: docker/php74/Dockerfile # OR docker/php80/Dockerfile
``` ```
3) Run `docker-compose build wordpress`.
4) Start the stack with `./do start`. 2. Run `docker-compose build wordpress`.
3. Start the stack with `./do start`.
To switch back to PHP 8.1 remove what was added in 1) and, run `docker-compose build wordpress` for application container and `docker-compose build test_wordpress` for tests container, To switch back to PHP 8.1 remove what was added in 1) and, run `docker-compose build wordpress` for application container and `docker-compose build test_wordpress` for tests container,
and start the stack using `./do start`. and start the stack using `./do start`.
## ✅ TODO ## ✅ TODO
- install woo commerce, members and other useful plugins by default - install woo commerce, members and other useful plugins by default

View File

@ -8,7 +8,7 @@ thanks for understanding.
- [Support](https://www.mailpoet.com/support) - [Support](https://www.mailpoet.com/support)
- [Feature Requests](https://feedback.mailpoet.com) - [Feature Requests](https://feedback.mailpoet.com)
*DO NOT* use the issue tracker to ask questions; _DO NOT_ use the issue tracker to ask questions;
use the links above for that. use the links above for that.
Questions posed to the issue tracker will be closed. Questions posed to the issue tracker will be closed.

View File

@ -1,13 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>MailPoet dev environment</title> <title>MailPoet dev environment</title>
<link rel="icon" href="favicon.png" /> <link rel="icon" href="favicon.png" />
<style> <style>
body { body {
background: #ffe0d0; background: #ffe0d0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
} }
p { p {
color: #071c6d; color: #071c6d;
@ -38,9 +40,9 @@
display: block; display: block;
} }
</style> </style>
</head> </head>
<body> <body>
<a href="/"><img src="logo.svg" /></a> <a href="/"><img src="logo.svg" /></a>
<p>Dev environment</p> <p>Dev environment</p>
<table> <table>
@ -66,11 +68,17 @@
<tr> <tr>
<td>💾</td> <td>💾</td>
<td> <td>
<a href="http://localhost:8081?server=db&mysql=wordpress&username=wordpress">Adminer</a> <a
href="http://localhost:8081?server=db&mysql=wordpress&username=wordpress"
>Adminer</a
>
</td> </td>
<td>DB management</td> <td>DB management</td>
<td> <td>
<a href="http://localhost:8081?server=db&mysql=wordpress&username=wordpress">http://localhost:8081</a> <a
href="http://localhost:8081?server=db&mysql=wordpress&username=wordpress"
>http://localhost:8081</a
>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -81,5 +89,5 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</body> </body>
</html> </html>

View File

@ -4,33 +4,39 @@ This is a place where we put documentation for developers who want to build an e
If you are a user looking for a user guide please visit our [knowledge base](https://kb.mailpoet.com/). If you are a user looking for a user guide please visit our [knowledge base](https://kb.mailpoet.com/).
## MailPoet API ## MailPoet API
MailPoet API is the officially supported way to integrate with the MailPoet 3 plugin. It focuses on functionality for managing subscribers. MailPoet API is the officially supported way to integrate with the MailPoet 3 plugin. It focuses on functionality for managing subscribers.
Developers integrating MailPoet functionality in their own plugins or projects are strongly discouraged against using other functions and classes within MailPoet codebase! We are continually refactoring as part of our rapid development process, and backward compatibility is not guaranteed. Developers integrating MailPoet functionality in their own plugins or projects are strongly discouraged against using other functions and classes within MailPoet codebase! We are continually refactoring as part of our rapid development process, and backward compatibility is not guaranteed.
### Basics ### Basics
MailPoet API is distributed within MailPoet3 plugin and it is implemented as a PHP class. MailPoet API is distributed within MailPoet3 plugin and it is implemented as a PHP class.
Currently supported version is `v1`. Currently supported version is `v1`.
### Instantiation ### Instantiation
```php ```php
if (class_exists(\MailPoet\API\API::class)) { if (class_exists(\MailPoet\API\API::class)) {
$mailpoet_api = \MailPoet\API\API::MP('v1'); $mailpoet_api = \MailPoet\API\API::MP('v1');
} }
``` ```
Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by WordPress. Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by WordPress.
### Available API Methods ### Available API Methods
* [Add List (addList)](api_methods/AddList.md)
* [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md) - [Add List (addList)](api_methods/AddList.md)
* [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md) - [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md)
* [Get Lists (getLists)](api_methods/GetLists.md) - [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
* [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md) - [Get Lists (getLists)](api_methods/GetLists.md)
* [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md) - [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
* [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md) - [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md)
* [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md) - [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md)
* [Subscribe to List (subscribeToLists)](api_methods/SubscribeToLists.md) - [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md)
* [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md) - [Subscribe to List (subscribeToLists)](api_methods/SubscribeToLists.md)
* [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md) - [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
- [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)
### Usage examples ### Usage examples
You can check some basic examples [here](UsageExamples.md). You can check some basic examples [here](UsageExamples.md).

View File

@ -1,9 +1,11 @@
[back to readme](Readme.md) [back to readme](Readme.md)
# Usage Examples # Usage Examples
Common usage is a rendering of a subscription form and processing it. Common usage is a rendering of a subscription form and processing it.
## Fetching data for a subscription form ## Fetching data for a subscription form
```php ```php
<?php <?php
@ -18,6 +20,7 @@ if (class_exists(\MailPoet\API\API::class)) {
``` ```
## Processing a subscription form ## Processing a subscription form
```php ```php
<?php <?php

View File

@ -9,14 +9,15 @@ In MailPoet, subscribers are organized into lists. This method provides function
It returns the new list. See [Get Lists](GetLists.md) for a list data structure description. It returns the new list. See [Get Lists](GetLists.md) for a list data structure description.
## Arguments ## Arguments
### `$list` (required) ### `$list` (required)
An associative array which contains list data. An associative array which contains list data.
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ---------------------- | ------------ | --------- | -------------------------- |
| name (required) | string | 90 chars | A name of the list. | | name (required) | string | 90 chars | A name of the list. |
| description (optional) | string\|null| 250 chars | A description of the list. | | description (optional) | string\|null | 250 chars | A description of the list. |
## Error handling ## Error handling
@ -28,7 +29,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | -------------------------------------------- |
| 14 | Missing list name | | 14 | Missing list name |
| 15 | Trying to create a list that already exists | | 15 | Trying to create a list that already exists |
| 16 | The list couldnt be created in the database | | 16 | The list couldnt be created in the database |

View File

@ -8,9 +8,9 @@ This method allows a subscriber to be created, adds them into lists, and handles
If sign-up confirmation (double opt-in) is enabled in the MailPoet settings a subscriber is created with status `unconfirmed` otherwise the status is set to `subscribed`. If sign-up confirmation (double opt-in) is enabled in the MailPoet settings a subscriber is created with status `unconfirmed` otherwise the status is set to `subscribed`.
- *A confirmation email* is an email which is sent to a subscriber so that they can confirm his subscription. It is sent only if sign-up confirmation is enabled in the MailPoet settings. - _A confirmation email_ is an email which is sent to a subscriber so that they can confirm his subscription. It is sent only if sign-up confirmation is enabled in the MailPoet settings.
- *A welcome email* is an automatic email which is sent to a new subscriber. This email is scheduled only if sign-up confirmation is disabled and a welcome email is configured for some of given lists. In case of required sign-up confirmation, it is scheduled later after a subscriber confirms the subscription. - _A welcome email_ is an automatic email which is sent to a new subscriber. This email is scheduled only if sign-up confirmation is disabled and a welcome email is configured for some of given lists. In case of required sign-up confirmation, it is scheduled later after a subscriber confirms the subscription.
- *An admin notification email* is sent to the site admin to inform them about a new subscription. It is sent only if the notification feature is enabled in the MailPoet setting. - _An admin notification email_ is sent to the site admin to inform them about a new subscription. It is sent only if the notification feature is enabled in the MailPoet setting.
All these emails can be disabled using `$options`. All these emails can be disabled using `$options`.
@ -21,30 +21,33 @@ There might be other `\Exceptions` because of some invalid input data such a inv
It returns a new subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure. It returns a new subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure.
## Arguments ## Arguments
### `$subscriber` (required) ### `$subscriber` (required)
An associative array containing subscriber data which contains default properties (email, first_name, last_name) and custom subscriber fields which were defined in MailPoet. An associative array containing subscriber data which contains default properties (email, first_name, last_name) and custom subscriber fields which were defined in MailPoet.
It has to contain an email and all required custom fields. To get defined custom fields see [Get Subscriber Fields](GetSubscriberFields.md) It has to contain an email and all required custom fields. To get defined custom fields see [Get Subscriber Fields](GetSubscriberFields.md)
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | -------------------------- | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| email (required) | string | 150 chars | a valid email address | | email (required) | string | 150 chars | a valid email address |
| first_name (optional) | string/null | 255 chars | Fist name of the subscriber. | | first_name (optional) | string/null | 255 chars | Fist name of the subscriber. |
| last_name (optional) | string/null | 255 chars | Last name of the subscriber. | | last_name (optional) | string/null | 255 chars | Last name of the subscriber. |
| cf_* (optional/required) | string/boolean/null | 65K chars | A custom field (see [Get Subscriber Fields](GetSubscriberFields.md)). <br> If a custom field is a checkbox, send truthy or falsy value (`true`/`false, `1`/`0` or `"1"`\`"0"`). | | cf\_\* (optional/required) | string/boolean/null | 65K chars | A custom field (see [Get Subscriber Fields](GetSubscriberFields.md)). <br> If a custom field is a checkbox, send truthy or falsy value (`true`/`false, `1`/`0`or`"1"`\`"0"`). |
### `$list_ids` (optional) ### `$list_ids` (optional)
An array containing list ids into which subscriber will be added. An array containing list ids into which subscriber will be added.
In case that the list is empty a subscriber will be created; but sending a confirmation email, notification email and scheduling welcome email will be skipped. In case that the list is empty a subscriber will be created; but sending a confirmation email, notification email and scheduling welcome email will be skipped.
### `$options` (optional) ### `$options` (optional)
All options are optional. If omitted a default value is used. All options are optional. If omitted a default value is used.
| Option | Type | Default | Description | | Option | Type | Default | Description |
| --- | --- | --- | --- | | ---------------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| send_confirmation_email | boolean | true | Can be used to disable a confirmation email. Otherwise, a confirmation email is sent as described above. It is strongly recommended to keep this option set to `true` so that MailPoet settings for sign-up confirmation are respected. Turning it to `false` might lead that subscriber to be added as `unconfirmed`. | | send_confirmation_email | boolean | true | Can be used to disable a confirmation email. Otherwise, a confirmation email is sent as described above. It is strongly recommended to keep this option set to `true` so that MailPoet settings for sign-up confirmation are respected. Turning it to `false` might lead that subscriber to be added as `unconfirmed`. |
| schedule_welcome_email | boolean | true | Can be used to disable a welcome email. Otherwise, a welcome email is scheduled as described above.| | schedule_welcome_email | boolean | true | Can be used to disable a welcome email. Otherwise, a welcome email is scheduled as described above. |
| skip_subscriber_notification | boolean | false | Can be used to disable an admin notification email. Otherwise, an admin notification email is sent as described above.| | skip_subscriber_notification | boolean | false | Can be used to disable an admin notification email. Otherwise, an admin notification email is sent as described above. |
## Error handling ## Error handling
@ -56,7 +59,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | -------------------------------------------------- |
| 11 | Missing email address | | 11 | Missing email address |
| 12 | Trying to create a subscriber that already exists | | 12 | Trying to create a subscriber that already exists |
| 13 | The subscriber couldnt be created in the database | | 13 | The subscriber couldnt be created in the database |

View File

@ -12,7 +12,7 @@ See [Subscriber Fields for more details](GetSubscriberFields.md)
### `$data` (required) ### `$data` (required)
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------- |
| name (required) | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. | | name (required) | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. |
| type (required) | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` | | type (required) | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` |
| params (optional) | array | - | Contains various information, see examples below. | | params (optional) | array | - | Contains various information, see examples below. |
@ -23,40 +23,40 @@ Params array differs for each type.
The common properties for all types: The common properties for all types:
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | -------- | ------ | ------------------------------------------------------------------------------------------- |
| required | string | Indicates if the value must be provided for each subscriber. Possible values are: "1" or "" | | required | string | Indicates if the value must be provided for each subscriber. Possible values are: "1" or "" |
| label | string | Label used for displaying the field to the end user. | | label | string | Label used for displaying the field to the end user. |
#### `$params` for text, textarea types #### `$params` for text, textarea types
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | -------- | ------ | ------------------------------------------------------------------------------------------- |
| validate | string | Can be used for validating input values. Possible values are: `number`, `alphanum`, `phone` | | validate | string | Can be used for validating input values. Possible values are: `number`, `alphanum`, `phone` |
#### `$params` for checkbox types #### `$params` for checkbox types
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | -------- | ----- | ------------------------------------------------------------ |
| values | array | Same array as for radio type. Must contain exactly 1 element | | values | array | Same array as for radio type. Must contain exactly 1 element |
#### `$params` for radio, select types #### `$params` for radio, select types
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | -------- | ----- | --------------------------------------------------------------------------------------------------- |
| values | array | Contains a list of options. Each element must contain a string `value` and can contain `is_checked` | | values | array | Contains a list of options. Each element must contain a string `value` and can contain `is_checked` |
#### `$params` for date type #### `$params` for date type
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| date_type | string | Possible values are: Values: `year_month_day`, `year_month`, `month`, `day` | | date_type | string | Possible values are: Values: `year_month_day`, `year_month`, `month`, `day` |
| date_format | string | Values: for year_month_day: `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY/MM/DD`, for year_month: `YYYY/MM`, `MM/YY`, for year: `YYYY`, for month: `MM` | | date_format | string | Values: for year_month_day: `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY/MM/DD`, for year_month: `YYYY/MM`, `MM/YY`, for year: `YYYY`, for month: `MM` |
## Response ## Response
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ---------- | ------------ | -------- | ------------------------------------------------------------------------------------------------- |
| id | string | 11 chars |Field Id | | id | string | 11 chars | Field Id |
| name | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. | | name | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. |
| type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` | | type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` |
| params | array | - | Contains various information, see examples below. | | params | array | - | Contains various information, see examples below. |
@ -73,7 +73,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | ---------------------------------------------------------------------------------- |
| 1 | The subscriber couldnt be created in the database | | 1 | The subscriber couldnt be created in the database |
| 1001 | Missing a mandatory field in the `$data` argument | | 1001 | Missing a mandatory field in the `$data` argument |
| 1002 | A mandatory field in the `$data` argument has wrong type | | 1002 | A mandatory field in the `$data` argument has wrong type |

View File

@ -9,7 +9,7 @@ In MailPoet, subscribers are organized into lists. This method returns an array
### A list data structure ### A list data structure
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ----------- | ------------ | --------- | ----------------------------------------------------------------------------------------------------------- |
| id | string | 11 chars | Id of the list | | id | string | 11 chars | Id of the list |
| name | string | 90 chars | Name of the list | | name | string | 90 chars | Name of the list |
| type | string | - | Type of the list. Currently, there is only one supported value: `default` | | type | string | - | Type of the list. Currently, there is only one supported value: `default` |
@ -19,6 +19,7 @@ In MailPoet, subscribers are organized into lists. This method returns an array
| deleted_at | string\|null | - | This property is not null only when the list is in the trash. It contains UTC time in 'Y-m-d H:i:s' format. | | deleted_at | string\|null | - | This property is not null only when the list is in the trash. It contains UTC time in 'Y-m-d H:i:s' format. |
### Response Example ### Response Example
```php ```php
<?php <?php
[ [

View File

@ -7,15 +7,17 @@
This method throws an `\Exception` in the event a subscriber with a given email address doesnt exist. This method throws an `\Exception` in the event a subscriber with a given email address doesnt exist.
## Arguments ## Arguments
| Argument | Type | Description | | Argument | Type | Description |
| --- | --- | --- | | ----------------- | ------ | --------------------- |
| $subscriber_email | string | a valid email address | | $subscriber_email | string | a valid email address |
## A subscriber data structure ## A subscriber data structure
### Subscriber ### Subscriber
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ------------------------ | ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| id | string | 11 chars | Id of the subscriber | | id | string | 11 chars | Id of the subscriber |
| wp_user_id | string\|null | 20 chars | Id of a WordPress user associated with the subscriber | | wp_user_id | string\|null | 20 chars | Id of a WordPress user associated with the subscriber |
| is_woocommerce_user | string | - | A flag telling whether the user is also a WooCommerce customer. Possible values are: `1`, `0` | | is_woocommerce_user | string | - | A flag telling whether the user is also a WooCommerce customer. Possible values are: `1`, `0` |
@ -30,14 +32,15 @@ This method throws an `\Exception` in the event a subscriber with a given email
| updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format | | updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format |
| deleted_at | string\|null | - | This property in not null in case that list is in trash and contains UTC time in 'Y-m-d H:i:s' format. | | deleted_at | string\|null | - | This property in not null in case that list is in trash and contains UTC time in 'Y-m-d H:i:s' format. |
| unconfirmed_data | string\|null | 65K chars | May contain serialized subscriber data in case when there are pending changes waiting for a confirmation from a subscriber | | unconfirmed_data | string\|null | 65K chars | May contain serialized subscriber data in case when there are pending changes waiting for a confirmation from a subscriber |
| source | string\|null | - | Possible values: `form`,`imported`,`administrator`,`api`,`wordpress_user`,`woocommerce_user`,`woocommerce_checkout`,`unknown`) | source | string\|null | - | Possible values: `form`,`imported`,`administrator`,`api`,`wordpress_user`,`woocommerce_user`,`woocommerce_checkout`,`unknown`) |
| count_confirmations | string | 11 chars | Counter for confirmation emails | | count_confirmations | string | 11 chars | Counter for confirmation emails |
| subscriptions | array | - | List of subcriber subscriptions | | subscriptions | array | - | List of subcriber subscriptions |
| cf_{custom_field['id']}| string | 65K chars | A custom subscriber field value (see [Get Subscriber Fields](GetSubscriberFields.md)| | cf\_{custom_field['id']} | string | 65K chars | A custom subscriber field value (see [Get Subscriber Fields](GetSubscriberFields.md) |
### Subscriber's subscription ### Subscriber's subscription
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | ------------- | ------ | -------- | ------------------------------------------------------------------------------------ |
| id | string | 11 chars | Id of relation | | id | string | 11 chars | Id of relation |
| subscriber_id | string | 11 chars | Id of subscriber | | subscriber_id | string | 11 chars | Id of subscriber |
| segment_id | string | 11 chars | Id of a list | | segment_id | string | 11 chars | Id of a list |
@ -46,6 +49,7 @@ This method throws an `\Exception` in the event a subscriber with a given email
| updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format | | updated_at | string | - | UTC time of last update in 'Y-m-d H:i:s' format |
### Response Example ### Response Example
```php ```php
<?php <?php
[ [
@ -98,5 +102,5 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | -------------------------------------------- |
| 4 | Asking for a subscriber that does not exist. | | 4 | Asking for a subscriber that does not exist. |

View File

@ -12,13 +12,14 @@ See also [addSubscriberField function.](AddSubscriberField.md)
## Subscriber Field ## Subscriber Field
| Property | Type | Limits | Description | | Property | Type | Limits | Description |
| --- | --- | --- | --- | | -------- | ------ | -------- | ------------------------------------------------------------------------------------------------- |
| id | string | 11 chars |Field Id | | id | string | 11 chars | Field Id |
| name | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. | | name | string | 90 chars | Human readable name. Intended to be used, as an example, as a label for form input. |
| type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` | | type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` |
| params | array | - | Contains various information, see examples below. | | params | array | - | Contains various information, see examples below. |
## Response Example ## Response Example
```php ```php
<?php <?php
[ [

View File

@ -6,29 +6,33 @@
This method allows adding an existing subscriber into lists and handles confirmation email and admin notification email sending. This method allows adding an existing subscriber into lists and handles confirmation email and admin notification email sending.
- *A confirmation email* is an email which is sent to a subscriber so they can confirm their subscription. It is sent only if sign-up confirmation is enabled in MailPoet settings and subscriber has not received any confirmation email yet. - _A confirmation email_ is an email which is sent to a subscriber so they can confirm their subscription. It is sent only if sign-up confirmation is enabled in MailPoet settings and subscriber has not received any confirmation email yet.
- *A welcome email* is an automatic email which is sent to a new subscriber. This email is scheduled only if sign-up confirmation is disabled and some welcome email is configured for some of given lists. - _A welcome email_ is an automatic email which is sent to a new subscriber. This email is scheduled only if sign-up confirmation is disabled and some welcome email is configured for some of given lists.
- *An admin notification email* is sent to the site admin to inform them about a new subscription. It is sent only if the notification feature is enabled in the MailPoet setting. - _An admin notification email_ is sent to the site admin to inform them about a new subscription. It is sent only if the notification feature is enabled in the MailPoet setting.
All these emails can be disabled using `$options`. All these emails can be disabled using `$options`.
It returns a subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure. It returns a subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure.
## Arguments ## Arguments
### string `$subscriber_id` (required) ### string `$subscriber_id` (required)
An id or an email address. An `\Exception` is thrown when the value doesn't match any subscriber. An id or an email address. An `\Exception` is thrown when the value doesn't match any subscriber.
### array `$list_ids` (required) ### array `$list_ids` (required)
An array of list ids. An `\Exception` is thrown if any of list ids are invalid. In such a case the subscriber isn't added to any list. An array of list ids. An `\Exception` is thrown if any of list ids are invalid. In such a case the subscriber isn't added to any list.
### array `$options` (optional) ### array `$options` (optional)
All options are optional. If omitted, a default value is used. All options are optional. If omitted, a default value is used.
| Option | Type | Default | Description | | Option | Type | Default | Description |
| --- | --- | --- | --- | | ---------------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| send_confirmation_email | boolean | true | Can be used to disable confirmation email. Otherwise, a confirmation email is sent as described above.| | send_confirmation_email | boolean | true | Can be used to disable confirmation email. Otherwise, a confirmation email is sent as described above. |
| schedule_welcome_email | boolean | true | Can be used to disable welcome email. Otherwise, a welcome email is scheduled as described above.| | schedule_welcome_email | boolean | true | Can be used to disable welcome email. Otherwise, a welcome email is scheduled as described above. |
| skip_subscriber_notification | boolean | false | Can be used to disable an admin notification email. Otherwise, an admin notification email is sent as described above.| | skip_subscriber_notification | boolean | false | Can be used to disable an admin notification email. Otherwise, an admin notification email is sent as described above. |
## Error handling ## Error handling
@ -40,7 +44,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | ------------------------------------------------------- |
| 3 | No lists provided | | 3 | No lists provided |
| 4 | Invalid subscriber that does not exist | | 4 | Invalid subscriber that does not exist |
| 5 | Invalid list that does not exist | | 5 | Invalid list that does not exist |

View File

@ -9,10 +9,13 @@ This method removes a subscriber from given lists.
It returns a subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure. It returns a subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure.
## Arguments ## Arguments
### string `$subscriber_id` (required) ### string `$subscriber_id` (required)
An id or email of an existing subscriber. An `\Exception` is thrown when an id or email doesn't match any subscriber. An id or email of an existing subscriber. An `\Exception` is thrown when an id or email doesn't match any subscriber.
### array `$list_ids` (required) ### array `$list_ids` (required)
An array of list ids. An `\Exception` is thrown if any of list ids are invalid. In such a case the subscriber remains subscribed to all lists. An array of list ids. An `\Exception` is thrown if any of list ids are invalid. In such a case the subscriber remains subscribed to all lists.
## Error handling ## Error handling
@ -25,7 +28,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description: Codes description:
| Code | Description | | Code | Description |
| --- | --- | | ---- | --------------------------------------------------- |
| 3 | No lists provided | | 3 | No lists provided |
| 4 | Invalid subscriber that does not exist | | 4 | Invalid subscriber that does not exist |
| 5 | Invalid list that does not exist | | 5 | Invalid list that does not exist |

View File

@ -22,16 +22,16 @@ volumes:
driver_opts: driver_opts:
type: nfs type: nfs
o: addr=host.docker.internal,nolock o: addr=host.docker.internal,nolock
device: ":/System/Volumes/Data${PWD}/wordpress" device: ':/System/Volumes/Data${PWD}/wordpress'
nfs-mailpoet: nfs-mailpoet:
driver: local driver: local
driver_opts: driver_opts:
type: nfs type: nfs
o: addr=host.docker.internal,nolock o: addr=host.docker.internal,nolock
device: ":/System/Volumes/Data${PWD}/mailpoet" device: ':/System/Volumes/Data${PWD}/mailpoet'
nfs-mailpoet-premium: nfs-mailpoet-premium:
driver: local driver: local
driver_opts: driver_opts:
type: nfs type: nfs
o: addr=host.docker.internal,nolock o: addr=host.docker.internal,nolock
device: ":/System/Volumes/Data${PWD}/mailpoet-premium" device: ':/System/Volumes/Data${PWD}/mailpoet-premium'

View File

@ -31,8 +31,8 @@ services:
UID: ${UID:-1000} UID: ${UID:-1000}
GID: ${GID:-1000} GID: ${GID:-1000}
ports: ports:
- "8002:80" - '8002:80'
- "8083:8083" # Storybook port number, see package.json - '8083:8083' # Storybook port number, see package.json
depends_on: depends_on:
- db - db
- smtp - smtp
@ -43,17 +43,17 @@ services:
WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_PASSWORD: wordpress
PHP_IDE_CONFIG: "serverName=Mailpoet" PHP_IDE_CONFIG: 'serverName=Mailpoet'
COMPOSER_HOME: "/tmp/.composer" COMPOSER_HOME: '/tmp/.composer'
NPM_CONFIG_CACHE: "/tmp/.npm" NPM_CONFIG_CACHE: '/tmp/.npm'
MAILPOET_DEV_SITE: 1 MAILPOET_DEV_SITE: 1
volumes: volumes:
- "./wordpress:/var/www/html" - './wordpress:/var/www/html'
- "./eslint-config:/var/www/html/wp-content/plugins/eslint-config" - './eslint-config:/var/www/html/wp-content/plugins/eslint-config'
- "./tsconfig.base.json:/var/www/html/wp-content/plugins/tsconfig.base.json:ro" - './tsconfig.base.json:/var/www/html/wp-content/plugins/tsconfig.base.json:ro'
- "./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'
- "./templates:/var/www/templates" - './templates:/var/www/templates'
test_wordpress: test_wordpress:
container_name: mp-test-wp container_name: mp-test-wp
@ -64,7 +64,7 @@ services:
UID: ${UID:-1000} UID: ${UID:-1000}
GID: ${GID:-1000} GID: ${GID:-1000}
ports: ports:
- "8003:80" - '8003:80'
depends_on: depends_on:
- db - db
- smtp - smtp
@ -74,11 +74,11 @@ services:
WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_PASSWORD: wordpress
PHP_IDE_CONFIG: "serverName=Mailpoet" PHP_IDE_CONFIG: 'serverName=Mailpoet'
volumes: volumes:
- "./eslint-config:/var/www/html/wp-content/plugins/eslint-config" - './eslint-config:/var/www/html/wp-content/plugins/eslint-config'
- "./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'
smtp: smtp:
container_name: mp-mailhog container_name: mp-mailhog
@ -88,9 +88,9 @@ services:
MH_STORAGE: maildir MH_STORAGE: maildir
MH_MAILDIR_PATH: /output MH_MAILDIR_PATH: /output
volumes: volumes:
- "./dev/data/mailhog:/output" - './dev/data/mailhog:/output'
ports: ports:
- "8082:8025" - '8082:8025'
adminer: adminer:
container_name: mp-adminer container_name: mp-adminer
@ -98,9 +98,9 @@ services:
depends_on: depends_on:
- db - db
ports: ports:
- "8081:8080" - '8081:8080'
volumes: volumes:
- "./dev/php.ini:/usr/local/etc/php/conf.d/custom.ini" - './dev/php.ini:/usr/local/etc/php/conf.d/custom.ini'
volumes: volumes:
my-datavolume: my-datavolume:

View File

@ -1,9 +1,5 @@
{ {
"extends": [ "extends": ["airbnb", "plugin:react/jsx-runtime", "prettier"],
"airbnb",
"plugin:react/jsx-runtime",
"prettier"
],
"env": { "env": {
"browser": true "browser": true
}, },
@ -14,9 +10,7 @@
"jsx": true "jsx": true
} }
}, },
"plugins": [ "plugins": ["react-hooks"],
"react-hooks"
],
"settings": { "settings": {
"import/resolver": "webpack" "import/resolver": "webpack"
}, },
@ -27,9 +21,12 @@
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "warn",
// Exceptions // Exceptions
"import/extensions": 0, // we wouldn't be able to import jQuery without this line "import/extensions": 0, // we wouldn't be able to import jQuery without this line
"jsx-a11y/anchor-is-valid": [ "error", { "jsx-a11y/anchor-is-valid": [
"components": [ "Link" ], "error",
"specialLink": [ "to" ] {
}] "components": ["Link"],
"specialLink": ["to"]
}
]
} }
} }

View File

@ -22,11 +22,7 @@
"jsx": true "jsx": true
} }
}, },
"plugins": [ "plugins": ["react-hooks", "no-only-tests", "@typescript-eslint"],
"react-hooks",
"no-only-tests",
"@typescript-eslint"
],
"settings": { "settings": {
"import/resolver": "webpack" "import/resolver": "webpack"
}, },
@ -49,19 +45,28 @@
"import/prefer-default-export": 0, // we want to stop using default exports and start using named exports "import/prefer-default-export": 0, // we want to stop using default exports and start using named exports
"react/destructuring-assignment": 0, // that would be too many changes to fix this one "react/destructuring-assignment": 0, // that would be too many changes to fix this one
"prefer-destructuring": 0, // that would be too many changes to fix this one "prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, { "jsx-a11y/label-has-for": [
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those 2,
}], {
"required": { "some": ["nesting", "id"] } // some of our labels are hidden and we cannot nest those
}
],
"jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes "jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes
"jsx-a11y/label-has-associated-control": [ 2, { "jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor "either": "either" // control has to be either nested or associated via htmlFor
}] }
]
}, },
"overrides": [ "overrides": [
{ {
"files": ["**/_stories/*.tsx"], "files": ["**/_stories/*.tsx"],
"rules": { "rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }] "import/no-extraneous-dependencies": [
"error",
{ "devDependencies": true }
]
} }
} }
] ]

View File

@ -1,8 +1,5 @@
{ {
"extends": [ "extends": ["airbnb/legacy", "prettier"],
"airbnb/legacy",
"prettier"
],
"env": { "env": {
"amd": true, "amd": true,
"browser": true "browser": true

View File

@ -1,9 +1,5 @@
{ {
"extends": [ "extends": ["airbnb", "plugin:react/jsx-runtime", "prettier"],
"airbnb",
"plugin:react/jsx-runtime",
"prettier"
],
"env": { "env": {
"amd": true, "amd": true,
"browser": true, "browser": true,
@ -16,10 +12,7 @@
"jsx": true "jsx": true
} }
}, },
"plugins": [ "plugins": ["react-hooks", "no-only-tests"],
"react-hooks",
"no-only-tests"
],
"settings": { "settings": {
"import/resolver": "webpack" "import/resolver": "webpack"
}, },
@ -36,13 +29,19 @@
"import/prefer-default-export": 0, // we want to stop using default exports and start using named exports "import/prefer-default-export": 0, // we want to stop using default exports and start using named exports
"react/destructuring-assignment": 0, // that would be too many changes to fix this one "react/destructuring-assignment": 0, // that would be too many changes to fix this one
"prefer-destructuring": 0, // that would be too many changes to fix this one "prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, { "jsx-a11y/label-has-for": [
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those 2,
}], {
"required": { "some": ["nesting", "id"] } // some of our labels are hidden and we cannot nest those
}
],
"jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes "jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes
"jsx-a11y/label-has-associated-control": [ 2, { "jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor "either": "either" // control has to be either nested or associated via htmlFor
}] }
]
}, },
"overrides": [ "overrides": [
{ {
@ -53,4 +52,3 @@
} }
] ]
} }

View File

@ -22,11 +22,7 @@
"jsx": true "jsx": true
} }
}, },
"plugins": [ "plugins": ["react-hooks", "no-only-tests", "@typescript-eslint"],
"react-hooks",
"no-only-tests",
"@typescript-eslint"
],
"settings": { "settings": {
"import/resolver": "webpack", "import/resolver": "webpack",
"import/core-modules": [ "import/core-modules": [
@ -45,10 +41,7 @@
] ]
}, },
"rules": { "rules": {
"react/no-unstable-nested-components": [ "react/no-unstable-nested-components": ["error", { "allowAsProps": true }],
"error",
{ "allowAsProps": true }
],
// PropTypes // PropTypes
"react/prop-types": 0, "react/prop-types": 0,
"react/jsx-props-no-spreading": 0, "react/jsx-props-no-spreading": 0,
@ -59,7 +52,7 @@
// Exceptions // Exceptions
"@typescript-eslint/no-explicit-any": "error", // make it an error instead of warning - we treat them the same, this is more visible "@typescript-eslint/no-explicit-any": "error", // make it an error instead of warning - we treat them the same, this is more visible
"no-void": 0, // can conflict with @typescript-eslint/no-floating-promises "no-void": 0, // can conflict with @typescript-eslint/no-floating-promises
"react/jsx-no-useless-fragment" : [ "react/jsx-no-useless-fragment": [
"error", "error",
{ {
"allowExpressions": true "allowExpressions": true
@ -76,13 +69,19 @@
"import/prefer-default-export": 0, // we want to stop using default exports and start using named exports "import/prefer-default-export": 0, // we want to stop using default exports and start using named exports
"react/destructuring-assignment": 0, // that would be too many changes to fix this one "react/destructuring-assignment": 0, // that would be too many changes to fix this one
"prefer-destructuring": 0, // that would be too many changes to fix this one "prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, { "jsx-a11y/label-has-for": [
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those 2,
}], {
"required": { "some": ["nesting", "id"] } // some of our labels are hidden and we cannot nest those
}
],
"jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes "jsx-a11y/anchor-is-valid": 0, // cannot fix this one, it would break wprdpress themes
"jsx-a11y/label-has-associated-control": [ 2, { "jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor "either": "either" // control has to be either nested or associated via htmlFor
}] }
]
}, },
"overrides": [ "overrides": [
{ {
@ -96,7 +95,10 @@
{ {
"files": ["**/_stories/*.tsx"], "files": ["**/_stories/*.tsx"],
"rules": { "rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }] "import/no-extraneous-dependencies": [
"error",
{ "devDependencies": true }
]
} }
}, },
{ {

View File

@ -1,6 +1,6 @@
const path = require('path'); const path = require('path');
const modulesDir = path.join( __dirname, '../node_modules' ); const modulesDir = path.join(__dirname, '../node_modules');
console.log('NODE', modulesDir); console.log('NODE', modulesDir);
// Workaround for Emotion 11 // Workaround for Emotion 11
// https://github.com/storybookjs/storybook/pull/13300#issuecomment-783268111 // https://github.com/storybookjs/storybook/pull/13300#issuecomment-783268111

View File

@ -6,4 +6,9 @@ import '../assets/dist/css/mailpoet-plugin.css';
import '../assets/dist/css/mailpoet-form-editor.css'; import '../assets/dist/css/mailpoet-form-editor.css';
addDecorator(withPerformance); addDecorator(withPerformance);
addDecorator(story => <div className="wp-core-ui" id="wpbody"><div id="mailpoet-modal"></div>{story()}</div>); addDecorator((story) => (
<div className="wp-core-ui" id="wpbody">
<div id="mailpoet-modal"></div>
{story()}
</div>
));

View File

@ -1,110 +1,106 @@
{ {
"plugins": [ 'plugins': ['stylelint-order', 'stylelint-scss'],
"stylelint-order", 'customSyntax': 'postcss-scss',
"stylelint-scss" 'rules':
{
'at-rule-empty-line-before':
['always', { except: ['first-nested', 'blockless-after-blockless'] }],
'at-rule-name-case': 'lower',
'at-rule-semicolon-newline-after': 'always',
'block-closing-brace-empty-line-before': 'never',
'block-closing-brace-newline-after': 'always',
'block-closing-brace-newline-before': 'always-multi-line',
'block-closing-brace-space-before': 'always-single-line',
'block-no-empty': true,
'block-opening-brace-newline-after': 'always-multi-line',
'block-opening-brace-space-after': 'always-single-line',
'block-opening-brace-space-before': 'always',
'color-hex-case': 'lower',
'color-hex-length': 'short',
'color-no-invalid-hex': true,
'comment-no-empty': true,
'comment-whitespace-inside': 'always',
'declaration-bang-space-after': 'never',
'declaration-bang-space-before': 'always',
'declaration-block-no-duplicate-properties':
[true, { ignore: ['consecutive-duplicates-with-different-values'] }],
'declaration-block-no-redundant-longhand-properties':
[true, { ignoreShorthands: [/flex/, /grid/] }],
'declaration-block-semicolon-newline-after': 'always-multi-line',
'declaration-block-semicolon-space-after': 'always-single-line',
'declaration-block-semicolon-space-before': 'never',
'declaration-block-single-line-max-declarations': 1,
'declaration-colon-newline-after': 'always-multi-line',
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
'declaration-empty-line-before': 'never',
'font-family-no-duplicate-names': true,
'function-comma-space-after': 'always-single-line',
'function-comma-space-before': 'never',
'function-max-empty-lines': 0,
'function-name-case': 'lower',
'function-parentheses-newline-inside': 'always-multi-line',
'function-parentheses-space-inside': 'never-single-line',
'function-url-quotes': 'always',
'function-whitespace-after': 'always',
'indentation': 2,
'keyframe-declaration-no-important': true,
'length-zero-no-unit': true,
'max-empty-lines': 1,
'media-feature-colon-space-after': 'always',
'media-feature-colon-space-before': 'never',
'media-feature-name-case': 'lower',
'media-feature-name-no-unknown': true,
'media-feature-parentheses-space-inside': 'never',
'media-feature-range-operator-space-after': 'always',
'media-query-list-comma-newline-after': 'always-multi-line',
'media-query-list-comma-space-after': 'always-single-line',
'media-query-list-comma-space-before': 'never',
'no-duplicate-selectors': true,
'no-eol-whitespace': true,
'no-extra-semicolons': true,
'no-missing-end-of-source-newline': true,
'number-leading-zero': 'never',
'number-no-trailing-zeros': true,
'order/properties-alphabetical-order': true,
'property-case': 'lower',
'property-no-unknown': true,
'rule-empty-line-before':
[
'always-multi-line',
{ except: ['first-nested'], ignore: ['after-comment'] },
], ],
"customSyntax": "postcss-scss", 'scss/at-rule-no-unknown': true,
"rules": { 'scss/dollar-variable-colon-space-after': 'always',
"at-rule-empty-line-before": ["always", { 'scss/dollar-variable-colon-space-before': 'never',
except: ["first-nested", "blockless-after-blockless"], 'scss/operator-no-newline-after': true,
}], 'scss/operator-no-newline-before': true,
"at-rule-name-case": "lower", 'scss/operator-no-unspaced': true,
"at-rule-semicolon-newline-after": "always", 'scss/selector-no-redundant-nesting-selector': true,
"block-closing-brace-empty-line-before": "never", 'selector-attribute-brackets-space-inside': 'never',
"block-closing-brace-newline-after": "always", 'selector-attribute-operator-space-after': 'never',
"block-closing-brace-newline-before": "always-multi-line", 'selector-attribute-operator-space-before': 'never',
"block-closing-brace-space-before": "always-single-line", 'selector-combinator-space-after': 'always',
"block-no-empty": true, 'selector-combinator-space-before': 'always',
"block-opening-brace-newline-after": "always-multi-line", 'selector-list-comma-newline-after': 'always',
"block-opening-brace-space-after": "always-single-line", 'selector-list-comma-space-before': 'never',
"block-opening-brace-space-before": "always", 'selector-max-empty-lines': 0,
"color-hex-case": "lower", 'selector-nested-pattern': '^(?!&-|&_).*',
"color-hex-length": "short", 'selector-pseudo-class-case': 'lower',
"color-no-invalid-hex": true, 'selector-pseudo-class-no-unknown': true,
"comment-no-empty": true, 'selector-pseudo-class-parentheses-space-inside': 'never',
"comment-whitespace-inside": "always", 'selector-pseudo-element-case': 'lower',
"declaration-bang-space-after": "never", 'selector-pseudo-element-colon-notation': 'single',
"declaration-bang-space-before": "always", 'selector-pseudo-element-no-unknown': true,
"declaration-block-no-duplicate-properties": [true, { 'selector-type-case': 'lower',
ignore: ["consecutive-duplicates-with-different-values"], 'shorthand-property-no-redundant-values': true,
}], 'string-no-newline': true,
"declaration-block-no-redundant-longhand-properties": [true, { 'string-quotes': 'single',
ignoreShorthands: [/flex/, /grid/] 'unit-case': 'lower',
}], 'unit-no-unknown': true,
"declaration-block-semicolon-newline-after": "always-multi-line", 'value-list-comma-newline-after': 'always-multi-line',
"declaration-block-semicolon-space-after": "always-single-line", 'value-list-comma-space-after': 'always-single-line',
"declaration-block-semicolon-space-before": "never", 'value-list-comma-space-before': 'never',
"declaration-block-single-line-max-declarations": 1, 'value-list-max-empty-lines': 0,
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-empty-line-before": "never",
"font-family-no-duplicate-names": true,
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-max-empty-lines": 0,
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-url-quotes": "always",
"function-whitespace-after": "always",
"indentation": 2,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"max-empty-lines": 1,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
"no-duplicate-selectors": true,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "never",
"number-no-trailing-zeros": true,
"order/properties-alphabetical-order": true,
"property-case": "lower",
"property-no-unknown": true,
"rule-empty-line-before": ["always-multi-line", {
except: ["first-nested"],
ignore: ["after-comment"]
}],
"scss/at-rule-no-unknown": true,
"scss/dollar-variable-colon-space-after": "always",
"scss/dollar-variable-colon-space-before": "never",
"scss/operator-no-newline-after": true,
"scss/operator-no-newline-before": true,
"scss/operator-no-unspaced": true,
"scss/selector-no-redundant-nesting-selector": true,
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-nested-pattern": "^(?!&-|&_).*",
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "single",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"shorthand-property-no-redundant-values": true,
"string-no-newline": true,
"string-quotes": "single",
"unit-case": "lower",
"unit-no-unknown": true,
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0,
}, },
} }

View File

@ -1,4 +1,5 @@
# MailPoet # MailPoet
The **MailPoet** plugin. The **MailPoet** plugin.
To use the official Docker-based development environment, see details To use the official Docker-based development environment, see details
@ -16,11 +17,13 @@ below.
## Setup ## Setup
### Requirements ### Requirements
- PHP >= 7.3 (only for the development environment, to run the plugin PHP >= 7.2 is required) - PHP >= 7.3 (only for the development environment, to run the plugin PHP >= 7.2 is required)
- NodeJS - NodeJS
- WordPress - WordPress
### Installation ### Installation
The instructions below assume you already have a working WordPress development environment: The instructions below assume you already have a working WordPress development environment:
```bash ```bash
@ -171,8 +174,7 @@ _x('text to translate', 'context for translators', 'mailpoet');
**in Twig views** **in Twig views**
```html ```html
<%= __('text to translate') %> <%= __('text to translate') %> <%= _n('single text', 'plural text', $number) %>
<%= _n('single text', 'plural text', $number) %>
<%= _x('text to translate', 'context for translators') %> <%= _x('text to translate', 'context for translators') %>
``` ```
@ -183,12 +185,8 @@ The domain `mailpoet` will be added automatically by the Twig functions.
First add the string to the translations block in the Twig view: First add the string to the translations block in the Twig view:
```html ```html
<% block translations %> <% block translations %> <%= localize({ 'key': __('string to translate'), ... })
<%= localize({ %> <% endblock %>
'key': __('string to translate'),
...
}) %>
<% endblock %>
``` ```
Then use `MailPoet.I18n.t('key')` to get the translated string on your Javascript code. Then use `MailPoet.I18n.t('key')` to get the translated string on your Javascript code.
@ -197,9 +195,11 @@ Then use `MailPoet.I18n.t('key')` to get the translated string on your Javascrip
To run the whole acceptance testing suite you need the docker daemon to be running and after that use a command: `./do test:acceptance`. To run the whole acceptance testing suite you need the docker daemon to be running and after that use a command: `./do test:acceptance`.
If you want to run only a single test use the parameter `--file`: If you want to run only a single test use the parameter `--file`:
```bash ```bash
./do test:acceptance --skip-deps --file tests/acceptance/ReceiveStandardEmailCest.php ./do test:acceptance --skip-deps --file tests/acceptance/ReceiveStandardEmailCest.php
``` ```
The argument `--skip-deps` is useful locally to speed up the run. The argument `--skip-deps` is useful locally to speed up the run.
If there are some unexpected errors you can delete all the runtime and start again. If there are some unexpected errors you can delete all the runtime and start again.

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,5 @@ import jQuery from 'jquery';
jQuery(function adminDomReady($) { jQuery(function adminDomReady($) {
// dom ready // dom ready
$(function domReady() { $(function domReady() {});
});
}); });

View File

@ -15,9 +15,11 @@ export type ErrorResponse = {
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isErrorResponse = (error: any): error is ErrorResponse => ( export const isErrorResponse = (error: any): error is ErrorResponse =>
error && typeof error === 'object' && 'errors' in error && Array.isArray(error.errors) error &&
); typeof error === 'object' &&
'errors' in error &&
Array.isArray(error.errors);
type ResponseType = JQuery.Deferred<Response, ErrorResponse>; type ResponseType = JQuery.Deferred<Response, ErrorResponse>;
@ -94,7 +96,9 @@ export const MailPoetAjax = {
}, },
constructGetUrl: function constructGetUrl(options): string { constructGetUrl: function constructGetUrl(options): string {
this.init(options); this.init(options);
return `${this.options.url as string}?${jQuery.param(this.getParams() as object)}`; return `${this.options.url as string}?${jQuery.param(
this.getParams() as object,
)}`;
}, },
request: function request(method, options): ResponseType { request: function request(method, options): ResponseType {
// set options // set options
@ -117,15 +121,26 @@ export const MailPoetAjax = {
success: null, success: null,
dataType: 'json', dataType: 'json',
timeout: this.options.timeout, timeout: this.options.timeout,
}).then((data: Response) => deferred.resolve(data), (failedXhr, textStatus) => { }).then(
(data: Response) => deferred.resolve(data),
(failedXhr, textStatus) => {
let errorData: ErrorResponse; let errorData: ErrorResponse;
if (textStatus === 'timeout') { if (textStatus === 'timeout') {
errorData = buildErrorResponse(MailPoetI18n.t('ajaxTimeoutErrorMessage').replace('%d', timeout.toString())); errorData = buildErrorResponse(
MailPoetI18n.t('ajaxTimeoutErrorMessage').replace(
'%d',
timeout.toString(),
),
);
} else { } else {
errorData = requestFailed(MailPoetI18n.t('ajaxFailedErrorMessage'), failedXhr); errorData = requestFailed(
MailPoetI18n.t('ajaxFailedErrorMessage'),
failedXhr,
);
} }
void deferred.reject(errorData); void deferred.reject(errorData);
}); },
);
// clear options // clear options
this.options = {}; this.options = {};

View File

@ -39,7 +39,10 @@ function track(name, data = []) {
function exportMixpanel() { function exportMixpanel() {
window.MailPoet.forceTrackEvent = track; window.MailPoet.forceTrackEvent = track;
if (window.mailpoet_analytics_enabled && window.MailPoet.libs3rdPartyEnabled) { if (
window.mailpoet_analytics_enabled &&
window.MailPoet.libs3rdPartyEnabled
) {
window.MailPoet.trackEvent = track; window.MailPoet.trackEvent = track;
} else { } else {
window.MailPoet.trackEvent = function emptyFunction() {}; window.MailPoet.trackEvent = function emptyFunction() {};

View File

@ -30,7 +30,10 @@ export const withFeatureAnnouncement = <P extends Record<string, unknown>>(
let beamerCallback; let beamerCallback;
function showPluginUpdateNotice() { function showPluginUpdateNotice() {
if (!window.mailpoet_update_available || document.getElementById('mailpoet_update_notice')) { if (
!window.mailpoet_update_available ||
document.getElementById('mailpoet_update_notice')
) {
return; return;
} }
const updateMailPoetNotice = ReactStringReplace( const updateMailPoetNotice = ReactStringReplace(
@ -99,7 +102,7 @@ export const withFeatureAnnouncement = <P extends Record<string, unknown>>(
}: Omit<P, 'hasNews' | 'onBeamerClick'>) { }: Omit<P, 'hasNews' | 'onBeamerClick'>) {
return ( return (
<Component <Component
{...props as P} {...(props as P)}
onBeamerClick={(e) => showBeamer(e)} onBeamerClick={(e) => showBeamer(e)}
hasNews={showDot} hasNews={showDot}
/> />

View File

@ -3,9 +3,10 @@ import { api } from './config';
const API_URL = `${api.root}/mailpoet/v1/automation`; const API_URL = `${api.root}/mailpoet/v1/automation`;
export const request = (path: string, init?: RequestInit): ReturnType<typeof fetch> => ( export const request = (
fetch(`${API_URL}/${path}`, init) path: string,
); init?: RequestInit,
): ReturnType<typeof fetch> => fetch(`${API_URL}/${path}`, init);
type Error<T> = { type Error<T> = {
response?: Response; response?: Response;
@ -18,26 +19,25 @@ type State<T> = {
error?: Error<T>; error?: Error<T>;
}; };
type Result<T> = [ type Result<T> = [(init?: RequestInit) => Promise<void>, State<T>];
(init?: RequestInit) => Promise<void>,
State<T>,
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Data = Record<string, any>; type Data = Record<string, any>;
export const useMutation = <T extends Data>(path: string, config?: RequestInit): Result<T> => { export const useMutation = <T extends Data>(
path: string,
config?: RequestInit,
): Result<T> => {
const [state, setState] = useState<State<T>>({ const [state, setState] = useState<State<T>>({
data: undefined, data: undefined,
loading: false, loading: false,
error: undefined, error: undefined,
}); });
const mutation = useCallback(async (init?: RequestInit) => { const mutation = useCallback(
async (init?: RequestInit) => {
setState((prevState) => ({ ...prevState, loading: true })); setState((prevState) => ({ ...prevState, loading: true }));
const response = await request( const response = await request(path, {
path,
{
...config, ...config,
...init, ...init,
headers: { headers: {
@ -45,8 +45,7 @@ export const useMutation = <T extends Data>(path: string, config?: RequestInit):
...(init?.headers ?? {}), ...(init?.headers ?? {}),
'x-wp-nonce': api.nonce, 'x-wp-nonce': api.nonce,
}, },
}, });
);
try { try {
const data = await response.json(); const data = await response.json();
@ -58,19 +57,24 @@ export const useMutation = <T extends Data>(path: string, config?: RequestInit):
} finally { } finally {
setState((prevState) => ({ ...prevState, loading: false })); setState((prevState) => ({ ...prevState, loading: false }));
} }
}, [config, path]); },
[config, path],
);
return [mutation, state]; return [mutation, state];
}; };
export const useQuery = <T extends Data>(path: string, init?: RequestInit): State<T> => { export const useQuery = <T extends Data>(
path: string,
init?: RequestInit,
): State<T> => {
const [mutation, result] = useMutation<T>(path, init); const [mutation, result] = useMutation<T>(path, init);
useEffect( useEffect(
() => { () => {
void mutation(); void mutation();
}, },
[], /* eslint-disable-line react-hooks/exhaustive-deps -- request only on initial load */ [] /* eslint-disable-line react-hooks/exhaustive-deps -- request only on initial load */,
); );
return result; return result;

View File

@ -13,24 +13,26 @@ function ApiCheck(): JSX.Element {
} }
function RecreateSchemaButton(): JSX.Element { function RecreateSchemaButton(): JSX.Element {
const [createSchema, { loading, error }] = useMutation('system/database', { method: 'POST' }); const [createSchema, { loading, error }] = useMutation('system/database', {
method: 'POST',
});
return ( return (
<div> <div>
<button <button type="button" onClick={() => createSchema()} disabled={loading}>
type="button"
onClick={() => createSchema()}
disabled={loading}
>
Recreate DB schema (data will be lost) Recreate DB schema (data will be lost)
</button> </button>
{error && (<div>{error?.data?.message ?? 'An unknown error occurred'}</div>)} {error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div> </div>
); );
} }
function DeleteSchemaButton(): JSX.Element { function DeleteSchemaButton(): JSX.Element {
const [deleteSchema, { loading, error }] = useMutation('system/database', { method: 'DELETE' }); const [deleteSchema, { loading, error }] = useMutation('system/database', {
method: 'DELETE',
});
return ( return (
<div> <div>
@ -38,13 +40,16 @@ function DeleteSchemaButton(): JSX.Element {
type="button" type="button"
onClick={async () => { onClick={async () => {
await deleteSchema(); await deleteSchema();
window.location.href = '/wp-admin/admin.php?page=mailpoet-experimental'; window.location.href =
'/wp-admin/admin.php?page=mailpoet-experimental';
}} }}
disabled={loading} disabled={loading}
> >
Delete DB schema & deactivate feature Delete DB schema & deactivate feature
</button> </button>
{error && (<div>{error?.data?.message ?? 'An unknown error occurred'}</div>)} {error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div> </div>
); );
} }

View File

@ -30,20 +30,26 @@ const createWorkflow = () => {
}; };
export function CreateTestingWorkflowButton(): JSX.Element { export function CreateTestingWorkflowButton(): JSX.Element {
const [createSchema, { loading, error }] = useMutation('workflows', { method: 'POST' }); const [createSchema, { loading, error }] = useMutation('workflows', {
method: 'POST',
});
return ( return (
<div> <div>
<button <button
type="button" type="button"
onClick={() => createSchema({ onClick={() =>
createSchema({
body: JSON.stringify(createWorkflow()), body: JSON.stringify(createWorkflow()),
})} })
}
disabled={loading} disabled={loading}
> >
Create testing workflow Create testing workflow
</button> </button>
{error && (<div>{error?.data?.message ?? 'An unknown error occurred'}</div>)} {error && (
<div>{error?.data?.message ?? 'An unknown error occurred'}</div>
)}
</div> </div>
); );
} }

View File

@ -12,10 +12,7 @@ export function WithoutIcons() {
<> <>
<Heading level={3}>Small buttons</Heading> <Heading level={3}>Small buttons</Heading>
<p> <p>
<Button <Button onClick={action('primary small')} dimension="small">
onClick={action('primary small')}
dimension="small"
>
Primary button Primary button
</Button> </Button>
<Button <Button
@ -44,27 +41,14 @@ export function WithoutIcons() {
<Heading level={3}>Regular buttons</Heading> <Heading level={3}>Regular buttons</Heading>
<p> <p>
<Button <Button onClick={action('primary regular')}>Primary button</Button>
onClick={action('primary regular')} <Button onClick={action('secondary regular')} variant="secondary">
>
Primary button
</Button>
<Button
onClick={action('secondary regular')}
variant="secondary"
>
Secondary button Secondary button
</Button> </Button>
<Button <Button onClick={action('tertiary regular')} variant="tertiary">
onClick={action('tertiary regular')}
variant="tertiary"
>
Tertiary button Tertiary button
</Button> </Button>
<Button <Button onClick={action('destructive regular')} variant="destructive">
onClick={action('destructive regular')}
variant="destructive"
>
Destructive button Destructive button
</Button> </Button>
</p> </p>
@ -72,25 +56,14 @@ export function WithoutIcons() {
<Heading level={3}>Disabled buttons</Heading> <Heading level={3}>Disabled buttons</Heading>
<p> <p>
<Button isDisabled> <Button isDisabled>Primary button</Button>
Primary button <Button isDisabled variant="secondary">
</Button>
<Button
isDisabled
variant="secondary"
>
Secondary button Secondary button
</Button> </Button>
<Button <Button isDisabled variant="tertiary">
isDisabled
variant="tertiary"
>
Tertiary button Tertiary button
</Button> </Button>
<Button <Button isDisabled variant="destructive">
isDisabled
variant="destructive"
>
Destructive button Destructive button
</Button> </Button>
</p> </p>
@ -98,25 +71,14 @@ export function WithoutIcons() {
<Heading level={3}>Buttons with spinner</Heading> <Heading level={3}>Buttons with spinner</Heading>
<p> <p>
<Button withSpinner> <Button withSpinner>Primary button</Button>
Primary button <Button withSpinner variant="secondary">
</Button>
<Button
withSpinner
variant="secondary"
>
Secondary button Secondary button
</Button> </Button>
<Button <Button withSpinner variant="tertiary">
withSpinner
variant="tertiary"
>
Tertiary button Tertiary button
</Button> </Button>
<Button <Button withSpinner variant="destructive">
withSpinner
variant="destructive"
>
Destructive button Destructive button
</Button> </Button>
</p> </p>
@ -124,10 +86,7 @@ export function WithoutIcons() {
<Heading level={3}>Full width buttons</Heading> <Heading level={3}>Full width buttons</Heading>
<p> <p>
<Button <Button onClick={action('primary full-width')} isFullWidth>
onClick={action('primary full-width')}
isFullWidth
>
Primary button Primary button
</Button> </Button>
<Button <Button

View File

@ -54,10 +54,7 @@ export function WithIcons() {
<Heading level={3}>Regular buttons</Heading> <Heading level={3}>Regular buttons</Heading>
<p> <p>
<Button <Button onClick={action('icon start primary regular')} iconStart={icon}>
onClick={action('icon start primary regular')}
iconStart={icon}
>
Icon start Icon start
</Button> </Button>
<Button <Button
@ -90,35 +87,15 @@ export function WithIcons() {
<Heading level={3}>Disabled buttons</Heading> <Heading level={3}>Disabled buttons</Heading>
<p> <p>
<Button <Button isDisabled iconStart={icon}>
isDisabled
iconStart={icon}
>
Icon start Icon start
</Button> </Button>
<Button <Button isDisabled variant="secondary" iconStart={icon} iconEnd={icon}>
isDisabled
variant="secondary"
iconStart={icon}
iconEnd={icon}
>
Both icons Both icons
</Button> </Button>
<Button <Button isDisabled variant="secondary" iconStart={icon} />
isDisabled <Button isDisabled variant="tertiary" iconStart={icon} />
variant="secondary" <Button isDisabled variant="destructive" iconEnd={icon}>
iconStart={icon}
/>
<Button
isDisabled
variant="tertiary"
iconStart={icon}
/>
<Button
isDisabled
variant="destructive"
iconEnd={icon}
>
Icon end Icon end
</Button> </Button>
</p> </p>
@ -126,35 +103,15 @@ export function WithIcons() {
<Heading level={3}>Buttons with spinner</Heading> <Heading level={3}>Buttons with spinner</Heading>
<p> <p>
<Button <Button withSpinner iconStart={icon}>
withSpinner
iconStart={icon}
>
Icon start Icon start
</Button> </Button>
<Button <Button withSpinner variant="secondary" iconStart={icon} iconEnd={icon}>
withSpinner
variant="secondary"
iconStart={icon}
iconEnd={icon}
>
Both icons Both icons
</Button> </Button>
<Button <Button withSpinner variant="secondary" iconStart={icon} />
withSpinner <Button withSpinner variant="tertiary" iconStart={icon} />
variant="secondary" <Button withSpinner variant="destructive" iconEnd={icon}>
iconStart={icon}
/>
<Button
withSpinner
variant="tertiary"
iconStart={icon}
/>
<Button
withSpinner
variant="destructive"
iconEnd={icon}
>
Icon end Icon end
</Button> </Button>
</p> </p>

View File

@ -45,12 +45,7 @@ function Button({
target={target} target={target}
rel={rel} rel={rel}
disabled={isDisabled} disabled={isDisabled}
className={ className={classnames(className, 'button', 'mailpoet-button', {
classnames(
className,
'button',
'mailpoet-button',
{
'mailpoet-button-with-spinner': withSpinner, 'mailpoet-button-with-spinner': withSpinner,
'mailpoet-button-disabled': isDisabled, 'mailpoet-button-disabled': isDisabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
@ -59,9 +54,7 @@ function Button({
'button-link': variant === 'tertiary', 'button-link': variant === 'tertiary',
'button-link button-link-delete': variant === 'destructive', 'button-link button-link-delete': variant === 'destructive',
'button-small': dimension === 'small', 'button-small': dimension === 'small',
}, })}
)
}
data-automation-id={automationId} data-automation-id={automationId}
> >
{iconStart} {iconStart}

View File

@ -17,9 +17,21 @@ export function CategoriesWithCount() {
return ( return (
<> <>
<Categories onSelect={noop} categories={categories} active={categories[0].name} /> <Categories
<Categories onSelect={noop} categories={categories} active={categories[2].name} /> onSelect={noop}
<Categories onSelect={noop} categories={categories} active={categories[4].name} /> categories={categories}
active={categories[0].name}
/>
<Categories
onSelect={noop}
categories={categories}
active={categories[2].name}
/>
<Categories
onSelect={noop}
categories={categories}
active={categories[4].name}
/>
</> </>
); );
} }
@ -35,9 +47,21 @@ export function CategoriesWithoutCount() {
return ( return (
<> <>
<Categories onSelect={noop} categories={categories} active={categories[0].name} /> <Categories
<Categories onSelect={noop} categories={categories} active={categories[2].name} /> onSelect={noop}
<Categories onSelect={noop} categories={categories} active={categories[4].name} /> categories={categories}
active={categories[0].name}
/>
<Categories
onSelect={noop}
categories={categories}
active={categories[2].name}
/>
<Categories
onSelect={noop}
categories={categories}
active={categories[4].name}
/>
</> </>
); );
} }

View File

@ -16,11 +16,7 @@ function Categories({ onSelect, categories, active }: Props) {
/> />
)); ));
return ( return <div className="mailpoet-categories">{cats}</div>;
<div className="mailpoet-categories">
{ cats }
</div>
);
} }
export default Categories; export default Categories;

View File

@ -20,10 +20,7 @@ function CategoriesItem({
automationId, automationId,
active, active,
}: Props) { }: Props) {
const classes = classNames( const classes = classNames('mailpoet-categories-item', { active: !!active });
'mailpoet-categories-item',
{ active: !!active },
);
return ( return (
<a <a
@ -41,7 +38,7 @@ function CategoriesItem({
</span> </span>
{count > 0 && ( {count > 0 && (
<span className="mailpoet-categories-count"> <span className="mailpoet-categories-count">
{ parseInt(count.toString(), 10).toLocaleString() } {parseInt(count.toString(), 10).toLocaleString()}
</span> </span>
)} )}
</a> </a>

View File

@ -8,13 +8,21 @@ function ConfirmAlert(props) {
template: ReactDOMServer.renderToString( template: ReactDOMServer.renderToString(
<> <>
<p>{props.message}</p> <p>{props.message}</p>
<button id="mailpoet_alert_cancel" className="button button-secondary" type="button"> <button
id="mailpoet_alert_cancel"
className="button button-secondary"
type="button"
>
{props.cancelLabel} {props.cancelLabel}
</button> </button>
<button id="mailpoet_alert_confirm" className="button button-primary" type="submit"> <button
id="mailpoet_alert_confirm"
className="button button-primary"
type="submit"
>
{props.confirmLabel} {props.confirmLabel}
</button> </button>
</> </>,
), ),
onInit: () => { onInit: () => {
document document
@ -55,6 +63,6 @@ export default function confirmAlert(props) {
cancelLabel={props.cancelLabel} cancelLabel={props.cancelLabel}
confirmLabel={props.confirmLabel} confirmLabel={props.confirmLabel}
onConfirm={props.onConfirm} onConfirm={props.onConfirm}
/> />,
); );
} }

View File

@ -11,7 +11,9 @@ export default async function callApi({ endpoint, action, data }) {
}); });
return { success: true, res }; return { success: true, res };
} catch (res) { } catch (res) {
const error = isErrorResponse(res) ? res.errors.map((e) => e.message) : null; const error = isErrorResponse(res)
? res.errors.map((e) => e.message)
: null;
return { success: false, error, res }; return { success: false, error, res };
} }
} }

View File

@ -1,14 +1,19 @@
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
const sleep = (ms:number) => new Promise((resolve) => { setTimeout(resolve, ms); }); const sleep = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export default async function trackEvent( export default async function trackEvent({
{
name, name,
data, data,
timeout = 0, timeout = 0,
}:{ name:string, data:object, timeout:number }, }: {
) { name: string;
data: object;
timeout: number;
}) {
MailPoet.trackEvent(name, data); MailPoet.trackEvent(name, data);
return sleep(timeout); return sleep(timeout);
} }

View File

@ -9,21 +9,13 @@ export default {
component: Datepicker, component: Datepicker,
}; };
function DatepickerWrapper({ function DatepickerWrapper({ ...props }) {
...props
}) {
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
const onChange = (date:Date) => { const onChange = (date: Date) => {
props.onChange(date); props.onChange(date);
setStartDate(date); setStartDate(date);
}; };
return ( return <Datepicker {...props} selected={startDate} onChange={onChange} />;
<Datepicker
{...props}
selected={startDate}
onChange={onChange}
/>
);
} }
export function Datepickers() { export function Datepickers() {

View File

@ -18,16 +18,11 @@ function Datepicker({
}: Props) { }: Props) {
return ( return (
<div <div
className={ className={classnames('mailpoet-datepicker mailpoet-form-input', {
classnames(
'mailpoet-datepicker mailpoet-form-input',
{
[`mailpoet-form-input-${dimension}`]: dimension, [`mailpoet-form-input-${dimension}`]: dimension,
'mailpoet-disabled': props.disabled, 'mailpoet-disabled': props.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}, })}
)
}
> >
{iconStart} {iconStart}
<ReactDatePicker <ReactDatePicker

View File

@ -17,13 +17,11 @@ function Checkbox({
}: Props) { }: Props) {
return ( return (
<label <label
className={ className={classnames({
classnames({
'mailpoet-form-checkbox': true, 'mailpoet-form-checkbox': true,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}) })}
}
data-automation-id={automationId} data-automation-id={automationId}
> >
<input <input

View File

@ -26,7 +26,7 @@ function CheckboxGroup({
const handleChange = (value: CheckboxValueType, isChecked: boolean) => { const handleChange = (value: CheckboxValueType, isChecked: boolean) => {
const index = values.indexOf(value); const index = values.indexOf(value);
let newValues:CheckboxValueType[] = []; let newValues: CheckboxValueType[] = [];
if (isChecked && index === -1) { if (isChecked && index === -1) {
newValues = values.concat([value]); newValues = values.concat([value]);
} }
@ -42,7 +42,7 @@ function CheckboxGroup({
<div> <div>
{options.map((props: CheckboxProps) => { {options.map((props: CheckboxProps) => {
const { label, ...attributes } = props; const { label, ...attributes } = props;
const value = (props.value as CheckboxValueType); const value = props.value as CheckboxValueType;
return ( return (
<Checkbox <Checkbox
checked={values.includes(value)} checked={values.includes(value)}

View File

@ -12,11 +12,7 @@ export function Inputs() {
<> <>
<Heading level={3}>Small inputs</Heading> <Heading level={3}>Small inputs</Heading>
<div> <div>
<Input <Input type="text" dimension="small" placeholder="Small input value" />
type="text"
dimension="small"
placeholder="Small input value"
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Input <Input
type="text" type="text"
@ -43,10 +39,7 @@ export function Inputs() {
<br /> <br />
<Heading level={3}>Regular inputs</Heading> <Heading level={3}>Regular inputs</Heading>
<div> <div>
<Input <Input type="text" placeholder="Regular input" />
type="text"
placeholder="Regular input"
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Input <Input
type="text" type="text"
@ -67,20 +60,12 @@ export function Inputs() {
iconEnd={icon} iconEnd={icon}
/> />
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Input <Input disabled type="text" placeholder="Disabled input" />
disabled
type="text"
placeholder="Disabled input"
/>
</div> </div>
<br /> <br />
<Heading level={3}>Full-width inputs</Heading> <Heading level={3}>Full-width inputs</Heading>
<div> <div>
<Input <Input type="text" placeholder="Full-width input" isFullWidth />
type="text"
placeholder="Full-width input"
isFullWidth
/>
<Input <Input
type="text" type="text"
placeholder="Full-width input with iconStart" placeholder="Full-width input with iconStart"

View File

@ -23,25 +23,25 @@ function Input({
}: Props) { }: Props) {
return ( return (
<div <div
className={ className={classnames(className, 'mailpoet-form-input', {
classnames(
className,
'mailpoet-form-input',
{
[`mailpoet-form-input-${dimension}`]: dimension, [`mailpoet-form-input-${dimension}`]: dimension,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}, })}
)
}
> >
{iconStart} {iconStart}
<input {...attributes} /> <input {...attributes} />
{customLabel && <div className="mailpoet-form-input-label">{customLabel}</div>} {customLabel && (
<div className="mailpoet-form-input-label">{customLabel}</div>
)}
{tooltip && ( {tooltip && (
<> <>
<span className="mailpoet-form-tooltip-holder"> <span className="mailpoet-form-tooltip-holder">
<span className="mailpoet-form-tooltip-icon" data-tip data-for={attributes.name} /> <span
className="mailpoet-form-tooltip-icon"
data-tip
data-for={attributes.name}
/>
</span> </span>
<Tooltip place="right" multiline id={attributes.name}> <Tooltip place="right" multiline id={attributes.name}>
{tooltip} {tooltip}

View File

@ -23,18 +23,10 @@ export function Radios() {
<> <>
<Heading level={3}>Inline individual radios</Heading> <Heading level={3}>Inline individual radios</Heading>
<div> <div>
<Radio <Radio onCheck={action('radio-individual-1')} name="story" value="1">
onCheck={action('radio-individual-1')}
name="story"
value="1"
>
Option 1 Option 1
</Radio> </Radio>
<Radio <Radio onCheck={action('radio-individual-2')} name="story" value="2">
onCheck={action('radio-individual-2')}
name="story"
value="2"
>
Option 2 Option 2
</Radio> </Radio>
</div> </div>

View File

@ -33,7 +33,7 @@ function RadioGroup({
<div> <div>
{options.map((props: RadioProps) => { {options.map((props: RadioProps) => {
const { label, ...attributes } = props; const { label, ...attributes } = props;
const value = (props.value as RadioValueType); const value = props.value as RadioValueType;
return ( return (
<Radio <Radio
checked={currentValue === value} checked={currentValue === value}

View File

@ -17,13 +17,11 @@ function Radio({
}: Props) { }: Props) {
return ( return (
<label <label
className={ className={classnames({
classnames({
'mailpoet-form-radio': true, 'mailpoet-form-radio': true,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}) })}
}
data-automation-id={automationId} data-automation-id={automationId}
> >
<input <input

View File

@ -26,7 +26,8 @@ export function ReactSelect() {
}, },
{ {
value: 'long', value: 'long',
label: 'Very very very very very very very very very very very very long option', label:
'Very very very very very very very very very very very very long option',
tag: 'long', tag: 'long',
count: 1234, count: 1234,
}, },

View File

@ -19,9 +19,13 @@ type LabelRendererProps = {
function LabelRenderer(data: LabelRendererProps) { function LabelRenderer(data: LabelRendererProps) {
return ( return (
<div className="mailpoet-form-react-select-option"> <div className="mailpoet-form-react-select-option">
{data.tag && <span className="mailpoet-form-react-select-tag">{data.tag}</span>} {data.tag && (
<span className="mailpoet-form-react-select-tag">{data.tag}</span>
)}
<span>{data.label}</span> <span>{data.label}</span>
{data.count !== undefined && <span className="mailpoet-form-react-select-count">{data.count}</span>} {data.count !== undefined && (
<span className="mailpoet-form-react-select-count">{data.count}</span>
)}
</div> </div>
); );
} }
@ -43,14 +47,12 @@ function Option(props: OptionProps<OptionData>) {
style={style} style={style}
ref={props.innerRef} ref={props.innerRef}
{...props.innerProps} {...props.innerProps}
className={ className={classnames({
classnames({
'mailpoet-form-react-select__option': true, 'mailpoet-form-react-select__option': true,
'mailpoet-form-react-select__option--is-disabled': props.isDisabled, 'mailpoet-form-react-select__option--is-disabled': props.isDisabled,
'mailpoet-form-react-select__option--is-focused': props.isFocused, 'mailpoet-form-react-select__option--is-focused': props.isFocused,
'mailpoet-form-react-select__option--is-selected': props.isSelected, 'mailpoet-form-react-select__option--is-selected': props.isSelected,
}) })}
}
> >
{LabelRenderer(props.data)} {LabelRenderer(props.data)}
</div> </div>
@ -62,12 +64,11 @@ function SingleValue(props: any) {
return ( return (
<div <div
{...props.innerProps} {...props.innerProps}
className={ className={classnames({
classnames({
'mailpoet-form-react-select__single-value': true, 'mailpoet-form-react-select__single-value': true,
'mailpoet-form-react-select__single-value--is-disabled': props.isDisabled, 'mailpoet-form-react-select__single-value--is-disabled':
}) props.isDisabled,
} })}
> >
{LabelRenderer(props.data as LabelRendererProps)} {LabelRenderer(props.data as LabelRendererProps)}
</div> </div>
@ -105,17 +106,11 @@ function ReactSelect({
}: Props) { }: Props) {
return ( return (
<div <div
className={ className={classnames('mailpoet-form-input', 'mailpoet-form-select', {
classnames(
'mailpoet-form-input',
'mailpoet-form-select',
{
[`mailpoet-form-input-${dimension}`]: dimension, [`mailpoet-form-input-${dimension}`]: dimension,
'mailpoet-disabled': props.disabled, 'mailpoet-disabled': props.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}, })}
)
}
data-automation-id={automationId} data-automation-id={automationId}
> >
{iconStart} {iconStart}

View File

@ -18,10 +18,7 @@ export function NativeSelect() {
<option value="3">Opt 3</option> <option value="3">Opt 3</option>
</Select> </Select>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Select <Select isMinWidth iconStart={icon}>
isMinWidth
iconStart={icon}
>
<option value="1">Opt 1</option> <option value="1">Opt 1</option>
<option value="2">Opt 2</option> <option value="2">Opt 2</option>
<option value="3">Opt 3</option> <option value="3">Opt 3</option>
@ -36,10 +33,7 @@ export function NativeSelect() {
<option value="3">Option 3</option> <option value="3">Option 3</option>
</Select> </Select>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Select <Select dimension="small" iconStart={icon}>
dimension="small"
iconStart={icon}
>
<option value="1">Option 1</option> <option value="1">Option 1</option>
<option value="2">Option 2</option> <option value="2">Option 2</option>
<option value="3">Option 3</option> <option value="3">Option 3</option>
@ -73,10 +67,7 @@ export function NativeSelect() {
<option value="3">Option 3</option> <option value="3">Option 3</option>
</Select> </Select>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Select <Select isFullWidth iconStart={icon}>
isFullWidth
iconStart={icon}
>
<option value="1">Option 1</option> <option value="1">Option 1</option>
<option value="2">Option 2</option> <option value="2">Option 2</option>
<option value="3">Option 3</option> <option value="3">Option 3</option>

View File

@ -1,9 +1,4 @@
import { import { forwardRef, ReactNode, Ref, SelectHTMLAttributes } from 'react';
forwardRef,
ReactNode,
Ref,
SelectHTMLAttributes,
} from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
type Props = SelectHTMLAttributes<HTMLSelectElement> & { type Props = SelectHTMLAttributes<HTMLSelectElement> & {
@ -15,7 +10,9 @@ type Props = SelectHTMLAttributes<HTMLSelectElement> & {
automationId?: string; automationId?: string;
}; };
const Select = forwardRef(({ const Select = forwardRef(
(
{
children, children,
dimension, dimension,
isFullWidth, isFullWidth,
@ -23,26 +20,23 @@ const Select = forwardRef(({
iconStart, iconStart,
automationId, automationId,
...attributes ...attributes
}: Props, ref?: Ref<HTMLSelectElement>) => ( }: Props,
ref?: Ref<HTMLSelectElement>,
) => (
<div <div
className={ className={classnames('mailpoet-form-input', 'mailpoet-form-select', {
classnames(
'mailpoet-form-input',
'mailpoet-form-select',
{
[`mailpoet-form-input-${dimension}`]: dimension, [`mailpoet-form-input-${dimension}`]: dimension,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
'mailpoet-min-width': isMinWidth, 'mailpoet-min-width': isMinWidth,
}, })}
)
}
> >
{iconStart} {iconStart}
<select {...attributes} ref={ref} data-automation-id={automationId}> <select {...attributes} ref={ref} data-automation-id={automationId}>
{children} {children}
</select> </select>
</div> </div>
)); ),
);
export default Select; export default Select;

View File

@ -11,30 +11,19 @@ export function Textareas() {
<> <>
<Heading level={3}>Small textareas</Heading> <Heading level={3}>Small textareas</Heading>
<div> <div>
<Textarea <Textarea dimension="small" placeholder="Small textarea value" />
dimension="small"
placeholder="Small textarea value"
/>
</div> </div>
<br /> <br />
<Heading level={3}>Regular textareas</Heading> <Heading level={3}>Regular textareas</Heading>
<div> <div>
<Textarea <Textarea placeholder="Regular textarea" />
placeholder="Regular textarea"
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Textarea <Textarea disabled placeholder="Disabled textarea" />
disabled
placeholder="Disabled textarea"
/>
</div> </div>
<br /> <br />
<Heading level={3}>Full-width textareas</Heading> <Heading level={3}>Full-width textareas</Heading>
<div> <div>
<Textarea <Textarea placeholder="Full-width textarea" isFullWidth />
placeholder="Full-width textarea"
isFullWidth
/>
</div> </div>
<br /> <br />
</> </>

View File

@ -21,24 +21,24 @@ function Textarea({
}: Props) { }: Props) {
return ( return (
<div <div
className={ className={classnames(className, 'mailpoet-form-textarea', {
classnames(
className,
'mailpoet-form-textarea',
{
[`mailpoet-form-textarea-${dimension}`]: dimension, [`mailpoet-form-textarea-${dimension}`]: dimension,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth, 'mailpoet-full-width': isFullWidth,
}, })}
)
}
> >
<textarea className={classnames({ code: isCode })} {...attributes} /> <textarea className={classnames({ code: isCode })} {...attributes} />
{customLabel && <div className="mailpoet-form-input-label">{customLabel}</div>} {customLabel && (
<div className="mailpoet-form-input-label">{customLabel}</div>
)}
{tooltip && ( {tooltip && (
<> <>
<span className="mailpoet-form-tooltip-holder"> <span className="mailpoet-form-tooltip-holder">
<span className="mailpoet-form-tooltip-icon" data-tip data-for={attributes.name} /> <span
className="mailpoet-form-tooltip-icon"
data-tip
data-for={attributes.name}
/>
</span> </span>
<Tooltip place="right" multiline id={attributes.name}> <Tooltip place="right" multiline id={attributes.name}>
{tooltip} {tooltip}

View File

@ -15,11 +15,7 @@ export function Toggles() {
<Grid.Column dimension="small"> <Grid.Column dimension="small">
<Grid.SpaceBetween> <Grid.SpaceBetween>
<label htmlFor="toggle-1">Toggle regular</label> <label htmlFor="toggle-1">Toggle regular</label>
<Toggle <Toggle onCheck={action('toggle-1')} id="toggle-1" name="toggle-1" />
onCheck={action('toggle-1')}
id="toggle-1"
name="toggle-1"
/>
</Grid.SpaceBetween> </Grid.SpaceBetween>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Grid.SpaceBetween> <Grid.SpaceBetween>

View File

@ -16,14 +16,12 @@ function Toggle({
}: Props) { }: Props) {
return ( return (
<label <label
className={ className={classnames({
classnames({
[className]: className, [className]: className,
'mailpoet-form-toggle': true, 'mailpoet-form-toggle': true,
[`mailpoet-form-toggle-${dimension}`]: dimension, [`mailpoet-form-toggle-${dimension}`]: dimension,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
}) })}
}
data-automation-id={automationId} data-automation-id={automationId}
> >
<input <input

View File

@ -14,35 +14,18 @@ export function YesNos() {
<Heading level={3}>YesNos</Heading> <Heading level={3}>YesNos</Heading>
<Grid.Column dimension="small"> <Grid.Column dimension="small">
<Grid.SpaceBetween verticalAlign="center"> <Grid.SpaceBetween verticalAlign="center">
<div> <div>YesNo</div>
YesNo <YesNo onCheck={action('yesno-1')} name="yesno-1" />
</div>
<YesNo
onCheck={action('yesno-1')}
name="yesno-1"
/>
</Grid.SpaceBetween> </Grid.SpaceBetween>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Grid.SpaceBetween verticalAlign="center"> <Grid.SpaceBetween verticalAlign="center">
<div> <div>YesNo with error</div>
YesNo with error <YesNo showError onCheck={action('yesno-2')} name="yesno-2" />
</div>
<YesNo
showError
onCheck={action('yesno-2')}
name="yesno-2"
/>
</Grid.SpaceBetween> </Grid.SpaceBetween>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Grid.SpaceBetween verticalAlign="center"> <Grid.SpaceBetween verticalAlign="center">
<div> <div>YesNo disabled</div>
YesNo disabled <YesNo disabled onCheck={action('yesno-3')} name="yesno-3" />
</div>
<YesNo
disabled
onCheck={action('yesno-3')}
name="yesno-3"
/>
</Grid.SpaceBetween> </Grid.SpaceBetween>
</Grid.Column> </Grid.Column>
</> </>

View File

@ -1,5 +1,8 @@
export default ( export default (
<svg width="17" height="16" viewBox="0 0 17 16"> <svg width="17" height="16" viewBox="0 0 17 16">
<path fill="currentColor" d="M12.407 3.31c.576.576.576 1.509 0 2.084L9.914 7.888l2.411 2.412c.586.585.586 1.535 0 2.121-.585.586-1.535.586-2.12 0l-2.413-2.412L5.3 12.503c-.576.575-1.509.575-2.084 0-.575-.575-.575-1.508 0-2.083l2.493-2.495-2.41-2.41c-.587-.587-.587-1.536 0-2.122.585-.586 1.535-.586 2.12 0L7.83 5.804l2.494-2.493c.575-.576 1.508-.576 2.083 0z" /> <path
fill="currentColor"
d="M12.407 3.31c.576.576.576 1.509 0 2.084L9.914 7.888l2.411 2.412c.586.585.586 1.535 0 2.121-.585.586-1.535.586-2.12 0l-2.413-2.412L5.3 12.503c-.576.575-1.509.575-2.084 0-.575-.575-.575-1.508 0-2.083l2.493-2.495-2.41-2.41c-.587-.587-.587-1.536 0-2.122.585-.586 1.535-.586 2.12 0L7.83 5.804l2.494-2.493c.575-.576 1.508-.576 2.083 0z"
/>
</svg> </svg>
); );

View File

@ -1,5 +1,8 @@
export default ( export default (
<svg width="13" height="11" viewBox="0 0 13 11"> <svg width="13" height="11" viewBox="0 0 13 11">
<path fill="currentColor" d="M9.967.638c.483-.698 1.405-.846 2.06-.33.654.515.793 1.499.31 2.197l-5.44 7.857c-.55.794-1.64.857-2.267.132l-3.4-3.928c-.552-.638-.515-1.632.083-2.22.598-.59 1.53-.55 2.082.088l2.19 2.532L9.968.638z" /> <path
fill="currentColor"
d="M9.967.638c.483-.698 1.405-.846 2.06-.33.654.515.793 1.499.31 2.197l-5.44 7.857c-.55.794-1.64.857-2.267.132l-3.4-3.928c-.552-.638-.515-1.632.083-2.22.598-.59 1.53-.55 2.082.088l2.19 2.532L9.968.638z"
/>
</svg> </svg>
); );

View File

@ -20,13 +20,11 @@ function YesNo({
}: Props) { }: Props) {
return ( return (
<div <div
className={ className={classnames({
classnames({
'mailpoet-form-yesno': true, 'mailpoet-form-yesno': true,
'mailpoet-form-yesno-error': showError, 'mailpoet-form-yesno-error': showError,
'mailpoet-disabled': attributes.disabled, 'mailpoet-disabled': attributes.disabled,
}) })}
}
data-automation-id={automationId} data-automation-id={automationId}
> >
<label> <label>

View File

@ -1,7 +1,9 @@
import { curry } from 'lodash'; import { curry } from 'lodash';
const setLowercaseValue = curry((setter: (value: string) => void, value: string) => { const setLowercaseValue = curry(
(setter: (value: string) => void, value: string) => {
setter(value.toLowerCase()); setter(value.toLowerCase());
}); },
);
export default setLowercaseValue; export default setLowercaseValue;

View File

@ -8,27 +8,22 @@ export default {
}; };
export function Layouts(): ReactElement { export function Layouts(): ReactElement {
const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi libero sapien, tristique sollicitudin lobortis id, viverra id libero.'; const content =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi libero sapien, tristique sollicitudin lobortis id, viverra id libero.';
return ( return (
<> <>
<Heading level={3}>Column</Heading> <Heading level={3}>Column</Heading>
<Grid.Column className="custom-class"> <Grid.Column className="custom-class">{content}</Grid.Column>
{content}
</Grid.Column>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Grid.Column align="center"> <Grid.Column align="center">{content}</Grid.Column>
{content}
</Grid.Column>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Heading level={3}>Column - small</Heading> <Heading level={3}>Column - small</Heading>
<Grid.Column dimension="small"> <Grid.Column dimension="small">{content}</Grid.Column>
{content}
</Grid.Column>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
@ -77,9 +72,7 @@ export function Layouts(): ReactElement {
<br /> <br />
Part Part
</div> </div>
<div> <div>Right Part</div>
Right Part
</div>
</Grid.SpaceBetween> </Grid.SpaceBetween>
</Grid.Column> </Grid.Column>
@ -93,22 +86,16 @@ export function Layouts(): ReactElement {
<br /> <br />
Part Part
</div> </div>
<div> <div>Right Part</div>
Right Part
</div>
</Grid.SpaceBetween> </Grid.SpaceBetween>
</Grid.Column> </Grid.Column>
<Heading level={3}>Centered row</Heading> <Heading level={3}>Centered row</Heading>
<Grid.Column> <Grid.Column>
<Grid.CenteredRow className="custom-class"> <Grid.CenteredRow className="custom-class">
<div> <div>Left</div>
Left
</div>
<Input type="text" /> <Input type="text" />
<div> <div>Right</div>
Right
</div>
</Grid.CenteredRow> </Grid.CenteredRow>
</Grid.Column> </Grid.Column>
</> </>

View File

@ -16,16 +16,10 @@ export function Column({
}: Props): ReactElement { }: Props): ReactElement {
return ( return (
<div <div
className={ className={classnames(className, 'mailpoet-grid-column', {
classnames(
className,
'mailpoet-grid-column',
{
[`mailpoet-grid-column-${dimension}`]: dimension, [`mailpoet-grid-column-${dimension}`]: dimension,
[`mailpoet-grid-column-${align}`]: align, [`mailpoet-grid-column-${align}`]: align,
}, })}
)
}
> >
{children} {children}
</div> </div>

View File

@ -14,15 +14,10 @@ export function SpaceBetween({
}: Props): ReactElement { }: Props): ReactElement {
return ( return (
<div <div
className={ className={classnames(className, 'mailpoet-grid-space-between', {
classnames( [`mailpoet-grid-space-between-vertical-${verticalAlign}`]:
className, verticalAlign,
'mailpoet-grid-space-between', })}
{
[`mailpoet-grid-space-between-vertical-${verticalAlign}`]: verticalAlign,
},
)
}
> >
{children} {children}
</div> </div>

View File

@ -7,9 +7,16 @@ type Props = {
automationId?: string; automationId?: string;
}; };
export function ThreeColumns({ children, className, automationId }: Props): ReactElement { export function ThreeColumns({
children,
className,
automationId,
}: Props): ReactElement {
return ( return (
<div className={classnames(className, 'mailpoet-grid-three-columns')} data-automation-id={automationId}> <div
className={classnames(className, 'mailpoet-grid-three-columns')}
data-automation-id={automationId}
>
{children} {children}
</div> </div>
); );

View File

@ -6,8 +6,8 @@ function KeyValueTable(props) {
<tbody> <tbody>
{props.rows.map((row) => ( {props.rows.map((row) => (
<tr key={`row_${row.key}`}> <tr key={`row_${row.key}`}>
<td className="row-title">{ row.key }</td> <td className="row-title">{row.key}</td>
<td>{ row.value }</td> <td>{row.value}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -17,14 +17,16 @@ function KeyValueTable(props) {
KeyValueTable.propTypes = { KeyValueTable.propTypes = {
max_width: PropTypes.string, max_width: PropTypes.string,
rows: PropTypes.arrayOf(PropTypes.shape({ rows: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired, key: PropTypes.string.isRequired,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
PropTypes.element, PropTypes.element,
]).isRequired, ]).isRequired,
})).isRequired, }),
).isRequired,
}; };
KeyValueTable.defaultProps = { KeyValueTable.defaultProps = {

View File

@ -14,7 +14,10 @@ MailPoet.I18n.add('openedStatTooltipAverage', 'under 10%');
MailPoet.I18n.add('clickedStatTooltipExcellent', 'above 3%'); MailPoet.I18n.add('clickedStatTooltipExcellent', 'above 3%');
MailPoet.I18n.add('clickedStatTooltipGood', 'between 1 and 3%'); MailPoet.I18n.add('clickedStatTooltipGood', 'between 1 and 3%');
MailPoet.I18n.add('clickedStatTooltipAverage', 'under 1%'); MailPoet.I18n.add('clickedStatTooltipAverage', 'under 1%');
MailPoet.I18n.add('revenueStatsTooltipShort', 'Revenues by customer who clicked on this email in the last 2 weeks.'); MailPoet.I18n.add(
'revenueStatsTooltipShort',
'Revenues by customer who clicked on this email in the last 2 weeks.',
);
export default { export default {
title: 'Listing', title: 'Listing',
@ -36,11 +39,26 @@ export function NewsletterStatsComponent() {
<Heading level={3}>With badges and revenues</Heading> <Heading level={3}>With badges and revenues</Heading>
<NewsletterStats opened={1} clicked={0.1} revenues="10€" newsletterId={4} /> <NewsletterStats
opened={1}
clicked={0.1}
revenues="10€"
newsletterId={4}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<NewsletterStats opened={11} clicked={1.1} revenues="100€" newsletterId={5} /> <NewsletterStats
opened={11}
clicked={1.1}
revenues="100€"
newsletterId={5}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<NewsletterStats opened={31} clicked={3.1} revenues="1000€" newsletterId={6} /> <NewsletterStats
opened={31}
clicked={3.1}
revenues="1000€"
newsletterId={6}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
@ -56,11 +74,29 @@ export function NewsletterStatsComponent() {
<Heading level={3}>No badges, with revenues</Heading> <Heading level={3}>No badges, with revenues</Heading>
<NewsletterStats hideBadges opened={1} clicked={0.1} revenues="10€" newsletterId={10} /> <NewsletterStats
hideBadges
opened={1}
clicked={0.1}
revenues="10€"
newsletterId={10}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<NewsletterStats hideBadges opened={11} clicked={1.1} revenues="100€" newsletterId={11} /> <NewsletterStats
hideBadges
opened={11}
clicked={1.1}
revenues="100€"
newsletterId={11}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<NewsletterStats hideBadges opened={31} clicked={3.1} revenues="1000€" newsletterId={12} /> <NewsletterStats
hideBadges
opened={31}
clicked={3.1}
revenues="1000€"
newsletterId={12}
/>
</> </>
); );
} }

View File

@ -40,7 +40,12 @@ export function NewsletterStatuses() {
<NewsletterStatus total={200} processed={0} /> <NewsletterStatus total={200} processed={0} />
<NewsletterStatus total={400} processed={150} /> <NewsletterStatus total={400} processed={150} />
<NewsletterStatus scheduledFor={inPast} total={300} processed={270} /> <NewsletterStatus scheduledFor={inPast} total={300} processed={270} />
<NewsletterStatus scheduledFor={inPast} total={300} processed={270} isPaused /> <NewsletterStatus
scheduledFor={inPast}
total={300}
processed={270}
isPaused
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />

View File

@ -1,2 +1,5 @@
export { default as NewsletterStats } from './newsletter_stats'; export { default as NewsletterStats } from './newsletter_stats';
export { default as NewsletterStatus, ScheduledIcon } from './newsletter_status'; export {
default as NewsletterStatus,
ScheduledIcon,
} from './newsletter_status';

View File

@ -32,8 +32,7 @@ function NewsletterStats({
% %
<br /> <br />
<span className="mailpoet-listing-stats-percentages-opens"> <span className="mailpoet-listing-stats-percentages-opens">
{openedDisplay} {openedDisplay}%
%
</span> </span>
</div> </div>
{!hideBadges && ( {!hideBadges && (
@ -53,12 +52,10 @@ function NewsletterStats({
const revenuesTooltipId = `revenues-${newsletterId || '0'}`; const revenuesTooltipId = `revenues-${newsletterId || '0'}`;
revenueStats = ( revenueStats = (
<div> <div>
<Tag data-tip data-for={revenuesTooltipId}>{revenues}</Tag> <Tag data-tip data-for={revenuesTooltipId}>
<Tooltip {revenues}
place="top" </Tag>
multiline <Tooltip place="top" multiline id={revenuesTooltipId}>
id={revenuesTooltipId}
>
<div className="mailpoet-listing-stats-tooltip-content"> <div className="mailpoet-listing-stats-tooltip-content">
{MailPoet.I18n.t('revenueStatsTooltipShort')} {MailPoet.I18n.t('revenueStatsTooltipShort')}
</div> </div>
@ -68,7 +65,10 @@ function NewsletterStats({
} }
if (wrapContentInLink) { if (wrapContentInLink) {
clickedAndOpenedStats = wrapContentInLink(clickedAndOpenedStats, 'opened-and-clicked'); clickedAndOpenedStats = wrapContentInLink(
clickedAndOpenedStats,
'opened-and-clicked',
);
revenueStats = wrapContentInLink(revenueStats, 'revenue'); revenueStats = wrapContentInLink(revenueStats, 'revenue');
} }

View File

@ -22,15 +22,10 @@ function Badge({
}: BadgeProps) { }: BadgeProps) {
return ( return (
<span> <span>
<Tag <Tag isInverted={isInverted} variant={type} data-tip data-for={tooltipId}>
isInverted={isInverted}
variant={type}
data-tip
data-for={tooltipId}
>
{name} {name}
</Tag> </Tag>
{ tooltip && ( {tooltip && (
<Tooltip <Tooltip
place={tooltipPlace || 'top'} place={tooltipPlace || 'top'}
multiline multiline
@ -38,7 +33,7 @@ function Badge({
> >
{tooltip} {tooltip}
</Tooltip> </Tooltip>
) } )}
</span> </span>
); );
} }

View File

@ -13,11 +13,7 @@ type StatsBadgeProps = {
const stats = { const stats = {
opened: { opened: {
badgeRanges: [30, 10, 0], badgeRanges: [30, 10, 0],
badgeTypes: [ badgeTypes: ['excellent', 'good', 'average'],
'excellent',
'good',
'average',
],
tooltipText: [ tooltipText: [
MailPoet.I18n.t('openedStatTooltipExcellent'), MailPoet.I18n.t('openedStatTooltipExcellent'),
MailPoet.I18n.t('openedStatTooltipGood'), MailPoet.I18n.t('openedStatTooltipGood'),
@ -26,11 +22,7 @@ const stats = {
}, },
clicked: { clicked: {
badgeRanges: [3, 1, 0], badgeRanges: [3, 1, 0],
badgeTypes: [ badgeTypes: ['excellent', 'good', 'average'],
'excellent',
'good',
'average',
],
tooltipText: [ tooltipText: [
MailPoet.I18n.t('clickedStatTooltipExcellent'), MailPoet.I18n.t('clickedStatTooltipExcellent'),
MailPoet.I18n.t('clickedStatTooltipGood'), MailPoet.I18n.t('clickedStatTooltipGood'),
@ -92,24 +84,15 @@ export function StatsBadge(props: StatsBadgeProps) {
{badge.tooltipTitle.toUpperCase()} {badge.tooltipTitle.toUpperCase()}
</div> </div>
<div className="mailpoet-listing-stats-tooltip-content"> <div className="mailpoet-listing-stats-tooltip-content">
<Badge <Badge type="excellent" name={badges.excellent.name} />
type="excellent"
name={badges.excellent.name}
/>
{' : '} {' : '}
{stat.tooltipText[0]} {stat.tooltipText[0]}
<br /> <br />
<Badge <Badge type="good" name={badges.good.name} />
type="good"
name={badges.good.name}
/>
{' : '} {' : '}
{stat.tooltipText[1]} {stat.tooltipText[1]}
<br /> <br />
<Badge <Badge type="average" name={badges.average.name} />
type="average"
name={badges.average.name}
/>
{' : '} {' : '}
{stat.tooltipText[2]} {stat.tooltipText[2]}
</div> </div>

View File

@ -1,8 +1,6 @@
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { addDays, differenceInMinutes, isFuture, isPast } from 'date-fns';
addDays, differenceInMinutes, isFuture, isPast,
} from 'date-fns';
import t from 'common/functions/t'; import t from 'common/functions/t';
import Tooltip from '../tooltip/tooltip'; import Tooltip from '../tooltip/tooltip';
@ -15,8 +13,18 @@ function CircularProgress({ percentage }: CircularProgressProps) {
const filled = perimeter * (percentage / 100); const filled = perimeter * (percentage / 100);
const empty = perimeter - filled; const empty = perimeter - filled;
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg
<circle cx="12" cy="12" r="8" className="mailpoet-listing-status-percentage-background" /> xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<circle
cx="12"
cy="12"
r="8"
className="mailpoet-listing-status-percentage-background"
/>
<circle <circle
r="8" r="8"
cx="12" cx="12"
@ -32,8 +40,17 @@ function CircularProgress({ percentage }: CircularProgressProps) {
export function ScheduledIcon() { export function ScheduledIcon() {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg
<path className="mailpoet-listing-status-scheduled-icon" strokeLinecap="round" d="M12 7L12 12 15 15" /> xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
className="mailpoet-listing-status-scheduled-icon"
strokeLinecap="round"
d="M12 7L12 12 15 15"
/>
</svg> </svg>
); );
} }
@ -55,18 +72,20 @@ function NewsletterStatus({
}: NewsletterStatusProps) { }: NewsletterStatusProps) {
const unknown = !scheduledFor && !processed && !total; const unknown = !scheduledFor && !processed && !total;
const scheduled = scheduledFor && isFuture(scheduledFor); const scheduled = scheduledFor && isFuture(scheduledFor);
const inProgress = (!scheduledFor || isPast(scheduledFor)) && processed < total; const inProgress =
(!scheduledFor || isPast(scheduledFor)) && processed < total;
const sent = (!scheduledFor || isPast(scheduledFor)) && processed >= total; const sent = (!scheduledFor || isPast(scheduledFor)) && processed >= total;
const sentWithoutQueue = status === 'sent' && total === undefined; const sentWithoutQueue = status === 'sent' && total === undefined;
let percentage = 0; let percentage = 0;
let label : string | JSX.Element = t('notSentYet'); let label: string | JSX.Element = t('notSentYet');
if (scheduled) { if (scheduled) {
const scheduledDate = MailPoet.Date.short(scheduledFor); const scheduledDate = MailPoet.Date.short(scheduledFor);
const scheduledTime = MailPoet.Date.time(scheduledFor); const scheduledTime = MailPoet.Date.time(scheduledFor);
const now = new Date(); const now = new Date();
const tomorrow = addDays(now, 1); const tomorrow = addDays(now, 1);
const isScheduledForToday = MailPoet.Date.short(now) === scheduledDate; const isScheduledForToday = MailPoet.Date.short(now) === scheduledDate;
const isScheduledForTomorrow = MailPoet.Date.short(tomorrow) === scheduledDate; const isScheduledForTomorrow =
MailPoet.Date.short(tomorrow) === scheduledDate;
if (isScheduledForToday || isScheduledForTomorrow) { if (isScheduledForToday || isScheduledForTomorrow) {
const randomId = Math.random().toString(36).substring(2, 15); const randomId = Math.random().toString(36).substring(2, 15);
const dateWord = isScheduledForToday ? t('today') : t('tomorrow'); const dateWord = isScheduledForToday ? t('today') : t('tomorrow');
@ -99,10 +118,14 @@ function NewsletterStatus({
percentage = 100; percentage = 100;
} }
} else if (inProgress) { } else if (inProgress) {
label = `${MailPoet.Num.toLocaleFixed(processed)} / ${MailPoet.Num.toLocaleFixed(total)}`; label = `${MailPoet.Num.toLocaleFixed(
processed,
)} / ${MailPoet.Num.toLocaleFixed(total)}`;
percentage = 100 * (processed / total); percentage = 100 * (processed / total);
} else if (sent) { } else if (sent) {
label = `${MailPoet.Num.toLocaleFixed(total)} / ${MailPoet.Num.toLocaleFixed(total)}`; label = `${MailPoet.Num.toLocaleFixed(
total,
)} / ${MailPoet.Num.toLocaleFixed(total)}`;
percentage = 100; percentage = 100;
} else if (sentWithoutQueue) { } else if (sentWithoutQueue) {
label = t('sent'); label = t('sent');
@ -112,7 +135,8 @@ function NewsletterStatus({
label = t('paused'); label = t('paused');
} }
return ( return (
<div className={classNames({ <div
className={classNames({
'mailpoet-listing-status': true, 'mailpoet-listing-status': true,
'mailpoet-listing-status-unknown': unknown, 'mailpoet-listing-status-unknown': unknown,
'mailpoet-listing-status-scheduled': scheduled, 'mailpoet-listing-status-scheduled': scheduled,

View File

@ -9,24 +9,16 @@ export function Loaders() {
return ( return (
<> <>
<p> <p>
Default loader: Default loader: <Loader />
{' '}
<Loader />
</p> </p>
<p> <p>
Light loader: Light loader: <Loader variant="light" />
{' '}
<Loader variant="light" />
</p> </p>
<p> <p>
Dark loader: Dark loader: <Loader variant="dark" />
{' '}
<Loader variant="dark" />
</p> </p>
<p> <p>
bigger loader: bigger loader: <Loader size={64} />
{' '}
<Loader size={64} />
</p> </p>
</> </>
); );

View File

@ -6,21 +6,50 @@ import Heading from '../../typography/heading/heading';
const shortContent = ( const shortContent = (
<> <>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Morbi libero sapien, tristique sollicitudin lobortis id, viverra id libero.</p> <p>
Morbi libero sapien, tristique sollicitudin lobortis id, viverra id
libero.
</p>
<p>Mauris dolor felis, sagittis at, luctus sed, aliquam non, tellus.</p> <p>Mauris dolor felis, sagittis at, luctus sed, aliquam non, tellus.</p>
</> </>
); );
const longContent = ( const longContent = (
<> <>
<p>{'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20)}</p> <p>
<p>{'Morbi libero sapien, tristique sollicitudin lobortis id, viverra id libero. '.repeat(20)}</p> {'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20)}
<p>{'Mauris dolor felis, sagittis at, luctus sed, aliquam non, tellus. '.repeat(20)}</p> </p>
<p>
{'Morbi libero sapien, tristique sollicitudin lobortis id, viverra id libero. '.repeat(
20,
)}
</p>
<p>
{'Mauris dolor felis, sagittis at, luctus sed, aliquam non, tellus. '.repeat(
20,
)}
</p>
<p>{'Vivamus ac leo pretium faucibus.'.repeat(20)}</p> <p>{'Vivamus ac leo pretium faucibus.'.repeat(20)}</p>
<p>{'Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. '.repeat(20)}</p> <p>
<p>{'Duis sapien nunc, commodo et, interdum suscipit, sollicitudin et, dolor. '.repeat(20)}</p> {'Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. '.repeat(
<p>{'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. '.repeat(20)}</p> 20,
<p>{'Cras pede libero, dapibus nec, pretium sit amet, tempor quis. '.repeat(20)}</p> )}
</p>
<p>
{'Duis sapien nunc, commodo et, interdum suscipit, sollicitudin et, dolor. '.repeat(
20,
)}
</p>
<p>
{'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. '.repeat(
20,
)}
</p>
<p>
{'Cras pede libero, dapibus nec, pretium sit amet, tempor quis. '.repeat(
20,
)}
</p>
</> </>
); );
@ -55,7 +84,7 @@ function ModalWrapper({
> >
{content} {content}
</Modal> </Modal>
) } )}
</p> </p>
); );
} }
@ -65,25 +94,71 @@ export function Modals() {
<> <>
<Heading level={3}>Modal with short text</Heading> <Heading level={3}>Modal with short text</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" /> <ModalWrapper buttonCaption="Show modal with title, with close button" />
<ModalWrapper buttonCaption="Show modal with title, without close button" isDismissible={false} /> <ModalWrapper
<ModalWrapper buttonCaption="Show modal without title, with close button" title={null} /> buttonCaption="Show modal with title, without close button"
<ModalWrapper buttonCaption="Show modal without title, without close button" title={null} isDismissible={false} /> isDismissible={false}
/>
<ModalWrapper
buttonCaption="Show modal without title, with close button"
title={null}
/>
<ModalWrapper
buttonCaption="Show modal without title, without close button"
title={null}
isDismissible={false}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Heading level={3}>Modal with long text</Heading> <Heading level={3}>Modal with long text</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" content={longContent} /> <ModalWrapper
<ModalWrapper buttonCaption="Show modal with title, without close button" isDismissible={false} content={longContent} /> buttonCaption="Show modal with title, with close button"
<ModalWrapper buttonCaption="Show modal without title, with close button" title={null} content={longContent} /> content={longContent}
<ModalWrapper buttonCaption="Show modal without title, without close button" title={null} isDismissible={false} content={longContent} /> />
<ModalWrapper
buttonCaption="Show modal with title, without close button"
isDismissible={false}
content={longContent}
/>
<ModalWrapper
buttonCaption="Show modal without title, with close button"
title={null}
content={longContent}
/>
<ModalWrapper
buttonCaption="Show modal without title, without close button"
title={null}
isDismissible={false}
content={longContent}
/>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
<Heading level={3}>Full-screen modal</Heading> <Heading level={3}>Full-screen modal</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" content={longContent} fullScreen /> <ModalWrapper
<ModalWrapper buttonCaption="Show modal with title, without close button" isDismissible={false} content={longContent} fullScreen /> buttonCaption="Show modal with title, with close button"
<ModalWrapper buttonCaption="Show modal without title, with close button" title={null} content={longContent} fullScreen /> content={longContent}
<ModalWrapper buttonCaption="Show modal without title, without close button" title={null} isDismissible={false} content={longContent} fullScreen /> fullScreen
/>
<ModalWrapper
buttonCaption="Show modal with title, without close button"
isDismissible={false}
content={longContent}
fullScreen
/>
<ModalWrapper
buttonCaption="Show modal without title, with close button"
title={null}
content={longContent}
fullScreen
/>
<ModalWrapper
buttonCaption="Show modal without title, without close button"
title={null}
isDismissible={false}
content={longContent}
fullScreen
/>
</> </>
); );
} }

View File

@ -7,11 +7,7 @@ type Props = {
children: ReactNode; children: ReactNode;
}; };
function ModalFrame({ function ModalFrame({ fullScreen = false, className = '', children }: Props) {
fullScreen = false,
className = '',
children,
}: Props) {
return ( return (
<div <div
className={classnames( className={classnames(

View File

@ -7,9 +7,7 @@ type Props = {
function ModalHeader({ title }: Props) { function ModalHeader({ title }: Props) {
return ( return (
<div className="mailpoet-modal-header"> <div className="mailpoet-modal-header">
<Heading level={3}> <Heading level={3}>{title}</Heading>
{ title }
</Heading>
</div> </div>
); );
} }

View File

@ -38,14 +38,9 @@ function Modal({
shouldCloseOnClickOutside={shouldCloseOnClickOutside} shouldCloseOnClickOutside={shouldCloseOnClickOutside}
className={overlayClassName} className={overlayClassName}
> >
<ModalFrame <ModalFrame className={contentClassName} fullScreen={fullScreen}>
className={contentClassName} {title && <ModalHeader title={title} />}
fullScreen={fullScreen} {isDismissible && (
>
{ title && (
<ModalHeader title={title} />
) }
{ isDismissible && (
<button <button
type="button" type="button"
onClick={onRequestClose} onClick={onRequestClose}
@ -54,12 +49,9 @@ function Modal({
> >
{ModalCloseIcon} {ModalCloseIcon}
</button> </button>
) } )}
<div <div className="mailpoet-modal-content" role="document">
className="mailpoet-modal-content" {children}
role="document"
>
{ children }
</div> </div>
</ModalFrame> </ModalFrame>
</ModalOverlay>, </ModalOverlay>,

View File

@ -31,25 +31,27 @@ const getBannerMessage = (translationKey: string) => {
{ReactStringReplace( {ReactStringReplace(
message, message,
/(\[subscribersCount]|\[subscribersLimit])/g, /(\[subscribersCount]|\[subscribersLimit])/g,
(match) => ((match === '[subscribersCount]') ? subscribersCount : subscribersLimit), (match) =>
match === '[subscribersCount]' ? subscribersCount : subscribersLimit,
)} )}
</p> </p>
); );
}; };
const getCtaButton = (translationKey: string, link: string, target = '_blank') => ( const getCtaButton = (
<Button translationKey: string,
href={link} link: string,
target={target} target = '_blank',
rel="noopener noreferrer" ) => (
> <Button href={link} target={target} rel="noopener noreferrer">
{MailPoet.I18n.t(translationKey)} {MailPoet.I18n.t(translationKey)}
</Button> </Button>
); );
function PremiumBannerWithUpgrade( function PremiumBannerWithUpgrade({
{ message, actionButton }: Props, message,
) : JSX.Element { actionButton,
}: Props): JSX.Element {
let bannerMessage: ReactNode; let bannerMessage: ReactNode;
let ctaButton: ReactNode; let ctaButton: ReactNode;
@ -57,14 +59,25 @@ function PremiumBannerWithUpgrade(
bannerMessage = getBannerMessage('premiumFeatureDescription'); bannerMessage = getBannerMessage('premiumFeatureDescription');
ctaButton = isPremiumPluginInstalled ctaButton = isPremiumPluginInstalled
? getCtaButton('premiumFeatureButtonActivatePremium', premiumPluginActivationUrl, '_self') ? getCtaButton(
: getCtaButton('premiumFeatureButtonDownloadPremium', premiumPluginDownloadUrl); 'premiumFeatureButtonActivatePremium',
premiumPluginActivationUrl,
'_self',
)
: getCtaButton(
'premiumFeatureButtonDownloadPremium',
premiumPluginDownloadUrl,
);
} else if (subscribersLimitReached) { } else if (subscribersLimitReached) {
bannerMessage = getBannerMessage('premiumFeatureDescriptionSubscribersLimitReached'); bannerMessage = getBannerMessage(
'premiumFeatureDescriptionSubscribersLimitReached',
);
const link = anyValidKey const link = anyValidKey
? MailPoet.MailPoetComUrlFactory.getUpgradeUrl(pluginPartialKey) ? MailPoet.MailPoetComUrlFactory.getUpgradeUrl(pluginPartialKey)
: MailPoet.MailPoetComUrlFactory.getPurchasePlanUrl(+subscribersCount + 1); : MailPoet.MailPoetComUrlFactory.getPurchasePlanUrl(
+subscribersCount + 1,
);
ctaButton = getCtaButton('premiumFeatureButtonUpgradePlan', link); ctaButton = getCtaButton('premiumFeatureButtonUpgradePlan', link);
} else { } else {

View File

@ -11,18 +11,16 @@ export function PremiumsRequired() {
<div> <div>
<PremiumRequired <PremiumRequired
title="This is a Premium Feature" title="This is a Premium Feature"
message={( message={
<p> <p>
Learn more about your subscribers and optimize your campaigns. See who Learn more about your subscribers and optimize your campaigns. See
opened your emails, which links they clicked, and then use the data to make y who opened your emails, which links they clicked, and then use the
our emails even better. And if you run a WooCommerce store, you will also data to make y our emails even better. And if you run a WooCommerce
see the revenue earned per email. store, you will also see the revenue earned per email.
<a href="#"> <a href="#">Learn more.</a>
Learn more.
</a>
</p> </p>
)} }
actionButton={(<Button href="#">Sign Up</Button>)} actionButton={<Button href="#">Sign Up</Button>}
/> />
</div> </div>
); );

View File

@ -13,15 +13,11 @@ function PremiumRequired({ title, message, actionButton }: Props) {
<div className="mailpoet-premium-required"> <div className="mailpoet-premium-required">
<div className="mailpoet-premium-required-message"> <div className="mailpoet-premium-required-message">
<Heading level={5}> <Heading level={5}>
<Badge title="Premium" /> <Badge title="Premium" /> {title}
{' '}
{title}
</Heading> </Heading>
{message} {message}
</div> </div>
<div className="mailpoet-premium-required-button"> <div className="mailpoet-premium-required-button">{actionButton}</div>
{actionButton}
</div>
</div> </div>
); );
} }

View File

@ -1,8 +1,19 @@
export default function DesktopIcon() { export default function DesktopIcon() {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"> <svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd"> <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g className="mailpoet_preview_icon_fill" transform="translate(-712.000000, -64.000000)" fillRule="nonzero"> <g
className="mailpoet_preview_icon_fill"
transform="translate(-712.000000, -64.000000)"
fillRule="nonzero"
>
<g transform="translate(712.000000, 64.000000)"> <g transform="translate(712.000000, 64.000000)">
<g> <g>
<path d="M13.965,20.5 C14.1138804,20.5 14.2550143,20.5663566 14.35,20.681 L14.35,20.681 L16.449,23.213 C16.5561084,23.3657981 16.5692196,23.5655248 16.4830056,23.7310137 C16.3967915,23.8965027 16.2255996,24.0002137 16.039,24 L16.039,24 L7.96,24 C7.7734004,24.0002137 7.60220849,23.8965027 7.51599443,23.7310137 C7.42978036,23.5655248 7.44289164,23.3657981 7.55,23.213 L7.55,23.213 L9.65,20.681 C9.74498572,20.5663566 9.88611956,20.5 10.035,20.5 L10.035,20.5 Z M22.5,-4.08562073e-14 C23.3284271,-4.08562073e-14 24,0.671572875 24,1.5 L24,1.5 L24,17.5 C24,18.3284271 23.3284271,19 22.5,19 L22.5,19 L1.5,19 C0.671572875,19 0,18.3284271 0,17.5 L0,17.5 L0,1.5 C0,0.671572875 0.671572875,-4.08562073e-14 1.5,-4.08562073e-14 L1.5,-4.08562073e-14 Z M21.5,2 L2.5,2 C2.22385763,2 2,2.22385763 2,2.5 L2,2.5 L2,14.5 C2,14.7761424 2.22385763,15 2.5,15 L2.5,15 L21.5,15 C21.7761424,15 22,14.7761424 22,14.5 L22,14.5 L22,2.5 C22,2.22385763 21.7761424,2 21.5,2 L21.5,2 Z" /> <path d="M13.965,20.5 C14.1138804,20.5 14.2550143,20.5663566 14.35,20.681 L14.35,20.681 L16.449,23.213 C16.5561084,23.3657981 16.5692196,23.5655248 16.4830056,23.7310137 C16.3967915,23.8965027 16.2255996,24.0002137 16.039,24 L16.039,24 L7.96,24 C7.7734004,24.0002137 7.60220849,23.8965027 7.51599443,23.7310137 C7.42978036,23.5655248 7.44289164,23.3657981 7.55,23.213 L7.55,23.213 L9.65,20.681 C9.74498572,20.5663566 9.88611956,20.5 10.035,20.5 L10.035,20.5 Z M22.5,-4.08562073e-14 C23.3284271,-4.08562073e-14 24,0.671572875 24,1.5 L24,1.5 L24,17.5 C24,18.3284271 23.3284271,19 22.5,19 L22.5,19 L1.5,19 C0.671572875,19 0,18.3284271 0,17.5 L0,17.5 L0,1.5 C0,0.671572875 0.671572875,-4.08562073e-14 1.5,-4.08562073e-14 L1.5,-4.08562073e-14 Z M21.5,2 L2.5,2 C2.22385763,2 2,2.22385763 2,2.5 L2,2.5 L2,14.5 C2,14.7761424 2.22385763,15 2.5,15 L2.5,15 L21.5,15 C21.7761424,15 22,14.7761424 22,14.5 L22,14.5 L22,2.5 C22,2.22385763 21.7761424,2 21.5,2 L21.5,2 Z" />

View File

@ -1,11 +1,25 @@
export default function MobileIcon() { export default function MobileIcon() {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16px" height="24px" viewBox="0 0 16 24" version="1.1"> <svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="16px"
height="24px"
viewBox="0 0 16 24"
version="1.1"
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd"> <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g className="mailpoet_preview_icon_fill" transform="translate(-760.000000, -64.000000)" fillRule="nonzero"> <g
className="mailpoet_preview_icon_fill"
transform="translate(-760.000000, -64.000000)"
fillRule="nonzero"
>
<g transform="translate(712.000000, 64.000000)"> <g transform="translate(712.000000, 64.000000)">
<g transform="translate(48.000000, 0.000000)"> <g transform="translate(48.000000, 0.000000)">
<path d="M16,3 C16,1.34314575 14.6568542,0 13,0 L3,0 C1.34314575,0 0,1.34314575 0,3 L0,21 C0,22.6568542 1.34314575,24 3,24 L13,24 C14.6568542,24 16,22.6568542 16,21 L16,3 Z M14,5.5 L14,17.5 C14,18.0522847 13.5522847,18.5 13,18.5 L3,18.5 C2.44771525,18.5 2,18.0522847 2,17.5 L2,5.5 C2,4.94771525 2.44771525,4.5 3,4.5 L13,4.5 C13.5522847,4.5 14,4.94771525 14,5.5 Z M7,21 C7,20.4477153 7.44771525,20 8,20 C8.55228475,20 9,20.4477153 9,21 C9,21.5522847 8.55228475,22 8,22 C7.44771525,22 7,21.5522847 7,21 Z" id="Shape" /> <path
d="M16,3 C16,1.34314575 14.6568542,0 13,0 L3,0 C1.34314575,0 0,1.34314575 0,3 L0,21 C0,22.6568542 1.34314575,24 3,24 L13,24 C14.6568542,24 16,22.6568542 16,21 L16,3 Z M14,5.5 L14,17.5 C14,18.0522847 13.5522847,18.5 13,18.5 L3,18.5 C2.44771525,18.5 2,18.0522847 2,17.5 L2,5.5 C2,4.94771525 2.44771525,4.5 3,4.5 L13,4.5 C13.5522847,4.5 14,4.94771525 14,5.5 Z M7,21 C7,20.4477153 7.44771525,20 8,20 C8.55228475,20 9,20.4477153 9,21 C9,21.5522847 8.55228475,22 8,22 C7.44771525,22 7,21.5522847 7,21 Z"
id="Shape"
/>
</g> </g>
</g> </g>
</g> </g>

View File

@ -5,11 +5,7 @@ import classnames from 'classnames';
import MobileIcon from './mobile_icon'; import MobileIcon from './mobile_icon';
import DesktopIcon from './desktop_icon'; import DesktopIcon from './desktop_icon';
function Preview({ function Preview({ children, onDisplayTypeChange, selectedDisplayType }) {
children,
onDisplayTypeChange,
selectedDisplayType,
}) {
const [displayType, setDisplayType] = useState(selectedDisplayType); const [displayType, setDisplayType] = useState(selectedDisplayType);
const changeType = (type) => { const changeType = (type) => {
setDisplayType(type); setDisplayType(type);
@ -19,7 +15,9 @@ function Preview({
<div className="mailpoet_browser_preview"> <div className="mailpoet_browser_preview">
<div className="mailpoet_browser_preview_toggle"> <div className="mailpoet_browser_preview_toggle">
<a <a
className={classnames('mailpoet_browser_preview_icon', { mailpoet_active: displayType === 'desktop' })} className={classnames('mailpoet_browser_preview_icon', {
mailpoet_active: displayType === 'desktop',
})}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
changeType('desktop'); changeType('desktop');
@ -31,7 +29,9 @@ function Preview({
<DesktopIcon /> <DesktopIcon />
</a> </a>
<a <a
className={classnames('mailpoet_browser_preview_icon', { mailpoet_active: displayType === 'mobile' })} className={classnames('mailpoet_browser_preview_icon', {
mailpoet_active: displayType === 'mobile',
})}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
changeType('mobile'); changeType('mobile');
@ -46,16 +46,22 @@ function Preview({
<div <div
className={classnames( className={classnames(
'mailpoet_browser_preview_container', 'mailpoet_browser_preview_container',
{ mailpoet_browser_preview_container_mobile: displayType !== 'desktop' }, {
{ mailpoet_browser_preview_container_desktop: displayType === 'desktop' }, mailpoet_browser_preview_container_mobile:
displayType !== 'desktop',
},
{
mailpoet_browser_preview_container_desktop:
displayType === 'desktop',
},
)} )}
> >
<div className="mailpoet_browser_preview_border"> <div className="mailpoet_browser_preview_border">{children}</div>
{children}
</div> </div>
</div> {displayType !== 'desktop' && (
{(displayType !== 'desktop') && ( <p className="mailpoet_form_preview_disclaimer">
<p className="mailpoet_form_preview_disclaimer">{MailPoet.I18n.t('formPreviewMobileDisclaimer')}</p> {MailPoet.I18n.t('formPreviewMobileDisclaimer')}
</p>
)} )}
</div> </div>
); );

View File

@ -4,9 +4,9 @@ import MailPoet from 'mailpoet';
function PrintBoolean(props) { function PrintBoolean(props) {
return ( return (
<span> <span>
{(props.children === true && props.truthy) {(props.children === true && props.truthy) ||
|| (props.children === false && props.falsy) (props.children === false && props.falsy) ||
|| (props.unknown)} props.unknown}
</span> </span>
); );
} }

View File

@ -23,9 +23,10 @@ function SenderEmailAddressWarning({ emailAddress, mssActive }) {
/(%1\$s|%2\$s|<em>.*<\/em>)/, /(%1\$s|%2\$s|<em>.*<\/em>)/,
(match) => { (match) => {
if (match === '%1$s') return suggestedEmailAddress; if (match === '%1$s') return suggestedEmailAddress;
if (match === '%2$s') return <em key="sender-email">{ emailAddress }</em>; if (match === '%2$s')
return <em key="sender-email">{emailAddress}</em>;
return <em key="reply-to">{match.replace(/<\/?em>/g, '')}</em>; return <em key="reply-to">{match.replace(/<\/?em>/g, '')}</em>;
} },
)} )}
</p> </p>
<p className="sender_email_address_warning"> <p className="sender_email_address_warning">

View File

@ -11,16 +11,20 @@ import { ErrorResponse, isErrorResponse } from '../ajax';
* @param {string|null} address * @param {string|null} address
* @returns {Promise} * @returns {Promise}
*/ */
const handleSave = (address: string | null) => MailPoet.Ajax.post({ const handleSave = (address: string | null) =>
MailPoet.Ajax.post({
api_version: MailPoet.apiVersion, api_version: MailPoet.apiVersion,
endpoint: 'settings', endpoint: 'settings',
action: 'setAuthorizedFromAddress', action: 'setAuthorizedFromAddress',
data: { data: {
address, address,
}, },
}); });
const getErrorMessage = (error: ErrorResponse['errors'][number] | null, address: string | null): string => { const getErrorMessage = (
error: ErrorResponse['errors'][number] | null,
address: string | null,
): string => {
if (!error) { if (!error) {
return MailPoet.I18n.t('setFromAddressEmailUnknownError'); return MailPoet.I18n.t('setFromAddressEmailUnknownError');
} }
@ -55,15 +59,21 @@ const getSuccessMessage = (): JSX.Element => (
); );
const removeUnauthorizedEmailNotices = () => { const removeUnauthorizedEmailNotices = () => {
const unauthorizedEmailNotice = document.querySelector('[data-notice="unauthorized-email-addresses-notice"]'); const unauthorizedEmailNotice = document.querySelector(
'[data-notice="unauthorized-email-addresses-notice"]',
);
if (unauthorizedEmailNotice) { if (unauthorizedEmailNotice) {
unauthorizedEmailNotice.remove(); unauthorizedEmailNotice.remove();
} }
const unauthorizedEmailInNewsletterNotice = document.querySelector('[data-notice="unauthorized-email-in-newsletters-addresses-notice"]'); const unauthorizedEmailInNewsletterNotice = document.querySelector(
'[data-notice="unauthorized-email-in-newsletters-addresses-notice"]',
);
if (unauthorizedEmailInNewsletterNotice) { if (unauthorizedEmailInNewsletterNotice) {
unauthorizedEmailInNewsletterNotice.remove(); unauthorizedEmailInNewsletterNotice.remove();
} }
const unauthorizedEmailInNewsletterDynamicNotice = document.querySelector('[data-id="mailpoet_authorization_error"]'); const unauthorizedEmailInNewsletterDynamicNotice = document.querySelector(
'[data-id="mailpoet_authorization_error"]',
);
if (unauthorizedEmailInNewsletterDynamicNotice) { if (unauthorizedEmailInNewsletterDynamicNotice) {
unauthorizedEmailInNewsletterDynamicNotice.remove(); unauthorizedEmailInNewsletterDynamicNotice.remove();
} }
@ -86,8 +96,7 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
contentClassName="set-from-address-modal" contentClassName="set-from-address-modal"
> >
<p> <p>
{ {ReactStringReplace(
ReactStringReplace(
MailPoet.I18n.t('setFromAddressModalDescription'), MailPoet.I18n.t('setFromAddressModalDescription'),
/\[link\](.*?)\[\/link\]/g, /\[link\](.*?)\[\/link\]/g,
(match) => ( (match) => (
@ -100,8 +109,7 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
{match} {match}
</a> </a>
), ),
) )}
}
</p> </p>
<input <input
@ -112,7 +120,9 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
data-parsley-type="email" data-parsley-type="email"
onChange={(event) => { onChange={(event) => {
setAddress(event.target.value.trim() || null); setAddress(event.target.value.trim() || null);
const addressValidator = jQuery('#mailpoet-set-from-address-modal-input').parsley(); const addressValidator = jQuery(
'#mailpoet-set-from-address-modal-input',
).parsley();
addressValidator.removeError('saveError'); addressValidator.removeError('saveError');
}} }}
/> />
@ -122,7 +132,9 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
type="submit" type="submit"
value={MailPoet.I18n.t('setFromAddressModalSave')} value={MailPoet.I18n.t('setFromAddressModalSave')}
onClick={async () => { onClick={async () => {
const addressValidator = jQuery('#mailpoet-set-from-address-modal-input').parsley(); const addressValidator = jQuery(
'#mailpoet-set-from-address-modal-input',
).parsley();
addressValidator.validate(); addressValidator.validate();
if (!addressValidator.isValid()) { if (!addressValidator.isValid()) {
return; return;
@ -137,9 +149,12 @@ function SetFromAddressModal({ onRequestClose, setAuthorizedAddress }: Props) {
removeUnauthorizedEmailNotices(); removeUnauthorizedEmailNotices();
notices.success(getSuccessMessage(), { timeout: false }); notices.success(getSuccessMessage(), { timeout: false });
} catch (e) { } catch (e) {
const error = isErrorResponse(e) && e.errors[0] ? e.errors[0] : null; const error =
isErrorResponse(e) && e.errors[0] ? e.errors[0] : null;
if (error.error === 'unauthorized') { if (error.error === 'unauthorized') {
MailPoet.trackEvent('Unauthorized email used', { 'Unauthorized email source': 'modal' }); MailPoet.trackEvent('Unauthorized email used', {
'Unauthorized email source': 'modal',
});
} }
const message = getErrorMessage(error, address); const message = getErrorMessage(error, address);
addressValidator.addError('saveError', { message }); addressValidator.addError('saveError', { message });

View File

@ -22,8 +22,9 @@ export function StepsWithoutTitles() {
<Heading level={3}>{`Step ${step}`}</Heading> <Heading level={3}>{`Step ${step}`}</Heading>
<p> <p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta natus Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta natus
consequuntur saepe harum nesciunt eum, a nulla facilis architecto incidunt consequuntur saepe harum nesciunt eum, a nulla facilis architecto
odio voluptas praesentium, ipsa laboriosam animi! Officiis atque odio nulla. incidunt odio voluptas praesentium, ipsa laboriosam animi! Officiis
atque odio nulla.
</p> </p>
<div> <div>
<Button <Button
@ -34,11 +35,7 @@ export function StepsWithoutTitles() {
> >
Previous step Previous step
</Button> </Button>
<Button <Button onClick={nextStep} dimension="small" isDisabled={step === 5}>
onClick={nextStep}
dimension="small"
isDisabled={step === 5}
>
Next step Next step
</Button> </Button>
</div> </div>
@ -55,13 +52,18 @@ export function StepsWithTitles() {
return ( return (
<> <>
<Steps count={5} current={step} titles={['First', 'Second', 'Third', 'Fourth', 'Fifth']} /> <Steps
count={5}
current={step}
titles={['First', 'Second', 'Third', 'Fourth', 'Fifth']}
/>
<StepsContent> <StepsContent>
<Heading level={3}>{`Step ${step}`}</Heading> <Heading level={3}>{`Step ${step}`}</Heading>
<p> <p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta natus Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta natus
consequuntur saepe harum nesciunt eum, a nulla facilis architecto incidunt consequuntur saepe harum nesciunt eum, a nulla facilis architecto
odio voluptas praesentium, ipsa laboriosam animi! Officiis atque odio nulla. incidunt odio voluptas praesentium, ipsa laboriosam animi! Officiis
atque odio nulla.
</p> </p>
<div> <div>
<Button <Button
@ -72,11 +74,7 @@ export function StepsWithTitles() {
> >
Previous step Previous step
</Button> </Button>
<Button <Button onClick={nextStep} dimension="small" isDisabled={step === 5}>
onClick={nextStep}
dimension="small"
isDisabled={step === 5}
>
Next step Next step
</Button> </Button>
</div> </div>

View File

@ -3,7 +3,9 @@ export function ContentWrapperFix() {
<> <>
<style <style
/* eslint-disable-next-line react/no-danger */ /* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: '#wpbody-content { padding-top: 73px; }' }} dangerouslySetInnerHTML={{
__html: '#wpbody-content { padding-top: 73px; }',
}}
/> />
<style <style

View File

@ -20,7 +20,14 @@ function Steps({ count, current, titles }: Props) {
})} })}
> >
<div className="mailpoet-step-badge">{i >= current ? i : ''}</div> <div className="mailpoet-step-badge">{i >= current ? i : ''}</div>
{titles[i - 1] && <div className="mailpoet-step-title" data-title={titles[i - 1] || ''}>{titles[i - 1] || ''}</div>} {titles[i - 1] && (
<div
className="mailpoet-step-title"
data-title={titles[i - 1] || ''}
>
{titles[i - 1] || ''}
</div>
)}
</div> </div>
))} ))}
<ContentWrapperFix /> <ContentWrapperFix />

View File

@ -8,11 +8,14 @@ type Props = {
cacheCalculation: string; cacheCalculation: string;
}; };
export function SubscribersCacheMessage({ cacheCalculation }: Props): JSX.Element { export function SubscribersCacheMessage({
cacheCalculation,
}: Props): JSX.Element {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState([]); const [errors, setErrors] = useState([]);
const datetimeDiff = new Date().getTime() - new Date(cacheCalculation).getTime(); const datetimeDiff =
const minutes = Math.floor((datetimeDiff / 1000) / 60); new Date().getTime() - new Date(cacheCalculation).getTime();
const minutes = Math.floor(datetimeDiff / 1000 / 60);
const handleRecalculate = () => { const handleRecalculate = () => {
setLoading(true); setLoading(true);
@ -20,9 +23,11 @@ export function SubscribersCacheMessage({ cacheCalculation }: Props): JSX.Elemen
api_version: MailPoet.apiVersion, api_version: MailPoet.apiVersion,
endpoint: 'settings', endpoint: 'settings',
action: 'recalculateSubscribersCountsCache', action: 'recalculateSubscribersCountsCache',
}).done(() => { })
.done(() => {
window.location.reload(); window.location.reload();
}).fail((response:ErrorResponse) => { })
.fail((response: ErrorResponse) => {
setErrors(response.errors.map((error) => error.message)); setErrors(response.errors.map((error) => error.message));
setLoading(false); setLoading(false);
}); });
@ -30,7 +35,6 @@ export function SubscribersCacheMessage({ cacheCalculation }: Props): JSX.Elemen
return ( return (
<div className="mailpoet-subscribers-cache-notice"> <div className="mailpoet-subscribers-cache-notice">
{ReactStringReplace( {ReactStringReplace(
MailPoet.I18n.t('subscribersCountWereCalculatedWithMinutesAgo'), MailPoet.I18n.t('subscribersCountWereCalculatedWithMinutesAgo'),
/<abbr>(.*?)<\/abbr>/, /<abbr>(.*?)<\/abbr>/,
@ -52,7 +56,13 @@ export function SubscribersCacheMessage({ cacheCalculation }: Props): JSX.Elemen
{MailPoet.I18n.t('recalculateNow')} {MailPoet.I18n.t('recalculateNow')}
</Button> </Button>
<div className="mailpoet-gap" /> <div className="mailpoet-gap" />
{errors.length > 0 && <Notice type="error">{errors.map((error) => <p key={error}>{error}</p>)}</Notice>} {errors.length > 0 && (
<Notice type="error">
{errors.map((error) => (
<p key={error}>{error}</p>
))}
</Notice>
)}
</div> </div>
); );
} }

View File

@ -27,13 +27,16 @@ function SubscribersInPlan({
return ( return (
<div className="mailpoet-subscribers-in-plan"> <div className="mailpoet-subscribers-in-plan">
{ReactStringReplace(MailPoet.I18n.t('subscribersInPlan'), '%s', () => subscribersInPlanCount)} {ReactStringReplace(
{' '} MailPoet.I18n.t('subscribersInPlan'),
'%s',
() => subscribersInPlanCount,
)}{' '}
<HelpTooltip <HelpTooltip
tooltip={MailPoet.I18n.t('subscribersInPlanTooltip')} tooltip={MailPoet.I18n.t('subscribersInPlanTooltip')}
place="right" place="right"
/> />
<span className="mailpoet-subscribers-in-plan-spacer">{' '}</span> <span className="mailpoet-subscribers-in-plan-spacer"> </span>
</div> </div>
); );
} }

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