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:
path: /home/circleci/mailpoet
- 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
- restore_cache:
key: tools-{{ checksum "tools/install.php" }}
@ -133,7 +133,7 @@ jobs:
- npm-{{ checksum "package-lock.json" }}
- npm- # fallback to most recent npm-* if not found by checksum
- run:
name: "Set up test environment"
name: 'Set up test environment'
command: |
# install plugin dependencies
COMPOSER_DEV_MODE=1 php tools/install.php
@ -197,7 +197,7 @@ jobs:
at: /home/circleci/mailpoet
- add_ssh_keys
- run:
name: "Install Premium plugin"
name: 'Install Premium plugin'
command: |
# 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
@ -207,7 +207,7 @@ jobs:
- restore_cache:
key: premium-composer-{{ checksum "mailpoet-premium/composer.json" }}-{{ checksum "mailpoet-premium/composer.lock" }}
- run:
name: "Set up test environment"
name: 'Set up test environment'
command: |
# install Premium dependencies
MAILPOET_FREE_PATH=$(pwd)/mailpoet
@ -241,7 +241,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Static analysis"
name: 'Static analysis'
command: ./do qa:phpstan --php-version=<< parameters.php_version >>
qa_js:
executor: wpcli_php_latest
@ -250,7 +250,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "QA Frontend Assets"
name: 'QA Frontend Assets'
command: ./do qa:frontend-assets
qa_php:
executor: wpcli_php_latest
@ -259,7 +259,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "QA PHP"
name: 'QA PHP'
command: ./do qa:php
qa_php_oldest:
executor: wpcli_php_oldest
@ -268,7 +268,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "QA PHP"
name: 'QA PHP'
command: ./do qa:php
qa_php_max_wporg:
executor: wpcli_php_max_wporg
@ -276,7 +276,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "QA PHP"
name: 'QA PHP'
command: ./do qa:php-max-wporg
js_tests:
executor: wpcli_php_latest
@ -285,15 +285,15 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Preparing test results folder"
name: 'Preparing test results folder'
command: mkdir test-results
- run:
name: "JS Newsletter Editor Tests"
name: 'JS Newsletter Editor Tests'
command: |
mkdir test-results/mocha
./do t:newsletter-editor test-results/mocha/newsletter_editor_junit.xml
- run:
name: "JS Tests"
name: 'JS Tests'
command: |
./do t:j test-results/mocha/junit.xml
- store_test_results:
@ -343,10 +343,10 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up virtual host"
name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run:
name: "Pull acceptance test docker images"
name: 'Pull acceptance test docker images'
# 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
- when:
@ -417,16 +417,16 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up virtual host"
name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- 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
- run:
name: "Set up test environment"
name: 'Set up test environment'
command: source ../.circleci/setup.bash && setup php7
- run:
name: "PHP Unit tests"
name: 'PHP Unit tests'
command: |
./do t:u --xml
- store_test_results:
@ -453,16 +453,16 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up virtual host"
name: 'Set up virtual host'
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- 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
- run:
name: "Set up test environment"
name: 'Set up test environment'
command: << parameters.setup_command >>
- run:
name: "PHP Integration tests"
name: 'PHP Integration tests'
command: << parameters.run_command >>
- store_test_results:
path: tests/_output
@ -479,7 +479,7 @@ jobs:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up environment"
name: 'Set up environment'
command: |
source ../.circleci/setup.bash && setup php7
sudo apt-get update
@ -487,7 +487,7 @@ jobs:
sed -i 's/^WP_ROOT=.*$/WP_ROOT=\/home\/circleci\/mailpoet\/wordpress/g' .env
echo ${CIRCLE_BUILD_NUM} > release_zip_build_number.txt
- run:
name: "Build"
name: 'Build'
command: ./build.sh
- store_artifacts:
path: /home/circleci/mailpoet/mailpoet/mailpoet.zip
@ -580,7 +580,7 @@ workflows:
nightly:
triggers:
- schedule:
cron: "0 22 * * 1-5"
cron: '0 22 * * 1-5'
filters:
branches:
only:

View File

@ -4,7 +4,6 @@ about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
1. Go to ...
2. Click on ...
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.
**Versions (please complete the following information):**
- WordPress version: [e.g: 5.3.2]
- PHP version: [e.g: 7.4.2]
- MailPoet version: [e.g: 3.46.13]

View File

@ -4,7 +4,6 @@ about: https://feedback.mailpoet.com/
title: ''
labels: ''
assignees: ''
---
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
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
@ -14,9 +14,9 @@ Generally, *only the latest version of MailPoet has continued support*. If a cri
Our most critical targets are:
* MailPoet plugin (this repository)
* MailPoet Premium
* mailpoet.com -- the primary site, and all of it subdomains, e.g. [account.mailpoet.com](https://account.mailpoet.com/)
- MailPoet plugin (this repository)
- MailPoet Premium
- 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).
@ -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:
* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
* Pen-testing Production:
* 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.
* **_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.
* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability.
- Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines).
- Pen-testing Production:
- 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.
- **_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.
- 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.

View File

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

View File

@ -1,6 +1,7 @@
# Contributing
## PHP Code
- Two spaces indentation.
- Space between keyword (if, for, switch...) and left bracket
- CamelCase for classes.
@ -15,20 +16,24 @@
- Cover your code in tests.
## SCSS Code
- camelCase for file name
- Components files are prefixed with underscore, to indicate, that they aren't compiled separately.
## JS Code
- Javascript code should follow the [Airbnb style guide](https://github.com/airbnb/javascript).
- Prefer named export before default export in JS and TS files
## Disabling linting rules
- 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:
`/* 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
## Git flow
- Do not commit to master.
- Open a short-living feature branch.
- Open a pull request.
@ -39,12 +44,13 @@
- Wait for review from another developer.
## Issues creation
- Issues are managed on Jira.
- Discuss issues on public Slack chats, discuss code in pull requests.
- Open a small Jira issue only when it has been discussed.
## 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.
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
The **MailPoet** plugin monorepo.
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).
## 🔌 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
In `Languages & Preferences > PHP > Servers` set path mappings:
```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).
## 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.
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 volumes can bring more stability and performance on Docker for Mac. To setup NFS volume sharing run:
```shell
sudo sh dev/mac-nfs-setup.sh
```
Then create a Docker Compose override file with NFS settings and restart containers:
```shell
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/).
# 🐶 Husky
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
need to create or update your `~/.huskyrc` file with:
```sh
# This loads nvm.sh and sets the correct PATH before running the hooks:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
```
Without it, you may experience errors in some Git clients.
## 🕹 Commands
The `./do` script define aliases for most of the commands you will need while working on plugins:
```shell
@ -88,11 +98,14 @@ Options:
You can access this help in your command line running `./do` without parameters.
## ✉️ 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
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
wordpress:
@ -100,11 +113,13 @@ To switch the environment to PHP 7.4/8.0:
context: .
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,
and start the stack using `./do start`.
## ✅ TODO
- 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)
- [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.
Questions posed to the issue tracker will be closed.

View File

@ -1,13 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<title>MailPoet dev environment</title>
<link rel="icon" href="favicon.png" />
<style>
body {
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 {
color: #071c6d;
@ -38,9 +40,9 @@
display: block;
}
</style>
</head>
</head>
<body>
<body>
<a href="/"><img src="logo.svg" /></a>
<p>Dev environment</p>
<table>
@ -66,11 +68,17 @@
<tr>
<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>DB management</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>
</tr>
<tr>
@ -81,5 +89,5 @@
</tr>
</tbody>
</table>
</body>
</body>
</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/).
## MailPoet API
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.
### Basics
MailPoet API is distributed within MailPoet3 plugin and it is implemented as a PHP class.
Currently supported version is `v1`.
### Instantiation
```php
if (class_exists(\MailPoet\API\API::class)) {
$mailpoet_api = \MailPoet\API\API::MP('v1');
}
```
Class `\MailPoet\API\API` becomes available once MailPoet plugin is loaded by WordPress.
### Available API Methods
* [Add List (addList)](api_methods/AddList.md)
* [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md)
* [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
* [Get Lists (getLists)](api_methods/GetLists.md)
* [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
* [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md)
* [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md)
* [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md)
* [Subscribe to List (subscribeToLists)](api_methods/SubscribeToLists.md)
* [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
* [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)
- [Add List (addList)](api_methods/AddList.md)
- [Add Subscriber (addSubscriber)](api_methods/AddSubscriber.md)
- [Add Subscriber Field (addSubscriberField)](api_methods/AddSubscriberField.md)
- [Get Lists (getLists)](api_methods/GetLists.md)
- [Get Subscriber (getSubscriber)](api_methods/GetSubscriber.md)
- [Get Subscriber Fields (getSubscriberFields)](api_methods/GetSubscriberFields.md)
- [Is Setup Complete (isSetupComplete)](api_methods/IsSetupComplete.md)
- [Subscribe to List (subscribeToList)](api_methods/SubscribeToList.md)
- [Subscribe to List (subscribeToLists)](api_methods/SubscribeToLists.md)
- [Unsubscribe from List (unsubscribeFromList)](api_methods/UnsubscribeFromList.md)
- [Unsubscribe from Lists (unsubscribeFromLists)](api_methods/UnsubscribeFromLists.md)
### Usage examples
You can check some basic examples [here](UsageExamples.md).

View File

@ -1,9 +1,11 @@
[back to readme](Readme.md)
# Usage Examples
Common usage is a rendering of a subscription form and processing it.
## Fetching data for a subscription form
```php
<?php
@ -18,6 +20,7 @@ if (class_exists(\MailPoet\API\API::class)) {
```
## Processing a subscription form
```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.
## Arguments
### `$list` (required)
An associative array which contains list data.
| Property | Type | Limits | Description |
| --- | --- | --- | --- |
| ---------------------- | ------------ | --------- | -------------------------- |
| 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
@ -28,7 +29,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description:
| Code | Description |
| --- | --- |
| ---- | -------------------------------------------- |
| 14 | Missing list name |
| 15 | Trying to create a list that already exists |
| 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`.
- *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.
- *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.
- _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.
- _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`.
@ -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.
## Arguments
### `$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.
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 |
| --- | --- | --- | --- |
| -------------------------- | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| email (required) | string | 150 chars | a valid email address |
| first_name (optional) | string/null | 255 chars | Fist 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)
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.
### `$options` (optional)
All options are optional. If omitted a default value is used.
| 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`. |
| 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.|
| 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. |
## Error handling
@ -56,7 +59,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description:
| Code | Description |
| --- | --- |
| ---- | -------------------------------------------------- |
| 11 | Missing email address |
| 12 | Trying to create a subscriber that already exists |
| 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)
| 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. |
| 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. |
@ -23,40 +23,40 @@ Params array differs for each type.
The common properties for all types:
| Property | Type | Description |
| --- | --- | --- |
| -------- | ------ | ------------------------------------------------------------------------------------------- |
| 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. |
#### `$params` for text, textarea types
| Property | Type | Description |
| --- | --- | --- |
| -------- | ------ | ------------------------------------------------------------------------------------------- |
| validate | string | Can be used for validating input values. Possible values are: `number`, `alphanum`, `phone` |
#### `$params` for checkbox types
| Property | Type | Description |
| --- | --- | --- |
| -------- | ----- | ------------------------------------------------------------ |
| values | array | Same array as for radio type. Must contain exactly 1 element |
#### `$params` for radio, select types
| Property | Type | Description |
| --- | --- | --- |
| -------- | ----- | --------------------------------------------------------------------------------------------------- |
| values | array | Contains a list of options. Each element must contain a string `value` and can contain `is_checked` |
#### `$params` for date type
| Property | Type | Description |
| --- | --- | --- |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 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` |
## Response
| 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. |
| type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` |
| 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:
| Code | Description |
| --- | --- |
| ---- | ---------------------------------------------------------------------------------- |
| 1 | The subscriber couldnt be created in the database |
| 1001 | Missing a mandatory field in the `$data` argument |
| 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
| Property | Type | Limits | Description |
| --- | --- | --- | --- |
| ----------- | ------------ | --------- | ----------------------------------------------------------------------------------------------------------- |
| id | string | 11 chars | Id 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` |
@ -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. |
### Response Example
```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.
## Arguments
| Argument | Type | Description |
| --- | --- | --- |
| ----------------- | ------ | --------------------- |
| $subscriber_email | string | a valid email address |
## A subscriber data structure
### Subscriber
| Property | Type | Limits | Description |
| --- | --- | --- | --- |
| ------------------------ | ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| id | string | 11 chars | Id of 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` |
@ -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 |
| 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 |
| 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 |
| 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
| Property | Type | Limits | Description |
| --- | --- | --- | --- |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------ |
| id | string | 11 chars | Id of relation |
| subscriber_id | string | 11 chars | Id of subscriber |
| 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 |
### Response Example
```php
<?php
[
@ -98,5 +102,5 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description:
| Code | Description |
| --- | --- |
| ---- | -------------------------------------------- |
| 4 | Asking for a subscriber that does not exist. |

View File

@ -12,13 +12,14 @@ See also [addSubscriberField function.](AddSubscriberField.md)
## Subscriber Field
| 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. |
| type | string | - | Type of the field. Possible values are: `text`, `date`, `textarea`, `radio`, `checkbox`, `select` |
| params | array | - | Contains various information, see examples below. |
## Response Example
```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.
- *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.
- *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.
- _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.
- _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`.
It returns a subscriber. See [Get Subscriber](GetSubscriber.md) for a subscriber data structure.
## Arguments
### string `$subscriber_id` (required)
An id or an email address. An `\Exception` is thrown when the value doesn't match any subscriber.
### 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.
### array `$options` (optional)
All options are optional. If omitted, a default value is used.
| 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.|
| 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.|
| ---------------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| 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. |
| 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
@ -40,7 +44,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description:
| Code | Description |
| --- | --- |
| ---- | ------------------------------------------------------- |
| 3 | No lists provided |
| 4 | Invalid subscriber 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.
## Arguments
### 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.
### 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.
## Error handling
@ -25,7 +28,7 @@ An exception of base class `\Exception` can be thrown when something unexpected
Codes description:
| Code | Description |
| --- | --- |
| ---- | --------------------------------------------------- |
| 3 | No lists provided |
| 4 | Invalid subscriber that does not exist |
| 5 | Invalid list that does not exist |

View File

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

View File

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

View File

@ -22,11 +22,7 @@
"jsx": true
}
},
"plugins": [
"react-hooks",
"no-only-tests",
"@typescript-eslint"
],
"plugins": ["react-hooks", "no-only-tests", "@typescript-eslint"],
"settings": {
"import/resolver": "webpack"
},
@ -49,19 +45,28 @@
"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
"prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, {
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those
}],
"jsx-a11y/label-has-for": [
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/label-has-associated-control": [ 2, {
"jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor
}]
}
]
},
"overrides": [
{
"files": ["**/_stories/*.tsx"],
"rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
"import/no-extraneous-dependencies": [
"error",
{ "devDependencies": true }
]
}
}
]

View File

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

View File

@ -1,9 +1,5 @@
{
"extends": [
"airbnb",
"plugin:react/jsx-runtime",
"prettier"
],
"extends": ["airbnb", "plugin:react/jsx-runtime", "prettier"],
"env": {
"amd": true,
"browser": true,
@ -16,10 +12,7 @@
"jsx": true
}
},
"plugins": [
"react-hooks",
"no-only-tests"
],
"plugins": ["react-hooks", "no-only-tests"],
"settings": {
"import/resolver": "webpack"
},
@ -36,13 +29,19 @@
"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
"prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, {
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those
}],
"jsx-a11y/label-has-for": [
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/label-has-associated-control": [ 2, {
"jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor
}]
}
]
},
"overrides": [
{
@ -53,4 +52,3 @@
}
]
}

View File

@ -22,11 +22,7 @@
"jsx": true
}
},
"plugins": [
"react-hooks",
"no-only-tests",
"@typescript-eslint"
],
"plugins": ["react-hooks", "no-only-tests", "@typescript-eslint"],
"settings": {
"import/resolver": "webpack",
"import/core-modules": [
@ -45,10 +41,7 @@
]
},
"rules": {
"react/no-unstable-nested-components": [
"error",
{ "allowAsProps": true }
],
"react/no-unstable-nested-components": ["error", { "allowAsProps": true }],
// PropTypes
"react/prop-types": 0,
"react/jsx-props-no-spreading": 0,
@ -59,7 +52,7 @@
// Exceptions
"@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
"react/jsx-no-useless-fragment" : [
"react/jsx-no-useless-fragment": [
"error",
{
"allowExpressions": true
@ -76,13 +69,19 @@
"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
"prefer-destructuring": 0, // that would be too many changes to fix this one
"jsx-a11y/label-has-for": [2, {
"required": {"some": ["nesting", "id"]} // some of our labels are hidden and we cannot nest those
}],
"jsx-a11y/label-has-for": [
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/label-has-associated-control": [ 2, {
"jsx-a11y/label-has-associated-control": [
2,
{
"either": "either" // control has to be either nested or associated via htmlFor
}]
}
]
},
"overrides": [
{
@ -96,7 +95,10 @@
{
"files": ["**/_stories/*.tsx"],
"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 modulesDir = path.join( __dirname, '../node_modules' );
const modulesDir = path.join(__dirname, '../node_modules');
console.log('NODE', modulesDir);
// Workaround for Emotion 11
// 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';
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": [
"stylelint-order",
"stylelint-scss"
'plugins': ['stylelint-order', 'stylelint-scss'],
'customSyntax': 'postcss-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",
"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"]
}],
"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,
'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
The **MailPoet** plugin.
To use the official Docker-based development environment, see details
@ -16,11 +17,13 @@ below.
## Setup
### Requirements
- PHP >= 7.3 (only for the development environment, to run the plugin PHP >= 7.2 is required)
- NodeJS
- WordPress
### Installation
The instructions below assume you already have a working WordPress development environment:
```bash
@ -171,8 +174,7 @@ _x('text to translate', 'context for translators', 'mailpoet');
**in Twig views**
```html
<%= __('text to translate') %>
<%= _n('single text', 'plural text', $number) %>
<%= __('text to translate') %> <%= _n('single text', 'plural text', $number) %>
<%= _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:
```html
<% block translations %>
<%= localize({
'key': __('string to translate'),
...
}) %>
<% endblock %>
<% block translations %> <%= localize({ 'key': __('string to translate'), ... })
%> <% endblock %>
```
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`.
If you want to run only a single test use the parameter `--file`:
```bash
./do test:acceptance --skip-deps --file tests/acceptance/ReceiveStandardEmailCest.php
```
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.

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,9 +17,21 @@ export function CategoriesWithCount() {
return (
<>
<Categories onSelect={noop} categories={categories} active={categories[0].name} />
<Categories onSelect={noop} categories={categories} active={categories[2].name} />
<Categories onSelect={noop} categories={categories} active={categories[4].name} />
<Categories
onSelect={noop}
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 (
<>
<Categories onSelect={noop} categories={categories} active={categories[0].name} />
<Categories onSelect={noop} categories={categories} active={categories[2].name} />
<Categories onSelect={noop} categories={categories} active={categories[4].name} />
<Categories
onSelect={noop}
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 (
<div className="mailpoet-categories">
{ cats }
</div>
);
return <div className="mailpoet-categories">{cats}</div>;
}
export default Categories;

View File

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

View File

@ -8,13 +8,21 @@ function ConfirmAlert(props) {
template: ReactDOMServer.renderToString(
<>
<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}
</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}
</button>
</>
</>,
),
onInit: () => {
document
@ -55,6 +63,6 @@ export default function confirmAlert(props) {
cancelLabel={props.cancelLabel}
confirmLabel={props.confirmLabel}
onConfirm={props.onConfirm}
/>
/>,
);
}

View File

@ -11,7 +11,9 @@ export default async function callApi({ endpoint, action, data }) {
});
return { success: true, 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 };
}
}

View File

@ -1,14 +1,19 @@
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,
data,
timeout = 0,
}:{ name:string, data:object, timeout:number },
) {
}: {
name: string;
data: object;
timeout: number;
}) {
MailPoet.trackEvent(name, data);
return sleep(timeout);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,8 @@ export function ReactSelect() {
},
{
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',
count: 1234,
},

View File

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

View File

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

View File

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

View File

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

View File

@ -21,24 +21,24 @@ function Textarea({
}: Props) {
return (
<div
className={
classnames(
className,
'mailpoet-form-textarea',
{
className={classnames(className, 'mailpoet-form-textarea', {
[`mailpoet-form-textarea-${dimension}`]: dimension,
'mailpoet-disabled': attributes.disabled,
'mailpoet-full-width': isFullWidth,
},
)
}
})}
>
<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 && (
<>
<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>
<Tooltip place="right" multiline id={attributes.name}>
{tooltip}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
export default (
<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>
);

View File

@ -1,5 +1,8 @@
export default (
<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>
);

View File

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

View File

@ -1,7 +1,9 @@
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());
});
},
);
export default setLowercaseValue;

View File

@ -8,27 +8,22 @@ export default {
};
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 (
<>
<Heading level={3}>Column</Heading>
<Grid.Column className="custom-class">
{content}
</Grid.Column>
<Grid.Column className="custom-class">{content}</Grid.Column>
<div className="mailpoet-gap" />
<Grid.Column align="center">
{content}
</Grid.Column>
<Grid.Column align="center">{content}</Grid.Column>
<div className="mailpoet-gap" />
<Heading level={3}>Column - small</Heading>
<Grid.Column dimension="small">
{content}
</Grid.Column>
<Grid.Column dimension="small">{content}</Grid.Column>
<div className="mailpoet-gap" />
@ -77,9 +72,7 @@ export function Layouts(): ReactElement {
<br />
Part
</div>
<div>
Right Part
</div>
<div>Right Part</div>
</Grid.SpaceBetween>
</Grid.Column>
@ -93,22 +86,16 @@ export function Layouts(): ReactElement {
<br />
Part
</div>
<div>
Right Part
</div>
<div>Right Part</div>
</Grid.SpaceBetween>
</Grid.Column>
<Heading level={3}>Centered row</Heading>
<Grid.Column>
<Grid.CenteredRow className="custom-class">
<div>
Left
</div>
<div>Left</div>
<Input type="text" />
<div>
Right
</div>
<div>Right</div>
</Grid.CenteredRow>
</Grid.Column>
</>

View File

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

View File

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

View File

@ -7,9 +7,16 @@ type Props = {
automationId?: string;
};
export function ThreeColumns({ children, className, automationId }: Props): ReactElement {
export function ThreeColumns({
children,
className,
automationId,
}: Props): ReactElement {
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}
</div>
);

View File

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

View File

@ -14,7 +14,10 @@ MailPoet.I18n.add('openedStatTooltipAverage', 'under 10%');
MailPoet.I18n.add('clickedStatTooltipExcellent', 'above 3%');
MailPoet.I18n.add('clickedStatTooltipGood', 'between 1 and 3%');
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 {
title: 'Listing',
@ -36,11 +39,26 @@ export function NewsletterStatsComponent() {
<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" />
<NewsletterStats opened={11} clicked={1.1} revenues="100€" newsletterId={5} />
<NewsletterStats
opened={11}
clicked={1.1}
revenues="100€"
newsletterId={5}
/>
<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" />
@ -56,11 +74,29 @@ export function NewsletterStatsComponent() {
<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" />
<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" />
<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={400} processed={150} />
<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" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,21 +6,50 @@ import Heading from '../../typography/heading/heading';
const shortContent = (
<>
<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>
</>
);
const longContent = (
<>
<p>{'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20)}</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>
{'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20)}
</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>{'Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. '.repeat(20)}</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>
<p>
{'Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. '.repeat(
20,
)}
</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}
</Modal>
) }
)}
</p>
);
}
@ -65,25 +94,71 @@ export function Modals() {
<>
<Heading level={3}>Modal with short text</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" />
<ModalWrapper buttonCaption="Show modal with title, without close button" 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} />
<ModalWrapper
buttonCaption="Show modal with title, without close button"
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" />
<Heading level={3}>Modal with long text</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" 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} />
<ModalWrapper
buttonCaption="Show modal with title, with close button"
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" />
<Heading level={3}>Full-screen modal</Heading>
<ModalWrapper buttonCaption="Show modal with title, with close button" content={longContent} 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 />
<ModalWrapper
buttonCaption="Show modal with title, with close button"
content={longContent}
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;
};
function ModalFrame({
fullScreen = false,
className = '',
children,
}: Props) {
function ModalFrame({ fullScreen = false, className = '', children }: Props) {
return (
<div
className={classnames(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,19 @@
export default function DesktopIcon() {
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 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>
<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() {
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 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(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>

View File

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

View File

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

View File

@ -23,9 +23,10 @@ function SenderEmailAddressWarning({ emailAddress, mssActive }) {
/(%1\$s|%2\$s|<em>.*<\/em>)/,
(match) => {
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>;
}
},
)}
</p>
<p className="sender_email_address_warning">

View File

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

View File

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

View File

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

View File

@ -20,7 +20,14 @@ function Steps({ count, current, titles }: Props) {
})}
>
<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>
))}
<ContentWrapperFix />

View File

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

View File

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

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