Compare commits
296 Commits
Author | SHA1 | Date | |
---|---|---|---|
0180a1245b | |||
593071efff | |||
3c46c87da9 | |||
9d1639c100 | |||
8ff4f7720b | |||
13d35c4385 | |||
c778b84932 | |||
1ee06d7196 | |||
5134c60291 | |||
57486a522a | |||
8ae34e83ec | |||
19d13c5165 | |||
f64b42d5cd | |||
78539d0e6f | |||
2e6dd9b20f | |||
d1b91cd661 | |||
fb3fdbf1ed | |||
a0e8e52183 | |||
9364a422b3 | |||
5e16bc4184 | |||
75295e55c2 | |||
13329d568d | |||
b9993da62c | |||
5605c0c9dd | |||
c60a7a233c | |||
6c8705f3c2 | |||
99a889fab6 | |||
a1457678b0 | |||
b6e0b7ceb0 | |||
8d3e751845 | |||
410aa76d37 | |||
c9b350adab | |||
9f46300d7d | |||
55e7c97af0 | |||
c4536f3591 | |||
e4d4b575bd | |||
2771c8c33e | |||
79dbaa119d | |||
05d054dfad | |||
4bbde3435d | |||
74d711fa73 | |||
464c27849b | |||
2f202f6b3d | |||
186b4c9a36 | |||
4b94392362 | |||
cca2a1923d | |||
76fe4dba59 | |||
fef557bb94 | |||
5c5069643e | |||
6e391559fe | |||
62e47dcf12 | |||
85e1467936 | |||
8d816337f7 | |||
8209facb5c | |||
19d5c44588 | |||
afc04e44af | |||
80d6cd0187 | |||
b1f9a8a84f | |||
5635014285 | |||
91e66476d3 | |||
33b4ed324e | |||
a90672652e | |||
cc49c36466 | |||
0a82c82387 | |||
d987341094 | |||
25f70df2ad | |||
42859d94a3 | |||
4abaebf96d | |||
cada06360b | |||
fe6519d503 | |||
5a4cebf0d7 | |||
cb8d532d64 | |||
7f31f0888e | |||
b54d31c797 | |||
f3a597ef90 | |||
d1fbe3dae1 | |||
f30e25964c | |||
908f2f9f60 | |||
4a781849db | |||
c4094efd91 | |||
c13974ad82 | |||
787cccb33d | |||
7e13bbcc57 | |||
741cfb1775 | |||
797491ddec | |||
cfcba021b1 | |||
35f59cdcc0 | |||
9f60cf1554 | |||
21f03bbcb7 | |||
ee37f39980 | |||
883023e581 | |||
ca3e309104 | |||
d46f9203ee | |||
7c19aecaa0 | |||
050417ec83 | |||
5b077b3911 | |||
70743b98b6 | |||
103714a035 | |||
0ae162b334 | |||
4f97bb45e1 | |||
d4fa041ba8 | |||
eb1acc8145 | |||
6479a6cc10 | |||
9eb586b8e6 | |||
f39c0c58c2 | |||
0c155ffe7c | |||
ad77fa547a | |||
7747c30028 | |||
ea9be3e078 | |||
fff8176a49 | |||
71ad9f50cb | |||
8eff3dc3c6 | |||
49ff4880f8 | |||
4d93aff2cb | |||
7b859660b7 | |||
3e22223d0c | |||
384c9fb17b | |||
79acc0e4df | |||
52a29ff449 | |||
c4db1df528 | |||
1fe7bedf20 | |||
f5be4e47e8 | |||
2ae39c9255 | |||
6028027d47 | |||
283452f47e | |||
7be91476f0 | |||
b528587b1f | |||
056b971f7b | |||
fcfd6f1f09 | |||
90186f2af5 | |||
c9ef3cfb95 | |||
b6f40a9b52 | |||
1936524cec | |||
8b2c5116f4 | |||
9734759ba5 | |||
c76b4c3c99 | |||
9092aa3029 | |||
87e515b89d | |||
05e5ca3f43 | |||
5fed328826 | |||
7538a08678 | |||
82d6a4b096 | |||
b21ef30202 | |||
653f31e997 | |||
149c794905 | |||
cbf48327d9 | |||
c83ed66160 | |||
042fe48669 | |||
9aba09d42e | |||
25a8519583 | |||
9fc442ae8e | |||
1e7542848a | |||
78821a69ab | |||
4ae459838a | |||
aa83883cfa | |||
6d9f0767af | |||
c03feeae3f | |||
8effcf2c59 | |||
f81c5617ed | |||
e6c4c02484 | |||
ff332e78e9 | |||
f211ffce3b | |||
ba127990e3 | |||
03ca022596 | |||
ee00464cc5 | |||
aaa1c659b8 | |||
b9ffc0e280 | |||
a943589de9 | |||
c0e252f42a | |||
980574eb6d | |||
72ea5fb84e | |||
cc303c0cd3 | |||
d11c9c5296 | |||
fd893cec0e | |||
debe9ea1ba | |||
3029e6102f | |||
9c8c89149b | |||
edb3f25838 | |||
2f730c280c | |||
1aa0a86d66 | |||
6b6f488f1e | |||
c1e8815134 | |||
64e5742700 | |||
ce6d45e557 | |||
91a1ca69ed | |||
549e2e7e86 | |||
ce6711d69e | |||
0639b1ad1f | |||
ad37c6b9c5 | |||
605f8f1ce1 | |||
1ffd5741be | |||
a1d606e533 | |||
a6ab757d22 | |||
654dd1e8d0 | |||
40c19cd5d8 | |||
3cccce3d86 | |||
57203a6917 | |||
d01805a911 | |||
4774ab09f3 | |||
5dad1fa545 | |||
c3481dd4b7 | |||
b5338979b1 | |||
436406f800 | |||
112f7b21d5 | |||
b45c47a9e7 | |||
cb3952c9fb | |||
b597cb39f1 | |||
3102f114bd | |||
bdf244536d | |||
ca3b6412ee | |||
469dc15e4d | |||
253be30153 | |||
d7fb5450f4 | |||
280bc06c03 | |||
c1a505c425 | |||
614d9f7e56 | |||
7c9b2be760 | |||
0f2bc75248 | |||
f0df936dbc | |||
4b675a41d6 | |||
0e7cc668e2 | |||
70debcc828 | |||
4249c7a2cb | |||
fbe2f72706 | |||
6b9c252649 | |||
be75e0d3ee | |||
8e12bbf97f | |||
1936a3bcdd | |||
7eb37119ed | |||
aadca62a32 | |||
11401e2dab | |||
4cae2a7d2d | |||
9866f4c707 | |||
b632d74e5f | |||
c3a97b7139 | |||
a772aceb39 | |||
d59ace50f5 | |||
ff6f686333 | |||
2ef8512fa7 | |||
cabea5dadb | |||
b80505a17c | |||
e0b676fac4 | |||
6566f515f5 | |||
2836f72435 | |||
0b644e7c8f | |||
df4d3bacab | |||
6dc0acb63a | |||
a346492d46 | |||
b6513262b0 | |||
868e07ab86 | |||
9ec6f52098 | |||
222d6b4eac | |||
0683dc9817 | |||
45e6ddd9cb | |||
c556ec64fc | |||
5bc9573ac6 | |||
a3d7afd1ca | |||
8c44ef561c | |||
c51a3f208c | |||
5524805ec3 | |||
816ec7e1d7 | |||
d74283ecad | |||
e810840445 | |||
b2e2087cfc | |||
69d920376f | |||
22efacd2d7 | |||
25c72c6ce7 | |||
e94d85fd47 | |||
c6b379c840 | |||
71bf3bbe19 | |||
6e60056061 | |||
96edbc159f | |||
c3c0765b78 | |||
b614997150 | |||
f81323ad52 | |||
cfc0d79bed | |||
c6adfc6a5c | |||
53149c291c | |||
e8f0a0067d | |||
cc4ff09aea | |||
77dfd28479 | |||
cd283c11fe | |||
57baff7b1d | |||
fd72756622 | |||
9b3abea2e5 | |||
42c557a981 | |||
e4db455a47 | |||
5d1f3153cd | |||
fcd8509cef | |||
ad731fc5ed | |||
4257ff6313 | |||
f9a3875de1 | |||
b17d217669 | |||
556e4a0ce0 | |||
a72d84b940 | |||
049074e793 |
@ -3,7 +3,7 @@ jobs:
|
||||
build_and_code_qa:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:7.1_20180417.1
|
||||
- image: mailpoet/wordpress:7.1_20181009.1
|
||||
environment:
|
||||
TZ: /usr/share/zoneinfo/Etc/UTC
|
||||
steps:
|
||||
@ -42,7 +42,35 @@ jobs:
|
||||
root: /home/circleci/mailpoet
|
||||
paths:
|
||||
- .
|
||||
php5_and_js:
|
||||
php5_unit:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:5.6.30_20180417.1
|
||||
- image: circleci/mysql:5.7
|
||||
environment:
|
||||
TZ: /usr/share/zoneinfo/Etc/UTC
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
- run:
|
||||
name: "Set up virtual host"
|
||||
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
|
||||
- run:
|
||||
name: "Set up test environment"
|
||||
command: source ./.circleci/setup.bash && setup php5
|
||||
- run:
|
||||
name: "PHP Unit tests"
|
||||
command: |
|
||||
WP_ROOT="/home/circleci/mailpoet/wordpress" ./do t:u --xml
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
path: tests/_output
|
||||
destination: codeception
|
||||
- store_artifacts:
|
||||
path: /tmp/fake-mailer/
|
||||
destination: fake-mailer
|
||||
php5_integration_and_js:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:5.6.30_20180417.1
|
||||
@ -67,9 +95,9 @@ jobs:
|
||||
mkdir test-results/mocha
|
||||
./do t:j test-results/mocha/junit.xml
|
||||
- run:
|
||||
name: "PHP Unit tests"
|
||||
name: "PHP Integration tests"
|
||||
command: |
|
||||
WP_TEST_PATH="/home/circleci/mailpoet/wordpress" ./do t:u --xml
|
||||
WP_ROOT="/home/circleci/mailpoet/wordpress" ./do t:i --xml
|
||||
- store_test_results:
|
||||
path: test-results/mocha
|
||||
- store_artifacts:
|
||||
@ -131,10 +159,10 @@ jobs:
|
||||
path: tests/_output
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
php7:
|
||||
php7_unit:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:7.1_20180417.1
|
||||
- image: mailpoet/wordpress:7.1_20181009.1
|
||||
- image: circleci/mysql:5.7
|
||||
environment:
|
||||
TZ: /usr/share/zoneinfo/Etc/UTC
|
||||
@ -162,10 +190,41 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: /tmp/fake-mailer/
|
||||
destination: fake-mailer
|
||||
php7_multisite:
|
||||
php7_integration:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:7.1_20180417.1
|
||||
- image: mailpoet/wordpress:7.1_20181009.1
|
||||
- image: circleci/mysql:5.7
|
||||
environment:
|
||||
TZ: /usr/share/zoneinfo/Etc/UTC
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
- run:
|
||||
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"
|
||||
command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts
|
||||
- run:
|
||||
name: "Set up test environment"
|
||||
command: source ./.circleci/setup.bash && setup php7
|
||||
- run:
|
||||
name: "PHP Integration tests"
|
||||
command: |
|
||||
./do t:i --xml
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
path: tests/_output
|
||||
destination: codeception
|
||||
- store_artifacts:
|
||||
path: /tmp/fake-mailer/
|
||||
destination: fake-mailer
|
||||
php7_integration_multisite:
|
||||
working_directory: /home/circleci/mailpoet
|
||||
docker:
|
||||
- image: mailpoet/wordpress:7.1_20181009.1
|
||||
- image: circleci/mysql:5.7
|
||||
environment:
|
||||
TZ: /usr/share/zoneinfo/Etc/UTC
|
||||
@ -182,9 +241,9 @@ jobs:
|
||||
name: "Set up test environment"
|
||||
command: source ./.circleci/setup.bash && setup php7_multisite
|
||||
- run:
|
||||
name: "PHP Unit tests"
|
||||
name: "PHP Integration tests"
|
||||
command: |
|
||||
./do t:multisite-unit --xml
|
||||
./do t:multisite-integration --xml
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
@ -198,20 +257,24 @@ workflows:
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build_and_code_qa
|
||||
- php7:
|
||||
- php5_unit:
|
||||
requires:
|
||||
- build_and_code_qa
|
||||
- php5_and_js:
|
||||
- php7_unit:
|
||||
requires:
|
||||
- build_and_code_qa
|
||||
- php5_integration_and_js:
|
||||
requires:
|
||||
- php5_unit
|
||||
- php7_integration:
|
||||
requires:
|
||||
- php7_unit
|
||||
- php7_integration_multisite:
|
||||
requires:
|
||||
- php7_unit
|
||||
- acceptance_tests:
|
||||
requires:
|
||||
- build_and_code_qa
|
||||
- php7_multisite:
|
||||
requires:
|
||||
- build_and_code_qa
|
||||
- php7
|
||||
- php5_unit
|
||||
- acceptance_tests_multisite:
|
||||
requires:
|
||||
- build_and_code_qa
|
||||
- acceptance_tests
|
||||
- php5_unit
|
||||
|
@ -40,14 +40,14 @@ function setup {
|
||||
# Add a second blog
|
||||
wp site create --slug=php7_multisite $wp_cli_wordpress_path $wp_cli_allow_root
|
||||
echo "WP_TEST_MULTISITE_SLUG=php7_multisite" >> .env
|
||||
echo "WP_TEST_PATH_MULTISITE=/home/circleci/mailpoet/wordpress" >> .env
|
||||
echo "WP_ROOT_MULTISITE=/home/circleci/mailpoet/wordpress" >> .env
|
||||
echo "HTTP_HOST=mailpoet.loc" >> .env
|
||||
|
||||
# Add a third dummy blog
|
||||
wp site create --slug=dummy_multisite $wp_cli_wordpress_path $wp_cli_allow_root
|
||||
else
|
||||
wp core install --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc --title="WordPress Single" $wp_cli_wordpress_path $wp_cli_allow_root
|
||||
echo "WP_TEST_PATH=/home/circleci/mailpoet/wordpress" >> .env
|
||||
echo "WP_ROOT=/home/circleci/mailpoet/wordpress" >> .env
|
||||
fi
|
||||
|
||||
# Softlink plugin to plugin path
|
||||
|
18
.env.sample
18
.env.sample
@ -1,10 +1,17 @@
|
||||
WP_TEST_PATH="/var/www/wordpress"
|
||||
WP_TEST_PATH_MULTISITE="/var/www/wordpress"
|
||||
# Required
|
||||
WP_ROOT="/var/www/wordpress"
|
||||
WP_TEST_ENABLE_NETWORK_TESTS="false"
|
||||
WP_TEST_MAILER_ENABLE_SENDING="false"
|
||||
|
||||
# Optional: for multisite acceptance tests
|
||||
WP_ROOT_MULTISITE="/var/www/wordpress"
|
||||
WP_TEST_MULTISITE_SLUG=""
|
||||
WP_TEST_ENABLE_NETWORK_TESTS="true"
|
||||
HTTP_HOST="" // URL of your site (used for multisite env and equals to DOMAIN_CURRENT_SITE from wp-config.php)
|
||||
|
||||
# Optional: for sending tests
|
||||
# These are required if WP_TEST_MAILER_ENABLE_SENDING is "true"
|
||||
WP_TEST_IMPORT_MAILCHIMP_API=""
|
||||
WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma)
|
||||
WP_TEST_MAILER_ENABLE_SENDING="true"
|
||||
WP_TEST_MAILER_AMAZON_ACCESS=""
|
||||
WP_TEST_MAILER_AMAZON_SECRET=""
|
||||
WP_TEST_MAILER_AMAZON_REGION=""
|
||||
@ -13,7 +20,8 @@ WP_TEST_MAILER_SENDGRID_API=""
|
||||
WP_TEST_MAILER_SMTP_HOST=""
|
||||
WP_TEST_MAILER_SMTP_LOGIN=""
|
||||
WP_TEST_MAILER_SMTP_PASSWORD=""
|
||||
|
||||
# Optional: for plugin deployment
|
||||
WP_SVN_USERNAME=""
|
||||
WP_SVN_PASSWORD=""
|
||||
WP_TRANSIFEX_API_TOKEN=""
|
||||
HTTP_HOST="" // URL of your site (used for multisite env and equals to DOMAIN_CURRENT_SITE from wp-config.php)
|
||||
|
@ -4,6 +4,7 @@
|
||||
"amd": true,
|
||||
"browser": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"ecmaFeatures": {
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -23,4 +23,8 @@ lang
|
||||
.mp_svn
|
||||
/nbproject/
|
||||
tests/_data/acceptanceGenerated.sql
|
||||
lib/Dependencies
|
||||
lib/Dependencies
|
||||
lib/DI/CachedContainer.php
|
||||
mozart/Dependencies
|
||||
mozart/Classes
|
||||
mozart/vendor
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||
## Code.
|
||||
## PHP Code
|
||||
- Two spaces indentation.
|
||||
- CamelCase for classes.
|
||||
- camelCase for methods.
|
||||
@ -10,27 +10,23 @@
|
||||
- Require other classes with 'use' at the beginning of the class file.
|
||||
- Do not specify 'public' if method is public, it's implicit.
|
||||
- Always use guard clauses.
|
||||
- Ensure compatibility with PHP 5.3 and newer versions.
|
||||
- Ensure compatibility with PHP 5.5 and newer versions.
|
||||
- Cover your code in tests.
|
||||
|
||||
Recommendations:
|
||||
- Max line length at 80 chars.
|
||||
- Keep classes under 100 LOC.
|
||||
- Keep methods under 10 LOC.
|
||||
- Pass no more than 4 parameters/hash keys into a method.
|
||||
- Keep Pull Requests small, under 100 LOC changed.
|
||||
## JS Code
|
||||
- Javascript code should follow the [Airbnb style guide](https://github.com/airbnb/javascript).
|
||||
|
||||
## Git flow.
|
||||
## Git flow
|
||||
- Do not commit to master.
|
||||
- Open a short-living feature branch.
|
||||
- Open a pull request.
|
||||
- Add Jira issue reference in the title of the Pull Request.
|
||||
- Work on the pull request.
|
||||
- Wait for review and confirmation from another developer before merging to master.
|
||||
- Commit title no more than 80 chars, empty line after.
|
||||
- Commit description as long as you want, 80 chars wrap.
|
||||
- Use the `./do qa` command to check your code style before pushing.
|
||||
- Use good commit messages as explained here https://chris.beams.io/posts/git-commit
|
||||
- Wait for review from another developer.
|
||||
|
||||
## Issues creation.
|
||||
## 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.
|
||||
|
@ -1,11 +1,11 @@
|
||||
FROM mailpoet/wordpress:5.6-cli_20180417.1
|
||||
FROM mailpoet/wordpress:5.6-cli_20181009.1
|
||||
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
RUN composer global require --optimize-autoloader "hirak/prestissimo"
|
||||
|
||||
WORKDIR /wp-core/wp-content/plugins/mailpoet
|
||||
ENV WP_TEST_PATH=/wp-core
|
||||
ENV WP_ROOT=/wp-core
|
||||
|
||||
ADD docker-entrypoint.sh /
|
||||
|
||||
|
302
README.md
302
README.md
@ -1,187 +1,156 @@
|
||||
# MailPoet.
|
||||
# MailPoet
|
||||
|
||||
MailPoet done the right way.
|
||||
|
||||
# Install.
|
||||
# Contents
|
||||
|
||||
- Install system dependencies:
|
||||
```
|
||||
php
|
||||
nodejs
|
||||
wordpress
|
||||
```
|
||||
- [Setup](#setup)
|
||||
- [Frameworks and libraries](#frameworks-and-libraries)
|
||||
- [Workflow Commands](#workflow-commands)
|
||||
- [Coding and Testing](#coding-and-testing)
|
||||
|
||||
- Clone the repo in `wp-content/plugins`.
|
||||
# Setup
|
||||
|
||||
- Install composer.
|
||||
```sh
|
||||
$ curl -sS https://getcomposer.org/installer | php
|
||||
$ ./composer.phar install
|
||||
```
|
||||
## Requirements
|
||||
- PHP 5.6+
|
||||
- NodeJS
|
||||
- WordPress
|
||||
- Docker & Docker Compose
|
||||
|
||||
- Install dependencies.
|
||||
```sh
|
||||
$ ./do install
|
||||
```
|
||||
|
||||
- Update dependencies when needed.
|
||||
```sh
|
||||
$ ./do update
|
||||
```
|
||||
|
||||
- Copy .env.sample to .env.
|
||||
```sh
|
||||
## Installation
|
||||
```bash
|
||||
# go to WP plugins directory
|
||||
$ cd path_to_wp_directory/wp-content/plugins
|
||||
# clone this repository
|
||||
$ git clone https://github.com/mailpoet/mailpoet.git
|
||||
$ cd mailpoet
|
||||
# create the .env file
|
||||
$ cp .env.sample .env
|
||||
```
|
||||
|
||||
- Compile assets.
|
||||
```sh
|
||||
# change the values on .env file
|
||||
# download composer
|
||||
$ curl -sS https://getcomposer.org/installer | php
|
||||
$ chmod +x ./composer.phar
|
||||
# install PHP dependencies
|
||||
$ ./composer.phar install
|
||||
# install all dependencies (PHP and JS)
|
||||
$ ./do install
|
||||
# compile JS and CSS files
|
||||
$ ./do compile:all
|
||||
```
|
||||
|
||||
# Tests.
|
||||
# Frameworks and libraries
|
||||
|
||||
- Unit tests (using [verify](https://github.com/Codeception/Verify)):
|
||||
```sh
|
||||
$ ./do test:unit
|
||||
- [Paris ORM](https://github.com/j4mie/paris).
|
||||
- [Symfony/dependency-injection](https://github.com/symfony/dependency-injection) ([docs for 3.4](https://symfony.com/doc/3.4/components/dependency_injection.html)).
|
||||
- [Mozart](https://github.com/coenjacobs/mozart) for moving dependencies into MP namespace
|
||||
- [Twig](https://twig.symfony.com/) and [Handlebars](https://handlebarsjs.com/) are used for templates rendering.
|
||||
- [Monolog](https://seldaek.github.io/monolog/) is used for logging.
|
||||
- [Robo](https://robo.li/) is used to write and run workflow commands.
|
||||
- [Codeception](https://codeception.com/) is used to write unit and acceptance tests.
|
||||
- [Docker](https://www.docker.com/), [Docker Compose](https://docs.docker.com/compose/) and [Selenium](https://www.seleniumhq.org/) to run acceptance tests.
|
||||
- [React](https://reactjs.org/) is used to create most of UIs.
|
||||
- [Marionette](https://marionettejs.com/) is used to build the newsletters editor.
|
||||
- [Stylus](http://stylus-lang.com/) is used to write styles.
|
||||
- [Mocha](https://mochajs.org/), [Chai](https://www.chaijs.com/) and [Sinon](https://sinonjs.org/) are used to write Javascript tests.
|
||||
- [ESLint](https://eslint.org/) is used to lint JS files.
|
||||
- [Webpack](https://webpack.js.org/) is used to bundle assets.
|
||||
|
||||
# Workflow Commands
|
||||
|
||||
```bash
|
||||
$ ./do install # install PHP and JS dependencies
|
||||
$ ./do update # update PHP and JS dependencies
|
||||
|
||||
$ ./do compile:css # compiles Stylus files into CSS.
|
||||
$ ./do compile:js # bundles JS files for the browser.
|
||||
$ ./do compile:all # compiles CSS and JS files.
|
||||
|
||||
$ ./do watch:css # watch CSS files for changes and compile them.
|
||||
$ ./do watch:js # watch JS files for changes and compile them.
|
||||
$ ./do watch # watch CSS and JS files for changes and compile them.
|
||||
|
||||
$ ./do test:unit [--file=...] [--debug]
|
||||
# runs the PHP unit tests.
|
||||
# if --file specified then only tests on that file are executed.
|
||||
# if --debug then tests are executed in debugging mode.
|
||||
$ ./do test:integration [--file=...] [--multisite] [--debug]
|
||||
# runs the PHP integration tests.
|
||||
# if --file specified then only tests on that file are executed.
|
||||
# if --multisite then tests are executed in a multisite wordpress setup.
|
||||
# if --debug then tests are executed in debugging mode.
|
||||
$ ./do test:multisite:integration # alias for ./do test:integration --multisite
|
||||
$ ./do test:debug:unit # alias for ./do test:unit --debug
|
||||
$ ./do test:debug:integration # alias for ./do test:integration --debug
|
||||
$ ./do test:failed:unit # run the last failing unit test.
|
||||
$ ./do test:failed:integration # run the last failing integration test.
|
||||
$ ./do test:coverage # run tests and output coverage information.
|
||||
$ ./do test:javascript # run the JS tests.
|
||||
$ ./do test:acceptance [--file=...] [--skip-deps]
|
||||
# run acceptances tests into a docker environment.
|
||||
# if --file given then only tests on that file are executed.
|
||||
# if --skip-deps then it skips installation of composer dependencies.
|
||||
$ ./do test:acceptance:multisite [--file=...] [--skip-deps]
|
||||
# same as test:acceptance but runs into a multisite wordpress setup.
|
||||
$ ./do delete:docker # stop and remove all running docker containers.
|
||||
|
||||
$ ./do qa:lint # PHP code linter.
|
||||
$ ./do qa:lint:javascript # JS code linter.
|
||||
$ ./do qa # PHP and JS linters.
|
||||
|
||||
$ ./do container:dump # Generates DI container cache.
|
||||
```
|
||||
|
||||
- JS tests (using Mocha):
|
||||
```sh
|
||||
$ ./do test:javascript
|
||||
# Coding and Testing
|
||||
|
||||
## DI
|
||||
|
||||
We use Symfony/dependency-injection container. Container configuration can be found in `libs/DI/ContainerFactory.php`
|
||||
The container is configured and used with minimum sub-dependencies to keep final package size small.
|
||||
You can check [the docs](https://symfony.com/doc/3.4/components/dependency_injection.html) to learn more about Symfony Container.
|
||||
|
||||
## Mozart
|
||||
|
||||
We use Mozart plugin for composer to prevent plugin libraries conflicts in PHP. Two plugins may be using different versions of a library. Mozart prefix dependencies namespaces and moves them into `libs\Dependencies` directory.
|
||||
Dependencies handled by Mozart are configured in extra configuration file `mozart/composer.json`. Installation and processing is triggered in post scripts of the main `composer.json` file.
|
||||
|
||||
## i18n
|
||||
|
||||
We use functions `__()`, `_n()` and `_x()` with domain `mailpoet` to translate strings.
|
||||
|
||||
**in PHP code**
|
||||
|
||||
```php
|
||||
__('text to translate', 'mailpoet');
|
||||
_n('single text', 'plural text', $number, 'mailpoet');
|
||||
_x('text to translate', 'context for translators', 'mailpoet');
|
||||
```
|
||||
|
||||
- Debug tests:
|
||||
```sh
|
||||
$ ./do test:debug
|
||||
```
|
||||
|
||||
- Code linters and quality checkers:
|
||||
```sh
|
||||
$ ./do qa
|
||||
```
|
||||
|
||||
- Javascript linter:
|
||||
```sh
|
||||
$ ./do lint:javascript
|
||||
```
|
||||
|
||||
# CSS
|
||||
- [Stylus](https://learnboost.github.io/stylus/)
|
||||
- [Nib extension](http://tj.github.io/nib/)
|
||||
|
||||
```sh
|
||||
assets/css/src -> place your *.styl files here
|
||||
```
|
||||
|
||||
### Watch for changes and recompile
|
||||
```sh
|
||||
$ ./do watch
|
||||
```
|
||||
|
||||
## Module loading and organization
|
||||
|
||||
Our JS modules are stored in `assets/js/` folder. Modules should follow AMD module definition style:
|
||||
|
||||
```js
|
||||
define('moduleName', ['dependency1', 'dependency2'], function(dependency1, dependency2){
|
||||
// Module code here
|
||||
|
||||
return {
|
||||
// Module exports here
|
||||
};
|
||||
})
|
||||
```
|
||||
|
||||
Module loader will look for `dependency1` in `node_modules/` dependencies, as well as in `assets/js`. So you can use dependencies, defined in `package.json`, without the need of providing an absolute path to it.
|
||||
Once found, dependencies will be injected into your module via function arguments.
|
||||
|
||||
When it comes to loading modules on a real page, WebPack uses "entry points" to create different bundles. In order for the module to be included in a specific bundle, it must be reachable from that bundle's entry point. [A good example on WebPack's website](http://webpack.github.io/docs/code-splitting.html#split-app-and-vendor-code).
|
||||
|
||||
Once javascript is compiled with `./do compile:javascript`, your module will be placed into a bundle. Including that bundle in a webpage will give provide you access to your module.
|
||||
|
||||
## Handlebars (`views/*.hbs`)
|
||||
**in Twig views**
|
||||
|
||||
```html
|
||||
<!-- use the `templates` block -->
|
||||
<% block templates %>
|
||||
<!-- include a .hbs template -->
|
||||
<%= partial('my_template_1', 'form/templates/toolbar/fields.hbs') %>
|
||||
<%= __('text to translate') %>
|
||||
<%= _n('single text', 'plural text', $number) %>
|
||||
<%= _x('text to translate', 'context for translators') %>
|
||||
```
|
||||
|
||||
<!-- include a .hbs template and register it as a partial -->
|
||||
<%= partial('my_template_2', 'form/templates/blocks.hbs', '_my_partial') %>
|
||||
The domain `mailpoet` will be added automatically by the Twig functions.
|
||||
|
||||
<!-- custom partial using partial defined above -->
|
||||
<script id="my_template_3" type="text/x-handlebars-template">
|
||||
{{> _my_partial }}
|
||||
</script>
|
||||
**in Javascript code**
|
||||
|
||||
First add the string to the translations block in the Twig view:
|
||||
|
||||
```html
|
||||
<% block translations %>
|
||||
<%= localize({
|
||||
'key': __('string to translate'),
|
||||
...
|
||||
}) %>
|
||||
<% endblock %>
|
||||
```
|
||||
|
||||
# i18n
|
||||
- Use the regular WordPress functions in PHP and Twig:
|
||||
Then use `MailPoet.I18n.t('key')` to get the translated string on your Javascript code.
|
||||
|
||||
```php
|
||||
__()
|
||||
_n()
|
||||
_x()
|
||||
```
|
||||
|
||||
```html
|
||||
<p>
|
||||
<%= __('Click %shere%s!') | format('<a href="#">', '</a>') | raw %>
|
||||
</p>
|
||||
```
|
||||
|
||||
```html
|
||||
<p>
|
||||
<%= _n('deleted %d message', 'deleted %d messages', count) | format(count) %>
|
||||
<!-- count === 1 -> "deleted 1 message" -->
|
||||
<!-- count > 1 -> "deleted $count messages" -->
|
||||
</p>
|
||||
```
|
||||
|
||||
- Handlebars.
|
||||
|
||||
You can use Twig i18n functions in Handlebars, just load your template from a Twig view.
|
||||
|
||||
# Build
|
||||
|
||||
To build a plugin , run `./build.sh`.
|
||||
|
||||
Some build process steps are described below (their dependencies etc.).
|
||||
|
||||
## packtranslations step
|
||||
|
||||
This step imports translations from Transifex and generates MO files. It requires:
|
||||
* `tx` client: https://docs.transifex.com/client/installing-the-client
|
||||
* `msgfmt` command (from Gettext package)
|
||||
Finally , a `WP_TRANSIFEX_API_TOKEN` environment variable should be initialized with a valid key.
|
||||
|
||||
# Publish
|
||||
|
||||
The `publish` command currently does the following:
|
||||
* Pushes translations POT file to Transifex;
|
||||
* Publishes the release in SVN.
|
||||
|
||||
Before you run it, you need to:
|
||||
1. Ensure there is an up-to-date local copy of MailPoet SVN repository in `.mp_svn` directory by running `./do svn:checkout`.
|
||||
2. Have all your features merged in Git `master`, your `mailpoet.php` and `readme.txt` tagged with a new version.
|
||||
3. Run `./build.sh` to produce a `mailpoet.zip` distributable archive.
|
||||
|
||||
Everything's ready? Then run `./do publish`.
|
||||
If the job goes fine, you'll get a message like this:
|
||||
```
|
||||
Go to '.mp_svn' and run 'svn ci -m "Release 3.0.0-beta.9"' to publish the
|
||||
release
|
||||
|
||||
Run 'svn copy ...' to tag the release
|
||||
```
|
||||
It's quite literal: you can review the changes to be pushed and if you're satisfied, run the suggested command to finish the release publishing process.
|
||||
|
||||
If you're confident, execute `./do publish --force` and your release will be published to the remote SVN repository without manual intervention (automatically). For easier authentication you might want to set `WP_SVN_USERNAME` and `WP_SVN_PASSWORD` environment variables.
|
||||
|
||||
# Acceptance testing
|
||||
## Acceptance testing
|
||||
|
||||
We are using Gravity Flow plugin's setup as an example for our acceptance test suite: https://www.stevenhenty.com/learn-acceptance-testing-deeply/
|
||||
|
||||
@ -189,17 +158,6 @@ From the article above:
|
||||
|
||||
_Windows users only: enable hard drive sharing in the Docker settings._
|
||||
|
||||
The browser runs in a docker container. You can use a VNC client to watch the test run, follow instructions in official
|
||||
The browser runs in a docker container. You can use a VNC client to watch the test run, follow instructions in official
|
||||
repo: https://github.com/SeleniumHQ/docker-selenium
|
||||
If you’re on a Mac, you can open vnc://localhost:5900 in Safari to watch the tests running in Chrome. If you’re on Windows, you’ll need a VNC client. Password: secret.
|
||||
|
||||
|
||||
To run tests:
|
||||
```sh
|
||||
$ ./do test:acceptance
|
||||
```
|
||||
|
||||
You can skip installation of composer dependencies using --skip-deps parameter.
|
||||
```sh
|
||||
$ ./do test:acceptance --skip-deps
|
||||
```
|
||||
|
77
RoboFile.php
77
RoboFile.php
@ -156,7 +156,27 @@ class RoboFile extends \Robo\Tasks {
|
||||
function testUnit(array $opts=['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
|
||||
$this->loadEnv();
|
||||
|
||||
$command = 'vendor/bin/codecept run unit -c codeception.unit.yml';
|
||||
$command = 'vendor/bin/codecept run unit';
|
||||
|
||||
if($opts['file']) {
|
||||
$command .= ' -f ' . $opts['file'];
|
||||
}
|
||||
|
||||
if($opts['xml']) {
|
||||
$command .= ' --xml';
|
||||
}
|
||||
|
||||
if($opts['debug']) {
|
||||
$command .= ' --debug';
|
||||
}
|
||||
|
||||
return $this->_exec($command);
|
||||
}
|
||||
|
||||
function testIntegration(array $opts=['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
|
||||
$this->loadEnv();
|
||||
|
||||
$command = 'vendor/bin/codecept run integration';
|
||||
|
||||
if($opts['multisite']) {
|
||||
$command = 'MULTISITE=true ' . $command;
|
||||
@ -177,14 +197,14 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->_exec($command);
|
||||
}
|
||||
|
||||
function testMultisiteUnit($opts=['file' => null, 'xml' => false, 'multisite' => true]) {
|
||||
return $this->testUnit($opts);
|
||||
function testMultisiteIntegration($opts=['file' => null, 'xml' => false, 'multisite' => true]) {
|
||||
return $this->testIntegration($opts);
|
||||
}
|
||||
|
||||
function testCoverage($opts=['file' => null, 'xml' => false]) {
|
||||
$this->loadEnv();
|
||||
$command = join(' ', array(
|
||||
'vendor/bin/codecept run unit -c codeception.unit.yml ',
|
||||
'vendor/bin/codecept run -s acceptance',
|
||||
(($opts['file']) ? $opts['file'] : ''),
|
||||
'--coverage',
|
||||
($opts['xml']) ? '--coverage-xml' : '--coverage-html'
|
||||
@ -219,16 +239,12 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->_exec('vendor/bin/security-checker security:check --format=simple');
|
||||
}
|
||||
|
||||
function testDebug($opts=['file' => null, 'xml' => false]) {
|
||||
$this->loadEnv();
|
||||
$this->_exec('vendor/bin/codecept build -c codeception.unit.yml');
|
||||
function testDebugUnit($opts=['file' => null, 'xml' => false, 'debug' => true]) {
|
||||
return $this->testUnit($opts);
|
||||
}
|
||||
|
||||
$command = 'vendor/bin/codecept run unit -c codeception.unit.yml --debug -f '.(($opts['file']) ? $opts['file'] : '');
|
||||
|
||||
if($opts['xml']) {
|
||||
$command .= ' --xml';
|
||||
}
|
||||
return $this->_exec($command);
|
||||
function testDebugIntegration($opts=['file' => null, 'xml' => false, 'debug' => true]) {
|
||||
return $this->testIntegration($opts);
|
||||
}
|
||||
|
||||
function testAcceptance($opts=['file' => null, 'skip-deps' => false]) {
|
||||
@ -254,10 +270,31 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->_exec('docker-compose down -v --remove-orphans --rmi all');
|
||||
}
|
||||
|
||||
function testFailed() {
|
||||
function testFailedUnit() {
|
||||
$this->loadEnv();
|
||||
$this->_exec('vendor/bin/codecept build -c codeception.unit.yml');
|
||||
return $this->_exec('vendor/bin/codecept run -c codeception.unit.yml -g failed');
|
||||
$this->_exec('vendor/bin/codecept build');
|
||||
return $this->_exec('vendor/bin/codecept run unit -g failed');
|
||||
}
|
||||
|
||||
function testFailedIntegration() {
|
||||
$this->loadEnv();
|
||||
$this->_exec('vendor/bin/codecept build');
|
||||
return $this->_exec('vendor/bin/codecept run integration -g failed');
|
||||
}
|
||||
|
||||
function containerDump() {
|
||||
$this->say('Deleting DI Container');
|
||||
$this->_exec('rm -f ./lib/DI/CachedContainer.php');
|
||||
$this->say('Generating DI container cache');
|
||||
$this->loadEnv();
|
||||
define('ABSPATH', getenv('WP_ROOT') . '/');
|
||||
if (!file_exists(ABSPATH . 'wp-config.php')) {
|
||||
$this->yell('WP_ROOT env variable does not contain valid path to wordpress root.', 40, 'red');
|
||||
exit(1);
|
||||
}
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
$container_factory = new \MailPoet\DI\ContainerFactory();
|
||||
$container_factory->dumpContainer();
|
||||
}
|
||||
|
||||
function qa() {
|
||||
@ -288,7 +325,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
->taskExec(
|
||||
'./vendor/bin/phpcs '.
|
||||
'--standard=./tasks/code_sniffer/MailPoet '.
|
||||
'--runtime-set testVersion 5.5-7.2 '.
|
||||
'--runtime-set testVersion 5.6-7.2 '.
|
||||
'--ignore=./lib/Util/Sudzy/*,./lib/Util/CSS.php,./lib/Util/XLSXWriter.php,./lib/Dependencies/*,'.
|
||||
'./lib/Util/pQuery/*,./lib/Config/PopulatorData/Templates/* '.
|
||||
'lib/ '.
|
||||
@ -297,9 +334,9 @@ class RoboFile extends \Robo\Tasks {
|
||||
->taskExec(
|
||||
'./vendor/bin/phpcs '.
|
||||
'--standard=./tasks/code_sniffer/MailPoet '.
|
||||
'--runtime-set testVersion 5.5-7.2 '.
|
||||
'--ignore=./tests/unit/_bootstrap.php '.
|
||||
'tests/unit/ '.
|
||||
'--runtime-set testVersion 5.6-7.2 '.
|
||||
'--ignore=./tests/unit/_bootstrap.php,./tests/unit/_fixtures.php,./tests/integration/_bootstrap.php,./tests/integration/_fixtures.php '.
|
||||
'tests/unit tests/integration tests/acceptance tests/DataFactories '.
|
||||
$severityFlag
|
||||
)
|
||||
->run();
|
||||
|
@ -16,3 +16,18 @@ Style for Members plugin
|
||||
|
||||
#wpbody
|
||||
padding-bottom: 20px;
|
||||
|
||||
/* menu icon */
|
||||
#adminmenu #toplevel_page_mailpoet-newsletters .wp-menu-image
|
||||
background-size: 18px 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
#adminmenu #toplevel_page_mailpoet-newsletters.wp-not-current-submenu .wp-menu-image
|
||||
background-image: url('data:image/png;base64,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');
|
||||
|
||||
#adminmenu #toplevel_page_mailpoet-newsletters.wp-has-current-submenu .wp-menu-image
|
||||
background-image: url('data:image/png;base64,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');
|
||||
|
||||
#adminmenu #toplevel_page_mailpoet-newsletters a:hover .wp-menu-image
|
||||
background-image: url('data:image/png;base64,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');
|
||||
|
@ -36,3 +36,5 @@
|
||||
@require 'welcome_wizard'
|
||||
@require 'intro'
|
||||
@require 'in_app_announcements'
|
||||
@require 'newsletter_congratulate.styl'
|
||||
@require 'discounts'
|
||||
|
17
assets/css/src/discounts.styl
Normal file
17
assets/css/src/discounts.styl
Normal file
@ -0,0 +1,17 @@
|
||||
.mailpoet-discount-container
|
||||
margin: 15px
|
||||
padding: 20px
|
||||
background: white
|
||||
border: 1px solid #FF5301
|
||||
text-align: center
|
||||
|
||||
.mailpoet-discount-container h1
|
||||
margin: 0
|
||||
line-height: 1.2em
|
||||
font-size: 2.8em
|
||||
font-weight: 400
|
||||
|
||||
.mailpoet-discount-container p
|
||||
line-height: 1.2em
|
||||
font-size: 1.2em
|
||||
|
@ -40,9 +40,14 @@
|
||||
top: -3px
|
||||
left: 8px
|
||||
|
||||
.mailpoet_in_app_announcement_background_videos
|
||||
.mailpoet_in_app_announcement_background_videos, .mailpoet_drag_and_drop_tutorial
|
||||
text-align: center
|
||||
h2
|
||||
font-size: 28px
|
||||
video
|
||||
margin-top: 20px
|
||||
|
||||
.new_subscriber_notification_announcement
|
||||
h2
|
||||
font-size: 28px
|
||||
text-align: center
|
||||
|
@ -168,14 +168,18 @@ body.mailpoet_modal_opened
|
||||
padding-bottom: 52px
|
||||
|
||||
#mailpoet_loading
|
||||
width: 150px
|
||||
height: 32px
|
||||
position: relative
|
||||
left: 50%
|
||||
top: 50%
|
||||
margin-left: -75px
|
||||
margin-top: -16px
|
||||
|
||||
.mailpoet_loading
|
||||
height: 32px
|
||||
width: 150px
|
||||
display flex
|
||||
flex-direction: row
|
||||
|
||||
.mailpoet_modal_loading
|
||||
animation-direction(linear)
|
||||
animation-duration(1.9500000000000002s)
|
||||
@ -183,18 +187,17 @@ body.mailpoet_modal_opened
|
||||
animation-name(bounce_mailpoet_modal_loading)
|
||||
border-radius(21px)
|
||||
background-color: #E01D4E
|
||||
float: left
|
||||
height: 32px
|
||||
margin-left: 17px
|
||||
width: 32px
|
||||
|
||||
#mailpoet_modal_loading_1
|
||||
#mailpoet_modal_loading_1, .mailpoet_modal_loading_1
|
||||
animation-delay(0.39s)
|
||||
|
||||
#mailpoet_modal_loading_2
|
||||
#mailpoet_modal_loading_2, .mailpoet_modal_loading_2
|
||||
animation-delay(0.9099999999999999s)
|
||||
|
||||
#mailpoet_modal_loading_3
|
||||
#mailpoet_modal_loading_3, .mailpoet_modal_loading_3
|
||||
animation-delay(1.1700000000000002s)
|
||||
|
||||
@keyframes bounce_mailpoet_modal_loading
|
||||
|
23
assets/css/src/newsletter_congratulate.styl
Normal file
23
assets/css/src/newsletter_congratulate.styl
Normal file
@ -0,0 +1,23 @@
|
||||
.newsletter_congratulate_page
|
||||
margin-top: 30px;
|
||||
|
||||
.mailpoet_newsletter_loading
|
||||
text-align: center;
|
||||
|
||||
.mailpoet_loading
|
||||
margin: 100px auto 0 auto;
|
||||
|
||||
.mailpoet_newsletter_loading_header
|
||||
margin: 30px;
|
||||
|
||||
.mailpoet_congratulate_success
|
||||
width: 100%;
|
||||
|
||||
h1
|
||||
text-align center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
img, .button
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
@ -236,3 +236,28 @@ select.mailpoet_font-size
|
||||
margin-left: 10px
|
||||
input.mailpoet_option_offset_left_small
|
||||
margin-left: 10px !important
|
||||
|
||||
.mailpoet_form_field span.select2-container
|
||||
width: 103px !important
|
||||
|
||||
span.select2-container--open > span.select2-dropdown
|
||||
width: 150px !important
|
||||
|
||||
span.select2-container--open > span.select2-dropdown li.select2-results__option
|
||||
font-size: 13px
|
||||
margin: 0px !important
|
||||
|
||||
& .select2-results__group
|
||||
font-weight: normal
|
||||
color: #bfbfbf
|
||||
|
||||
& .select2-results__option
|
||||
padding-left: 15px
|
||||
font-size: 13px
|
||||
|
||||
&[aria-selected=true]
|
||||
background-color: #eee
|
||||
color: #444
|
||||
|
||||
.mailpoet-fonts-notice
|
||||
color: #999
|
||||
|
@ -1,5 +1,5 @@
|
||||
#mailpoet_editor_bottom
|
||||
margin: 10px 0 70px
|
||||
margin: 10px 0 120px
|
||||
|
||||
.mailpoet_save_wrapper
|
||||
float: right
|
||||
@ -7,6 +7,9 @@
|
||||
margin-right: 20px
|
||||
margin-bottom: 10px
|
||||
|
||||
.mailpoet_save_next
|
||||
margin-left: 5px
|
||||
|
||||
.mailpoet_save_options
|
||||
border-radius(3px)
|
||||
|
||||
@ -43,6 +46,8 @@
|
||||
|
||||
.mailpoet_save_show_options_icon
|
||||
vertical-align: middle
|
||||
height: 14px;
|
||||
margin-top: -6px;
|
||||
|
||||
.mailpoet_save_as_template_container,
|
||||
.mailpoet_export_template_container
|
||||
@ -61,16 +66,25 @@
|
||||
.mailpoet_save_as_template_title,
|
||||
.mailpoet_export_template_title
|
||||
font-size: 1.1em
|
||||
|
||||
.mailpoet_save_next, .mailpoet_save_button_group
|
||||
float: right
|
||||
|
||||
.mailpoet_editor_messages
|
||||
position: absolute
|
||||
right: 0
|
||||
|
||||
.mailpoet_editor_last_saved
|
||||
color: $primary-inactive-color
|
||||
font-size: 0.9em
|
||||
position: absolute
|
||||
right: 0
|
||||
margin-top: 10px
|
||||
text-align: right
|
||||
|
||||
.mailpoet_save_error
|
||||
margin-top: 10px
|
||||
width: $sidebar-width - 20px
|
||||
color: $error-text-color
|
||||
text-align: right
|
||||
|
||||
.mailpoet_save_dropdown_down
|
||||
.mailpoet_save_options,
|
||||
|
@ -20,6 +20,10 @@
|
||||
& > .mailpoet_block
|
||||
width: 100%
|
||||
|
||||
.mailpoet_container_block
|
||||
margin-bottom: 0
|
||||
|
||||
|
||||
.mailpoet_automated_latest_content_display_options
|
||||
animation-slide-open-downwards()
|
||||
|
||||
|
@ -2,6 +2,7 @@ $column-margin = 20px
|
||||
$one-column-width = $newsletter-width - (2 * $column-margin)
|
||||
$two-column-width = ($newsletter-width / 2) - (2 * $column-margin)
|
||||
$three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
|
||||
$two-column-wider-column-width = (($newsletter-width / 3) - $column-margin) * 2
|
||||
|
||||
.mailpoet_container
|
||||
width: 100%
|
||||
@ -27,12 +28,6 @@ $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
|
||||
.mailpoet_container_horizontal > *
|
||||
vertical-align: top
|
||||
|
||||
/**
|
||||
* Enforce column widths:
|
||||
* 1 column: 20px + 560px + 20px
|
||||
* 2 columns: 20px + 260px + 20px + 260px + 20px
|
||||
* 3 columns: 20px + 160px + 20px + 20px + 160px + 20px + 20px + 160px + 20px
|
||||
*/
|
||||
|
||||
#mailpoet_editor_content
|
||||
.mailpoet_container
|
||||
@ -73,6 +68,14 @@ $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
|
||||
//padding-right: 20px
|
||||
width: $column-margin + $three-column-width + $column-margin
|
||||
|
||||
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_1_2 > .mailpoet_container_block:first-child,
|
||||
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_2_1 > .mailpoet_container_block:nth-child(2)
|
||||
width: $column-margin + $three-column-width + $column-margin
|
||||
|
||||
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_2_1 > .mailpoet_container_block:first-child,
|
||||
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_1_2 > .mailpoet_container_block:nth-child(2)
|
||||
width: $column-margin + $two-column-wider-column-width + $column-margin
|
||||
|
||||
.mailpoet_container_empty
|
||||
text-align: center
|
||||
background-color: #f2f2f2
|
||||
|
BIN
assets/img/in_app_announcements/new-subscriber-notification.png
Normal file
BIN
assets/img/in_app_announcements/new-subscriber-notification.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 751 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
BIN
assets/img/newsletter/congrat-illu-success.png
Normal file
BIN
assets/img/newsletter/congrat-illu-success.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 229 KiB |
Binary file not shown.
After Width: | Height: | Size: 436 KiB |
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const KeyValueTable = props => (
|
||||
@ -13,13 +14,13 @@ const KeyValueTable = props => (
|
||||
);
|
||||
|
||||
KeyValueTable.propTypes = {
|
||||
max_width: React.PropTypes.string,
|
||||
rows: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
key: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.element,
|
||||
max_width: PropTypes.string,
|
||||
rows: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
})).isRequired,
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
@ -10,10 +11,10 @@ const PrintBoolean = props => (
|
||||
);
|
||||
|
||||
PrintBoolean.propTypes = {
|
||||
truthy: React.PropTypes.string,
|
||||
falsy: React.PropTypes.string,
|
||||
unknown: React.PropTypes.string,
|
||||
children: React.PropTypes.bool,
|
||||
truthy: PropTypes.string,
|
||||
falsy: PropTypes.string,
|
||||
unknown: PropTypes.string,
|
||||
children: PropTypes.bool,
|
||||
};
|
||||
|
||||
PrintBoolean.defaultProps = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const SteppedProgressBar = (props) => {
|
||||
@ -20,8 +21,8 @@ const SteppedProgressBar = (props) => {
|
||||
};
|
||||
|
||||
SteppedProgressBar.propTypes = {
|
||||
steps_count: React.PropTypes.number.isRequired,
|
||||
step: React.PropTypes.number.isRequired,
|
||||
steps_count: PropTypes.number.isRequired,
|
||||
step: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
module.exports = SteppedProgressBar;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormFieldCheckbox = React.createClass({
|
||||
onValueChange: function onValueChange(e) {
|
||||
class FormFieldCheckbox extends React.Component {
|
||||
onValueChange = (e) => {
|
||||
e.target.value = this.checkbox.checked ? '1' : '0';
|
||||
return this.props.onValueChange(e);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.field.values === undefined) {
|
||||
return false;
|
||||
}
|
||||
@ -37,7 +39,16 @@ const FormFieldCheckbox = React.createClass({
|
||||
{ options }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FormFieldCheckbox.propTypes = {
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
export default FormFieldCheckbox;
|
||||
|
@ -36,7 +36,10 @@ FormFieldDateYear.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
year: PropTypes.string.isRequired,
|
||||
year: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
function FormFieldDateMonth(props) {
|
||||
@ -71,7 +74,10 @@ FormFieldDateMonth.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
month: PropTypes.string.isRequired,
|
||||
month: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
monthNames: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
@ -108,7 +114,10 @@ FormFieldDateDay.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
day: PropTypes.string.isRequired,
|
||||
day: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
class FormFieldDate extends React.Component {
|
||||
|
@ -7,9 +7,10 @@ import FormFieldCheckbox from 'form/fields/checkbox.jsx';
|
||||
import FormFieldSelection from 'form/fields/selection.jsx';
|
||||
import FormFieldDate from 'form/fields/date.jsx';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormField = React.createClass({
|
||||
renderField: function renderField(data, inline = false) {
|
||||
class FormField extends React.Component {
|
||||
renderField = (data, inline = false) => {
|
||||
let description = false;
|
||||
if (data.field.description) {
|
||||
description = (
|
||||
@ -76,8 +77,9 @@ const FormField = React.createClass({
|
||||
{ description }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
let field = false;
|
||||
|
||||
if (this.props.field.fields !== undefined) {
|
||||
@ -113,7 +115,29 @@ const FormField = React.createClass({
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
onValueChange: PropTypes.func,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
tip: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.string,
|
||||
]),
|
||||
label: PropTypes.string,
|
||||
fields: PropTypes.array,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
FormField.defaultProps = {
|
||||
onValueChange: function onValueChange() {
|
||||
// no-op
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default FormField;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormFieldRadio = React.createClass({
|
||||
render: function render() {
|
||||
class FormFieldRadio extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
|
||||
render() {
|
||||
if (this.props.field.values === undefined) {
|
||||
return false;
|
||||
}
|
||||
@ -30,7 +31,23 @@ const FormFieldRadio = React.createClass({
|
||||
{ options }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormFieldRadio.propTypes = {
|
||||
onValueChange: PropTypes.func,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
FormFieldRadio.defaultProps = {
|
||||
onValueChange: function onValueChange() {
|
||||
// no-op
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export default FormFieldRadio;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import _ from 'underscore';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormFieldSelect = React.createClass({
|
||||
class FormFieldSelect extends React.Component {
|
||||
render() {
|
||||
if (this.props.field.values === undefined) {
|
||||
return false;
|
||||
@ -70,7 +71,29 @@ const FormFieldSelect = React.createClass({
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormFieldSelect.propTypes = {
|
||||
onValueChange: PropTypes.func,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
placeholder: PropTypes.string,
|
||||
filter: PropTypes.func,
|
||||
sortBy: PropTypes.func,
|
||||
validation: PropTypes.object,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
automationId: PropTypes.string,
|
||||
};
|
||||
|
||||
FormFieldSelect.defaultProps = {
|
||||
automationId: '',
|
||||
onValueChange: function onValueChange() {
|
||||
// no-op
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = FormFieldSelect;
|
||||
|
@ -3,23 +3,16 @@ import jQuery from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import 'react-dom';
|
||||
import 'select2';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Selection = React.createClass({
|
||||
allowMultipleValues: function allowMultipleValues() {
|
||||
return (this.props.field.multiple === true);
|
||||
},
|
||||
isSelect2Initialized: function isSelect2Initialized() {
|
||||
return (jQuery(`#${this.select.id}`).hasClass('select2-hidden-accessible') === true);
|
||||
},
|
||||
isSelect2Component: function isSelect2Component() {
|
||||
return this.allowMultipleValues() || this.props.field.forceSelect2;
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
class Selection extends React.Component {
|
||||
componentDidMount() {
|
||||
if (this.isSelect2Component()) {
|
||||
this.setupSelect2();
|
||||
}
|
||||
},
|
||||
componentDidUpdate: function componentDidUpdate(prevProps) {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if ((this.props.item !== undefined && prevProps.item !== undefined)
|
||||
&& (this.props.item.id !== prevProps.item.id)
|
||||
) {
|
||||
@ -34,38 +27,73 @@ const Selection = React.createClass({
|
||||
) {
|
||||
this.resetSelect2();
|
||||
}
|
||||
},
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.isSelect2Component()) {
|
||||
this.destroySelect2();
|
||||
}
|
||||
},
|
||||
getFieldId: function getFieldId(data) {
|
||||
}
|
||||
|
||||
getFieldId = (data) => {
|
||||
const props = data || this.props;
|
||||
return props.field.id || props.field.name;
|
||||
},
|
||||
resetSelect2: function resetSelect2() {
|
||||
this.destroySelect2();
|
||||
this.setupSelect2();
|
||||
},
|
||||
destroySelect2: function destroySelect2() {
|
||||
if (this.isSelect2Initialized()) {
|
||||
jQuery(`#${this.select.id}`).select2('destroy');
|
||||
this.cleanupAfterSelect2();
|
||||
}
|
||||
},
|
||||
cleanupAfterSelect2: function cleanupAfterSelect2() {
|
||||
// remove DOM elements created by Select2 that are not tracked by React
|
||||
jQuery(`#${this.select.id}`)
|
||||
.find('option:not(.default)')
|
||||
.remove();
|
||||
};
|
||||
|
||||
// unbind events (https://select2.org/programmatic-control/methods#event-unbinding)
|
||||
jQuery(`#${this.select.id}`)
|
||||
.off('select2:unselecting')
|
||||
.off('select2:opening');
|
||||
},
|
||||
setupSelect2: function setupSelect2() {
|
||||
getSelectedValues = () => {
|
||||
if (this.props.field.selected !== undefined) {
|
||||
return this.props.field.selected(this.props.item);
|
||||
} else if (this.props.item !== undefined && this.props.field.name !== undefined) {
|
||||
if (this.allowMultipleValues()) {
|
||||
if (_.isArray(this.props.item[this.props.field.name])) {
|
||||
return this.props.item[this.props.field.name].map(item => item.id);
|
||||
}
|
||||
} else {
|
||||
return this.props.item[this.props.field.name];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getItems = () => {
|
||||
let items;
|
||||
if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
|
||||
items = window[`mailpoet_${this.props.field.endpoint}`];
|
||||
} else if (this.props.field.values !== undefined) {
|
||||
items = this.props.field.values;
|
||||
}
|
||||
|
||||
if (_.isArray(items)) {
|
||||
if (this.props.field.filter !== undefined) {
|
||||
items = items.filter(this.props.field.filter);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
getLabel = (item) => {
|
||||
if (this.props.field.getLabel !== undefined) {
|
||||
return this.props.field.getLabel(item, this.props.item);
|
||||
}
|
||||
return item.name;
|
||||
};
|
||||
|
||||
getSearchLabel = (item) => {
|
||||
if (this.props.field.getSearchLabel !== undefined) {
|
||||
return this.props.field.getSearchLabel(item, this.props.item);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getValue = (item) => {
|
||||
if (this.props.field.getValue !== undefined) {
|
||||
return this.props.field.getValue(item, this.props.item);
|
||||
}
|
||||
return item.id;
|
||||
};
|
||||
|
||||
setupSelect2 = () => {
|
||||
if (this.isSelect2Initialized()) {
|
||||
return;
|
||||
}
|
||||
@ -138,38 +166,39 @@ const Selection = React.createClass({
|
||||
});
|
||||
|
||||
select2.on('change', this.handleChange);
|
||||
},
|
||||
getSelectedValues: function getSelectedValues() {
|
||||
if (this.props.field.selected !== undefined) {
|
||||
return this.props.field.selected(this.props.item);
|
||||
} else if (this.props.item !== undefined && this.props.field.name !== undefined) {
|
||||
if (this.allowMultipleValues()) {
|
||||
if (_.isArray(this.props.item[this.props.field.name])) {
|
||||
return this.props.item[this.props.field.name].map(item => item.id);
|
||||
}
|
||||
} else {
|
||||
return this.props.item[this.props.field.name];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getItems: function getItems() {
|
||||
let items;
|
||||
if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
|
||||
items = window[`mailpoet_${this.props.field.endpoint}`];
|
||||
} else if (this.props.field.values !== undefined) {
|
||||
items = this.props.field.values;
|
||||
}
|
||||
};
|
||||
|
||||
if (_.isArray(items)) {
|
||||
if (this.props.field.filter !== undefined) {
|
||||
items = items.filter(this.props.field.filter);
|
||||
}
|
||||
}
|
||||
resetSelect2 = () => {
|
||||
this.destroySelect2();
|
||||
this.setupSelect2();
|
||||
};
|
||||
|
||||
return items;
|
||||
},
|
||||
handleChange: function handleChange(e) {
|
||||
destroySelect2 = () => {
|
||||
if (this.isSelect2Initialized()) {
|
||||
jQuery(`#${this.select.id}`).select2('destroy');
|
||||
this.cleanupAfterSelect2();
|
||||
}
|
||||
};
|
||||
|
||||
cleanupAfterSelect2 = () => {
|
||||
// remove DOM elements created by Select2 that are not tracked by React
|
||||
jQuery(`#${this.select.id}`)
|
||||
.find('option:not(.default)')
|
||||
.remove();
|
||||
|
||||
// unbind events (https://select2.org/programmatic-control/methods#event-unbinding)
|
||||
jQuery(`#${this.select.id}`)
|
||||
.off('select2:unselecting')
|
||||
.off('select2:opening');
|
||||
};
|
||||
|
||||
allowMultipleValues = () => (this.props.field.multiple === true);
|
||||
|
||||
isSelect2Initialized = () => (jQuery(`#${this.select.id}`).hasClass('select2-hidden-accessible') === true);
|
||||
|
||||
isSelect2Component = () => this.allowMultipleValues() || this.props.field.forceSelect2;
|
||||
|
||||
handleChange = (e) => {
|
||||
if (this.props.onValueChange === undefined) return;
|
||||
|
||||
const valueTextPair = jQuery(`#${this.select.id}`).children(':selected').map(function element() {
|
||||
@ -185,43 +214,28 @@ const Selection = React.createClass({
|
||||
id: e.target.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
getLabel: function getLabel(item) {
|
||||
if (this.props.field.getLabel !== undefined) {
|
||||
return this.props.field.getLabel(item, this.props.item);
|
||||
}
|
||||
return item.name;
|
||||
},
|
||||
getSearchLabel: function getSearchLabel(item) {
|
||||
if (this.props.field.getSearchLabel !== undefined) {
|
||||
return this.props.field.getSearchLabel(item, this.props.item);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getValue: function getValue(item) {
|
||||
if (this.props.field.getValue !== undefined) {
|
||||
return this.props.field.getValue(item, this.props.item);
|
||||
}
|
||||
return item.id;
|
||||
},
|
||||
};
|
||||
|
||||
// When it's impossible to represent the desired value in DOM,
|
||||
// this function may be used to transform the placeholder value into
|
||||
// desired value.
|
||||
transformChangedValue: function transformChangedValue(value, textValuePair) {
|
||||
transformChangedValue = (value, textValuePair) => {
|
||||
if (typeof this.props.field.transformChangedValue === 'function') {
|
||||
return this.props.field.transformChangedValue.call(this, value, textValuePair);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
insertEmptyOption: function insertEmptyOption() {
|
||||
};
|
||||
|
||||
insertEmptyOption = () => {
|
||||
// https://select2.org/placeholders
|
||||
// For single selects only, in order for the placeholder value to appear,
|
||||
// we must have a blank <option> as the first option in the <select> control.
|
||||
if (this.allowMultipleValues()) return undefined;
|
||||
if (this.props.field.placeholder) return (<option className="default" />);
|
||||
return undefined;
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
const items = this.getItems(this.props.field);
|
||||
const selectedValues = this.getSelectedValues();
|
||||
const options = items.map((item) => {
|
||||
@ -255,7 +269,42 @@ const Selection = React.createClass({
|
||||
{ options }
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Selection.propTypes = {
|
||||
onValueChange: PropTypes.func,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
getLabel: PropTypes.func,
|
||||
resetSelect2OnUpdate: PropTypes.bool,
|
||||
selected: PropTypes.func,
|
||||
endpoint: PropTypes.string,
|
||||
filter: PropTypes.func,
|
||||
getSearchLabel: PropTypes.func,
|
||||
getValue: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
remoteQuery: PropTypes.object,
|
||||
extendSelect2Options: PropTypes.object,
|
||||
multiple: PropTypes.bool,
|
||||
forceSelect2: PropTypes.bool,
|
||||
transformChangedValue: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
validation: PropTypes.object,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
disabled: PropTypes.bool,
|
||||
width: PropTypes.string,
|
||||
};
|
||||
|
||||
Selection.defaultProps = {
|
||||
onValueChange: function onValueChange() {
|
||||
// no-op
|
||||
},
|
||||
});
|
||||
disabled: false,
|
||||
width: '',
|
||||
};
|
||||
|
||||
|
||||
export default Selection;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormFieldText = React.createClass({
|
||||
class FormFieldText extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
|
||||
render() {
|
||||
const name = this.props.field.name || null;
|
||||
const item = this.props.item || {};
|
||||
@ -51,7 +52,31 @@ const FormFieldText = React.createClass({
|
||||
{...this.props.field.validation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormFieldText.propTypes = {
|
||||
onValueChange: PropTypes.func,
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
class: PropTypes.string,
|
||||
size: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
disabled: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
validation: PropTypes.object,
|
||||
}).isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
FormFieldText.defaultProps = {
|
||||
onValueChange: function onValueChange() {
|
||||
// no-op
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = FormFieldText;
|
||||
|
@ -3,30 +3,43 @@ import MailPoet from 'mailpoet';
|
||||
import classNames from 'classnames';
|
||||
import FormField from 'form/fields/field.jsx';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Form = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
getDefaultProps: function getDefaultProps() {
|
||||
return {
|
||||
params: {},
|
||||
};
|
||||
},
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
errors: [],
|
||||
item: {},
|
||||
};
|
||||
},
|
||||
getValues: function getValues() {
|
||||
return this.props.item ? this.props.item : this.state.item;
|
||||
},
|
||||
getErrors: function getErrors() {
|
||||
return this.props.errors ? this.props.errors : this.state.errors;
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
class Form extends React.Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
params: {},
|
||||
errors: undefined,
|
||||
fields: undefined,
|
||||
item: undefined,
|
||||
onItemLoad: undefined,
|
||||
isValid: undefined,
|
||||
onSuccess: undefined,
|
||||
onChange: undefined,
|
||||
loading: false,
|
||||
beforeFormContent: undefined,
|
||||
afterFormContent: undefined,
|
||||
children: undefined,
|
||||
id: '',
|
||||
onSubmit: undefined,
|
||||
automationId: '',
|
||||
messages: {
|
||||
onUpdate: () => { /* no-op */ },
|
||||
onCreate: () => { /* no-op */ },
|
||||
},
|
||||
endpoint: undefined,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
errors: [],
|
||||
item: {},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.params.id !== undefined) {
|
||||
this.loadItem(this.props.params.id);
|
||||
} else {
|
||||
@ -36,8 +49,9 @@ const Form = React.createClass({
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
componentWillReceiveProps: function componentWillReceiveProps(props) {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.params.id === undefined) {
|
||||
setImmediate(() => {
|
||||
this.setState({
|
||||
@ -49,10 +63,16 @@ const Form = React.createClass({
|
||||
this.form.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
loadItem: function loadItem(id) {
|
||||
}
|
||||
|
||||
getValues = () => this.props.item || this.state.item;
|
||||
|
||||
getErrors = () => this.props.errors || this.state.errors;
|
||||
|
||||
loadItem = (id) => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
if (!this.props.endpoint) return;
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: this.props.endpoint,
|
||||
@ -76,8 +96,9 @@ const Form = React.createClass({
|
||||
this.context.router.push('/new');
|
||||
});
|
||||
});
|
||||
},
|
||||
handleSubmit: function handleSubmit(e) {
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// handle validation
|
||||
@ -105,6 +126,8 @@ const Form = React.createClass({
|
||||
item.id = this.props.params.id;
|
||||
}
|
||||
|
||||
if (!this.props.endpoint) return;
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: this.props.endpoint,
|
||||
@ -129,8 +152,9 @@ const Form = React.createClass({
|
||||
this.setState({ errors: response.errors });
|
||||
}
|
||||
});
|
||||
},
|
||||
handleValueChange: function handleValueChange(e) {
|
||||
};
|
||||
|
||||
handleValueChange = (e) => {
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(e);
|
||||
}
|
||||
@ -143,8 +167,9 @@ const Form = React.createClass({
|
||||
item,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
let errors;
|
||||
if (this.getErrors() !== undefined) {
|
||||
errors = this.getErrors().map(error => (
|
||||
@ -231,7 +256,32 @@ const Form = React.createClass({
|
||||
{ afterFormContent }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Form.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
endpoint: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.object),
|
||||
messages: PropTypes.shape({
|
||||
onUpdate: PropTypes.func,
|
||||
onCreate: PropTypes.func,
|
||||
}).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
children: PropTypes.array, // eslint-disable-line react/forbid-prop-types
|
||||
id: PropTypes.string,
|
||||
automationId: PropTypes.string,
|
||||
beforeFormContent: PropTypes.func,
|
||||
afterFormContent: PropTypes.func,
|
||||
onItemLoad: PropTypes.func,
|
||||
isValid: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onSuccess: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
@ -672,7 +672,7 @@ WysijaForm = {
|
||||
if (type === undefined) type = 'block';
|
||||
// identify element
|
||||
id = element.identify();
|
||||
instance = WysijaForm.instances[id] || new WysijaForm[type.capitalize().camelize()](id);
|
||||
instance = WysijaForm.instances[id] || new (WysijaForm[type.capitalize().camelize()])(id);
|
||||
|
||||
WysijaForm.instances[id] = instance;
|
||||
return instance;
|
||||
|
@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createHashHistory } from 'history';
|
||||
import FormList from './list.jsx';
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
const App = React.createClass({
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const container = document.getElementById('forms_container');
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import jQuery from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import Listing from '../listing/listing.jsx';
|
||||
|
||||
const columns = [
|
||||
@ -122,8 +123,8 @@ const itemActions = [
|
||||
},
|
||||
];
|
||||
|
||||
const FormList = React.createClass({
|
||||
createForm() {
|
||||
class FormList extends React.Component {
|
||||
createForm = () => {
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'forms',
|
||||
@ -138,8 +139,9 @@ const FormList = React.createClass({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
renderItem(form, actions) {
|
||||
};
|
||||
|
||||
renderItem = (form, actions) => {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
@ -177,7 +179,8 @@ const FormList = React.createClass({
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
@ -204,7 +207,12 @@ const FormList = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FormList.propTypes = {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
module.exports = FormList;
|
||||
|
@ -151,6 +151,16 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
|
||||
case 'Times New Roman': return new Handlebars.SafeString("'Times New Roman', Times, Baskerville, Georgia, serif");
|
||||
case 'Trebuchet MS': return new Handlebars.SafeString("'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif");
|
||||
case 'Verdana': return new Handlebars.SafeString('Verdana, Geneva, sans-serif');
|
||||
case 'Arvo': return new Handlebars.SafeString('arvo, courier, georgia, serif');
|
||||
case 'Lato': return new Handlebars.SafeString("lato, 'helvetica neue', helvetica, arial, sans-serif");
|
||||
case 'Lora': return new Handlebars.SafeString("lora, georgia, 'times new roman', serif");
|
||||
case 'Merriweather': return new Handlebars.SafeString("merriweather, georgia, 'times new roman', serif");
|
||||
case 'Merriweather Sans': return new Handlebars.SafeString("'merriweather sans', 'helvetica neue', helvetica, arial, sans-serif");
|
||||
case 'Noticia Text': return new Handlebars.SafeString("'noticia text', georgia, 'times new roman', serif");
|
||||
case 'Open Sans': return new Handlebars.SafeString("'open sans', 'helvetica neue', helvetica, arial, sans-serif");
|
||||
case 'Playfair Display': return new Handlebars.SafeString("playfair display, georgia, 'times new roman', serif");
|
||||
case 'Roboto': return new Handlebars.SafeString("roboto, 'helvetica neue', helvetica, arial, sans-serif");
|
||||
case 'Source Sans Pro': return new Handlebars.SafeString("'source sans pro', 'helvetica neue', helvetica, arial, sans-serif");
|
||||
default: return font;
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import ReactHtmlParser from 'react-html-parser';
|
||||
@ -47,10 +48,10 @@ function Tooltip(props) {
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
tooltipId: React.PropTypes.string,
|
||||
tooltip: React.PropTypes.node.isRequired,
|
||||
place: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
tooltipId: PropTypes.string,
|
||||
tooltip: PropTypes.node.isRequired,
|
||||
place: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Tooltip.defaultProps = {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import KeyValueTable from 'common/key_value_table.jsx';
|
||||
import PrintBoolean from 'common/print_boolean.jsx';
|
||||
@ -46,12 +47,12 @@ const CronStatus = (props) => {
|
||||
};
|
||||
|
||||
CronStatus.propTypes = {
|
||||
status_data: React.PropTypes.shape({
|
||||
accessible: React.PropTypes.bool,
|
||||
status: React.PropTypes.string,
|
||||
updated_at: React.PropTypes.number,
|
||||
run_accessed_at: React.PropTypes.number,
|
||||
run_completed_at: React.PropTypes.number,
|
||||
status_data: PropTypes.shape({
|
||||
accessible: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
updated_at: PropTypes.number,
|
||||
run_accessed_at: PropTypes.number,
|
||||
run_completed_at: PropTypes.number,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
@ -2,18 +2,24 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router';
|
||||
import { createHashHistory } from 'history';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import SystemStatus from 'help/system_status.jsx';
|
||||
import SystemInfo from 'help/system_info.jsx';
|
||||
import KnowledgeBase from 'help/knowledge_base.jsx';
|
||||
import SystemInfo from 'help/system_info.jsx';
|
||||
import SystemStatus from 'help/system_status.jsx';
|
||||
import YourPrivacy from 'help/your_privacy.jsx';
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
const App = React.createClass({
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const container = document.getElementById('help_container');
|
||||
|
||||
@ -26,6 +32,7 @@ if (container) {
|
||||
<Route path="knowledgeBase(/)**" params={{ tab: 'knowledgeBase' }} component={KnowledgeBase} />
|
||||
<Route path="systemStatus(/)**" params={{ tab: 'systemStatus' }} component={SystemStatus} />
|
||||
<Route path="systemInfo(/)**" params={{ tab: 'systemInfo' }} component={SystemInfo} />
|
||||
<Route path="yourPrivacy(/)**" params={{ tab: 'yourPrivacy' }} component={YourPrivacy} />
|
||||
</Route>
|
||||
</Router>
|
||||
), container);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import KeyValueTable from 'common/key_value_table.jsx';
|
||||
import TasksList from './tasks_list/tasks_list.jsx';
|
||||
@ -58,23 +59,23 @@ const QueueStatus = (props) => {
|
||||
};
|
||||
|
||||
QueueStatus.propTypes = {
|
||||
status_data: React.PropTypes.shape({
|
||||
status: React.PropTypes.string,
|
||||
started: React.PropTypes.number,
|
||||
sent: React.PropTypes.number,
|
||||
retry_attempt: React.PropTypes.number,
|
||||
retry_at: React.PropTypes.number,
|
||||
error: React.PropTypes.shape({
|
||||
operation: React.PropTypes.string,
|
||||
error_message: React.PropTypes.string,
|
||||
status_data: PropTypes.shape({
|
||||
status: PropTypes.string,
|
||||
started: PropTypes.number,
|
||||
sent: PropTypes.number,
|
||||
retry_attempt: PropTypes.number,
|
||||
retry_at: PropTypes.number,
|
||||
error: PropTypes.shape({
|
||||
operation: PropTypes.string,
|
||||
error_message: PropTypes.string,
|
||||
}),
|
||||
tasksStatusCounts: React.PropTypes.shape({
|
||||
completed: React.PropTypes.number.isRequired,
|
||||
running: React.PropTypes.number.isRequired,
|
||||
paused: React.PropTypes.number.isRequired,
|
||||
scheduled: React.PropTypes.number.isRequired,
|
||||
tasksStatusCounts: PropTypes.shape({
|
||||
completed: PropTypes.number.isRequired,
|
||||
running: PropTypes.number.isRequired,
|
||||
paused: PropTypes.number.isRequired,
|
||||
scheduled: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
latestTasks: React.PropTypes.arrayOf(TasksListDataRow.propTypes.task).isRequired,
|
||||
latestTasks: PropTypes.arrayOf(TasksListDataRow.propTypes.task).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
@ -19,6 +20,11 @@ const tabs = [
|
||||
label: MailPoet.I18n.t('tabSystemInfoTitle'),
|
||||
link: '/systemInfo',
|
||||
},
|
||||
{
|
||||
name: 'yourPrivacy',
|
||||
label: MailPoet.I18n.t('tabYourPrivacyTitle'),
|
||||
link: '/yourPrivacy',
|
||||
},
|
||||
];
|
||||
|
||||
function Tabs(props) {
|
||||
@ -44,7 +50,7 @@ function Tabs(props) {
|
||||
);
|
||||
}
|
||||
|
||||
Tabs.propTypes = { tab: React.PropTypes.string };
|
||||
Tabs.propTypes = { tab: PropTypes.string };
|
||||
Tabs.defaultProps = { tab: 'knowledgeBase' };
|
||||
|
||||
module.exports = Tabs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import TaskListDataRow from './tasks_list_data_row.jsx';
|
||||
@ -30,8 +31,8 @@ const TasksList = (props) => {
|
||||
};
|
||||
|
||||
TasksList.propTypes = {
|
||||
show_scheduled_at: React.PropTypes.bool,
|
||||
tasks: React.PropTypes.arrayOf(TaskListDataRow.propTypes.task).isRequired,
|
||||
show_scheduled_at: PropTypes.bool,
|
||||
tasks: PropTypes.arrayOf(TaskListDataRow.propTypes.task).isRequired,
|
||||
};
|
||||
|
||||
TasksList.defaultProps = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
@ -36,19 +37,19 @@ const TasksListDataRow = props => (
|
||||
);
|
||||
|
||||
TasksListDataRow.propTypes = {
|
||||
show_scheduled_at: React.PropTypes.bool,
|
||||
task: React.PropTypes.shape({
|
||||
id: React.PropTypes.number.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
priority: React.PropTypes.number.isRequired,
|
||||
updated_at: React.PropTypes.number.isRequired,
|
||||
scheduled_at: React.PropTypes.number,
|
||||
status: React.PropTypes.string,
|
||||
newsletter: React.PropTypes.shape({
|
||||
newsletter_id: React.PropTypes.number.isRequired,
|
||||
queue_id: React.PropTypes.number.isRequired,
|
||||
preview_url: React.PropTypes.string.isRequired,
|
||||
subject: React.PropTypes.string,
|
||||
show_scheduled_at: PropTypes.bool,
|
||||
task: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
updated_at: PropTypes.number.isRequired,
|
||||
scheduled_at: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
newsletter: PropTypes.shape({
|
||||
newsletter_id: PropTypes.number.isRequired,
|
||||
queue_id: PropTypes.number.isRequired,
|
||||
preview_url: PropTypes.string.isRequired,
|
||||
subject: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
@ -13,7 +14,7 @@ const TasksListLabelsRow = props => (
|
||||
);
|
||||
|
||||
TasksListLabelsRow.propTypes = {
|
||||
show_scheduled_at: React.PropTypes.bool,
|
||||
show_scheduled_at: PropTypes.bool,
|
||||
};
|
||||
|
||||
TasksListLabelsRow.defaultProps = {
|
||||
|
20
assets/js/src/help/your_privacy.jsx
Normal file
20
assets/js/src/help/your_privacy.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
import Tabs from './tabs.jsx';
|
||||
|
||||
function YourPrivacy() {
|
||||
return (
|
||||
<div>
|
||||
<Tabs tab="yourPrivacy" />
|
||||
|
||||
<p>{MailPoet.I18n.t('yourPrivacyContent1')}</p>
|
||||
<p>{MailPoet.I18n.t('yourPrivacyContent2')}</p>
|
||||
<p>{MailPoet.I18n.t('yourPrivacyContent3')}</p>
|
||||
|
||||
<a target="_blank" rel="noreferrer noopener" href="https://www.mailpoet.com/privacy-notice/" className="button button-primary">{MailPoet.I18n.t('yourPrivacyButton')}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = YourPrivacy;
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import InAppAnnouncementDot from './in_app_announcement_dot.jsx';
|
||||
@ -78,11 +79,11 @@ const validateBooleanWithWindowDependency = (props, propName, componentName, win
|
||||
};
|
||||
|
||||
InAppAnnouncement.propTypes = {
|
||||
width: React.PropTypes.string,
|
||||
height: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
validUntil: React.PropTypes.instanceOf(Date),
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.element.isRequired,
|
||||
validUntil: PropTypes.instanceOf(Date),
|
||||
showToNewUser: (props, propName, componentName) => (
|
||||
validateBooleanWithWindowDependency(props, propName, componentName, 'mailpoet_is_new_user')
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import classNames from 'classnames';
|
||||
@ -20,11 +21,11 @@ const InAppAnnouncementDot = props => (
|
||||
);
|
||||
|
||||
InAppAnnouncementDot.propTypes = {
|
||||
children: React.PropTypes.element.isRequired,
|
||||
width: React.PropTypes.string,
|
||||
height: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
onUserTrigger: React.PropTypes.func,
|
||||
children: PropTypes.element.isRequired,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onUserTrigger: PropTypes.func,
|
||||
};
|
||||
|
||||
InAppAnnouncementDot.defaultProps = {
|
||||
|
@ -1,29 +1,26 @@
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ListingBulkActions = React.createClass({
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
action: false,
|
||||
extra: false,
|
||||
};
|
||||
},
|
||||
handleChangeAction: function handleChangeAction(e) {
|
||||
this.setState({
|
||||
action: e.target.value,
|
||||
extra: false,
|
||||
}, () => {
|
||||
const action = this.getSelectedAction();
|
||||
class ListingBulkActions extends React.Component {
|
||||
state = {
|
||||
action: false,
|
||||
extra: false,
|
||||
};
|
||||
|
||||
// action on select callback
|
||||
if (action !== null && action.onSelect !== undefined) {
|
||||
this.setState({
|
||||
extra: action.onSelect(e),
|
||||
});
|
||||
getSelectedAction = () => {
|
||||
const selectedAction = this.action.value;
|
||||
if (selectedAction.length > 0) {
|
||||
const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
|
||||
|
||||
if (action.length > 0) {
|
||||
return action[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
handleApplyAction: function handleApplyAction(e) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
handleApplyAction = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const action = this.getSelectedAction();
|
||||
@ -58,19 +55,25 @@ const ListingBulkActions = React.createClass({
|
||||
action: false,
|
||||
extra: false,
|
||||
});
|
||||
},
|
||||
getSelectedAction: function getSelectedAction() {
|
||||
const selectedAction = this.action.value;
|
||||
if (selectedAction.length > 0) {
|
||||
const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
|
||||
};
|
||||
|
||||
if (action.length > 0) {
|
||||
return action[0];
|
||||
handleChangeAction = (e) => {
|
||||
this.setState({
|
||||
action: e.target.value,
|
||||
extra: false,
|
||||
}, () => {
|
||||
const action = this.getSelectedAction();
|
||||
|
||||
// action on select callback
|
||||
if (action !== null && action.onSelect !== undefined) {
|
||||
this.setState({
|
||||
extra: action.onSelect(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
render: function render() {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.bulk_actions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@ -108,7 +111,17 @@ const ListingBulkActions = React.createClass({
|
||||
{ this.state.extra }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ListingBulkActions.propTypes = {
|
||||
bulk_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onBulkAction: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ListingBulkActions;
|
||||
|
@ -1,32 +1,10 @@
|
||||
import React from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ListingFilters = React.createClass({
|
||||
handleFilterAction: function handleFilterAction() {
|
||||
const filters = {};
|
||||
this.getAvailableFilters().forEach((filter, i) => {
|
||||
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
|
||||
});
|
||||
if (this.props.onBeforeSelectFilter) {
|
||||
this.props.onBeforeSelectFilter(filters);
|
||||
}
|
||||
return this.props.onSelectFilter(filters);
|
||||
},
|
||||
handleEmptyTrash: function handleEmptyTrash() {
|
||||
return this.props.onEmptyTrash();
|
||||
},
|
||||
getAvailableFilters: function getAvailableFilters() {
|
||||
const filters = this.props.filters;
|
||||
return Object.keys(filters).filter(filter => !(
|
||||
filters[filter].length === 0
|
||||
|| (
|
||||
filters[filter].length === 1
|
||||
&& !filters[filter][0].value
|
||||
)
|
||||
));
|
||||
},
|
||||
componentDidUpdate: function componentDidUpdate() {
|
||||
class ListingFilters extends React.Component {
|
||||
componentDidUpdate() {
|
||||
const selectedFilters = this.props.filter;
|
||||
this.getAvailableFilters().forEach(
|
||||
(filter, i) => {
|
||||
@ -37,8 +15,33 @@ const ListingFilters = React.createClass({
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
}
|
||||
|
||||
getAvailableFilters = () => {
|
||||
const filters = this.props.filters;
|
||||
return Object.keys(filters).filter(filter => !(
|
||||
filters[filter].length === 0
|
||||
|| (
|
||||
filters[filter].length === 1
|
||||
&& !filters[filter][0].value
|
||||
)
|
||||
));
|
||||
};
|
||||
|
||||
handleEmptyTrash = () => this.props.onEmptyTrash();
|
||||
|
||||
handleFilterAction = () => {
|
||||
const filters = {};
|
||||
this.getAvailableFilters().forEach((filter, i) => {
|
||||
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
|
||||
});
|
||||
if (this.props.onBeforeSelectFilter) {
|
||||
this.props.onBeforeSelectFilter(filters);
|
||||
}
|
||||
return this.props.onSelectFilter(filters);
|
||||
};
|
||||
|
||||
render() {
|
||||
const filters = this.props.filters;
|
||||
const availableFilters = this.getAvailableFilters()
|
||||
.map((filter, i) => (
|
||||
@ -89,7 +92,23 @@ const ListingFilters = React.createClass({
|
||||
{ emptyTrash }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ListingFilters.propTypes = {
|
||||
filters: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
]).isRequired,
|
||||
onEmptyTrash: PropTypes.func.isRequired,
|
||||
onBeforeSelectFilter: PropTypes.func,
|
||||
onSelectFilter: PropTypes.func.isRequired,
|
||||
filter: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ListingFilters.defaultProps = {
|
||||
onBeforeSelectFilter: undefined,
|
||||
};
|
||||
|
||||
export default ListingFilters;
|
||||
|
@ -32,6 +32,7 @@ class ListingGroups extends React.Component {
|
||||
data-automation-id={`filters_${group.label.replace(' ', '_').toLowerCase()}`}
|
||||
>
|
||||
{group.label}
|
||||
|
||||
<span className="count">({ parseInt(group.count, 10).toLocaleString() })</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,14 +1,12 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListingColumn from './listing_column.jsx';
|
||||
|
||||
const ListingHeader = React.createClass({
|
||||
handleSelectItems: function handleSelectItems() {
|
||||
return this.props.onSelectItems(
|
||||
this.toggle.checked
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
class ListingHeader extends React.Component {
|
||||
handleSelectItems = () => this.props.onSelectItems(this.toggle.checked);
|
||||
|
||||
render() {
|
||||
const columns = this.props.columns.map((column, index) => {
|
||||
const renderColumn = column;
|
||||
renderColumn.is_primary = (index === 0);
|
||||
@ -53,49 +51,26 @@ const ListingHeader = React.createClass({
|
||||
{columns}
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ListingColumn = React.createClass({
|
||||
handleSort: function handleSort() {
|
||||
const sortBy = this.props.column.name;
|
||||
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
|
||||
this.props.onSort(sortBy, sortOrder);
|
||||
},
|
||||
render: function render() {
|
||||
const classes = classNames(
|
||||
'manage-column',
|
||||
{ 'column-primary': this.props.column.is_primary },
|
||||
{ sortable: this.props.column.sortable },
|
||||
this.props.column.sorted,
|
||||
{ sorted: (this.props.sort_by === this.props.column.name) }
|
||||
);
|
||||
let label;
|
||||
ListingHeader.propTypes = {
|
||||
onSelectItems: PropTypes.func.isRequired,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object),
|
||||
sort_by: PropTypes.string,
|
||||
sort_order: PropTypes.string,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
if (this.props.column.sortable === true) {
|
||||
label = (
|
||||
<a
|
||||
onClick={this.handleSort}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span>{ this.props.column.label }</span>
|
||||
<span className="sorting-indicator" />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
label = this.props.column.label;
|
||||
}
|
||||
return (
|
||||
<th
|
||||
role="columnheader"
|
||||
className={classes}
|
||||
id={this.props.column.name}
|
||||
scope="col"
|
||||
width={this.props.column.width || null}
|
||||
>{label}</th>
|
||||
);
|
||||
},
|
||||
});
|
||||
ListingHeader.defaultProps = {
|
||||
columns: [],
|
||||
sort_by: undefined,
|
||||
sort_order: 'desc',
|
||||
};
|
||||
|
||||
module.exports = ListingHeader;
|
||||
|
@ -1,305 +1,76 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import jQuery from 'jquery';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import _ from 'underscore';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ListingBulkActions from 'listing/bulk_actions.jsx';
|
||||
import ListingHeader from 'listing/header.jsx';
|
||||
import ListingPages from 'listing/pages.jsx';
|
||||
import ListingSearch from 'listing/search.jsx';
|
||||
import ListingGroups from 'listing/groups.jsx';
|
||||
import ListingFilters from 'listing/filters.jsx';
|
||||
import ListingItems from 'listing/listing_items.jsx';
|
||||
|
||||
const ListingItem = React.createClass({
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
expanded: false,
|
||||
};
|
||||
const Listing = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
displayName: 'Listing',
|
||||
|
||||
/* eslint-disable react/require-default-props */
|
||||
propTypes: {
|
||||
limit: PropTypes.number,
|
||||
sort_by: PropTypes.string,
|
||||
sort_order: PropTypes.string,
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
auto_refresh: PropTypes.bool,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}),
|
||||
base_url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
endpoint: PropTypes.string.isRequired,
|
||||
afterGetItems: PropTypes.func,
|
||||
messages: PropTypes.shape({
|
||||
onRestore: PropTypes.func,
|
||||
onTrash: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
}),
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object),
|
||||
bulk_actions: PropTypes.arrayOf(PropTypes.object),
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object),
|
||||
search: PropTypes.bool,
|
||||
groups: PropTypes.bool,
|
||||
renderExtraActions: PropTypes.func,
|
||||
onBeforeSelectFilter: PropTypes.func,
|
||||
getListingItemKey: PropTypes.func,
|
||||
},
|
||||
handleSelectItem: function handleSelectItem(e) {
|
||||
this.props.onSelectItem(
|
||||
parseInt(e.target.value, 10),
|
||||
e.target.checked
|
||||
);
|
||||
/* eslint-enable react/require-default-props */
|
||||
|
||||
return !e.target.checked;
|
||||
},
|
||||
handleRestoreItem: function handleRestoreItem(id) {
|
||||
this.props.onRestoreItem(id);
|
||||
},
|
||||
handleTrashItem: function handleTrashItem(id) {
|
||||
this.props.onTrashItem(id);
|
||||
},
|
||||
handleDeleteItem: function handleDeleteItem(id) {
|
||||
this.props.onDeleteItem(id);
|
||||
},
|
||||
handleToggleItem: function handleToggleItem() {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
},
|
||||
render: function render() {
|
||||
let checkbox = false;
|
||||
|
||||
if (this.props.is_selectable === true) {
|
||||
checkbox = (
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
|
||||
`Select ${this.props.item[this.props.columns[0].name]}`
|
||||
}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={this.props.item.id}
|
||||
checked={
|
||||
this.props.item.selected || this.props.selection === 'all'
|
||||
}
|
||||
onChange={this.handleSelectItem}
|
||||
disabled={this.props.selection === 'all'}
|
||||
id={`listing-row-checkbox-${this.props.item.id}`}
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
const customActions = this.props.item_actions;
|
||||
let itemActions = false;
|
||||
|
||||
if (customActions.length > 0) {
|
||||
let isFirst = true;
|
||||
itemActions = customActions
|
||||
.filter(action => action.display === undefined || action.display(this.props.item))
|
||||
.map((action, index) => {
|
||||
let customAction = null;
|
||||
|
||||
if (action.name === 'trash') {
|
||||
customAction = (
|
||||
<span key={`action-${action.name}`} className="trash">
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleTrashItem(this.props.item.id)}
|
||||
>
|
||||
{MailPoet.I18n.t('moveToTrash')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
} else if (action.refresh) {
|
||||
customAction = (
|
||||
<span
|
||||
onClick={this.props.onRefreshItems}
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
role="button"
|
||||
tabIndex={index}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else if (action.link) {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={
|
||||
(action.onClick !== undefined)
|
||||
? () => action.onClick(this.props.item, this.props.onRefreshItems)
|
||||
: false
|
||||
}
|
||||
>{ action.label }</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (customAction !== null && isFirst === true) {
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return customAction;
|
||||
});
|
||||
} else {
|
||||
itemActions = (
|
||||
<span className="edit">
|
||||
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let actions;
|
||||
|
||||
if (this.props.group === 'trash') {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
<span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleRestoreItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('restore')}</a>
|
||||
</span>
|
||||
{ ' | ' }
|
||||
<span className="delete">
|
||||
<a
|
||||
className="submitdelete"
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleDeleteItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('deletePermanently')}</a>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
{ itemActions }
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
|
||||
|
||||
return (
|
||||
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
|
||||
{ checkbox }
|
||||
{ this.props.onRenderItem(this.props.item, actions) }
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const ListingItems = React.createClass({
|
||||
render: function render() {
|
||||
if (this.props.items.length === 0) {
|
||||
let message;
|
||||
if (this.props.loading === true) {
|
||||
message = (this.props.messages.onLoadingItems
|
||||
&& this.props.messages.onLoadingItems(this.props.group))
|
||||
|| MailPoet.I18n.t('loadingItems');
|
||||
} else {
|
||||
message = (this.props.messages.onNoItemsFound
|
||||
&& this.props.messages.onNoItemsFound(this.props.group))
|
||||
|| MailPoet.I18n.t('noItemsFound');
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className="no-items">
|
||||
<td
|
||||
colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
className="colspanchange"
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
const selectAllClasses = classNames(
|
||||
'mailpoet_select_all',
|
||||
{ mailpoet_hidden: (
|
||||
this.props.selection === false
|
||||
|| (this.props.count <= this.props.limit)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className={selectAllClasses}>
|
||||
<td colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
>
|
||||
{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLabel')
|
||||
: MailPoet.I18n.t('selectedAllLabel').replace(
|
||||
'%d',
|
||||
this.props.count.toLocaleString()
|
||||
)
|
||||
}
|
||||
|
||||
<a
|
||||
onClick={this.props.onSelectAll}
|
||||
href="javascript:;"
|
||||
>{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLink')
|
||||
: MailPoet.I18n.t('clearSelection')
|
||||
}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.props.items.map((item) => {
|
||||
const renderItem = item;
|
||||
renderItem.id = parseInt(item.id, 10);
|
||||
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
|
||||
let key = `item-${renderItem.id}-${item.id}`;
|
||||
if (typeof this.props.getListingItemKey === 'function') {
|
||||
key = this.props.getListingItemKey(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListingItem
|
||||
columns={this.props.columns}
|
||||
onSelectItem={this.props.onSelectItem}
|
||||
onRenderItem={this.props.onRenderItem}
|
||||
onDeleteItem={this.props.onDeleteItem}
|
||||
onRestoreItem={this.props.onRestoreItem}
|
||||
onTrashItem={this.props.onTrashItem}
|
||||
onRefreshItems={this.props.onRefreshItems}
|
||||
selection={this.props.selection}
|
||||
is_selectable={this.props.is_selectable}
|
||||
item_actions={this.props.item_actions}
|
||||
group={this.props.group}
|
||||
key={key}
|
||||
item={renderItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const Listing = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
router: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: () => ({
|
||||
limit: 10,
|
||||
sort_by: null,
|
||||
sort_order: undefined,
|
||||
auto_refresh: true,
|
||||
location: undefined,
|
||||
base_url: '',
|
||||
type: undefined,
|
||||
afterGetItems: undefined,
|
||||
messages: undefined,
|
||||
columns: [],
|
||||
bulk_actions: [],
|
||||
item_actions: [],
|
||||
search: true,
|
||||
groups: true,
|
||||
renderExtraActions: undefined,
|
||||
onBeforeSelectFilter: undefined,
|
||||
getListingItemKey: undefined,
|
||||
}),
|
||||
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
@ -319,63 +90,28 @@ const Listing = React.createClass({
|
||||
meta: {},
|
||||
};
|
||||
},
|
||||
getParam: function getParam(param) {
|
||||
const regex = /(.*)\[(.*)\]/;
|
||||
const matches = regex.exec(param);
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
initWithParams: function initWithParams(params) {
|
||||
const state = this.getInitialState();
|
||||
// check for url params
|
||||
if (params.splat) {
|
||||
params.splat.split('/').forEach((param) => {
|
||||
const [key, value] = this.getParam(param);
|
||||
const filters = {};
|
||||
switch (key) {
|
||||
case 'filter':
|
||||
value.split('&').forEach((pair) => {
|
||||
const [k, v] = pair.split('=');
|
||||
filters[k] = v;
|
||||
});
|
||||
|
||||
state.filter = filters;
|
||||
break;
|
||||
default:
|
||||
state[key] = value;
|
||||
}
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.isComponentMounted = true;
|
||||
const params = this.props.params || {};
|
||||
this.initWithParams(params);
|
||||
|
||||
if (this.props.auto_refresh) {
|
||||
jQuery(document).on('heartbeat-tick.mailpoet', () => {
|
||||
this.getItems();
|
||||
});
|
||||
}
|
||||
|
||||
// limit per page
|
||||
if (this.props.limit !== undefined) {
|
||||
state.limit = Math.abs(Number(this.props.limit));
|
||||
}
|
||||
|
||||
// sort by
|
||||
if (state.sort_by === null && this.props.sort_by !== undefined) {
|
||||
state.sort_by = this.props.sort_by;
|
||||
}
|
||||
|
||||
// sort order
|
||||
if (state.sort_order === null && this.props.sort_order !== undefined) {
|
||||
state.sort_order = this.props.sort_order;
|
||||
}
|
||||
|
||||
this.setState(state, () => {
|
||||
this.getItems();
|
||||
});
|
||||
},
|
||||
getParams: function getParams() {
|
||||
// get all route parameters (without the "splat")
|
||||
const params = _.omit(this.props.params, 'splat');
|
||||
// TODO:
|
||||
// find a way to set the "type" in the routes definition
|
||||
// so that it appears in `this.props.params`
|
||||
if (this.props.type) {
|
||||
params.type = this.props.type;
|
||||
}
|
||||
return params;
|
||||
|
||||
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
|
||||
const params = nextProps.params || {};
|
||||
this.initWithParams(params);
|
||||
},
|
||||
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
this.isComponentMounted = false;
|
||||
},
|
||||
|
||||
setParams: function setParams() {
|
||||
if (this.props.location) {
|
||||
const params = Object.keys(this.state)
|
||||
@ -413,17 +149,19 @@ const Listing = React.createClass({
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getUrlWithParams: function getUrlWithParams(params) {
|
||||
let baseUrl = (this.props.base_url !== undefined)
|
||||
? this.props.base_url
|
||||
: null;
|
||||
|
||||
if (baseUrl !== null) {
|
||||
if (baseUrl) {
|
||||
baseUrl = this.setBaseUrlParams(baseUrl);
|
||||
return `/${baseUrl}/${params}`;
|
||||
}
|
||||
return `/${params}`;
|
||||
},
|
||||
|
||||
setBaseUrlParams: function setBaseUrlParams(baseUrl) {
|
||||
let ret = baseUrl;
|
||||
if (ret.indexOf(':') !== -1) {
|
||||
@ -437,24 +175,25 @@ const Listing = React.createClass({
|
||||
|
||||
return ret;
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.isComponentMounted = true;
|
||||
const params = this.props.params || {};
|
||||
this.initWithParams(params);
|
||||
|
||||
if (this.props.auto_refresh) {
|
||||
jQuery(document).on('heartbeat-tick.mailpoet', () => {
|
||||
this.getItems();
|
||||
});
|
||||
getParams: function getParams() {
|
||||
// get all route parameters (without the "splat")
|
||||
const params = _.omit(this.props.params, 'splat');
|
||||
// TODO:
|
||||
// find a way to set the "type" in the routes definition
|
||||
// so that it appears in `this.props.params`
|
||||
if (this.props.type) {
|
||||
params.type = this.props.type;
|
||||
}
|
||||
return params;
|
||||
},
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
this.isComponentMounted = false;
|
||||
},
|
||||
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
|
||||
const params = nextProps.params || {};
|
||||
this.initWithParams(params);
|
||||
|
||||
getParam: function getParam(param) {
|
||||
const regex = /(.*)\[(.*)\]/;
|
||||
const matches = regex.exec(param);
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
getItems: function getItems() {
|
||||
if (!this.isComponentMounted) return;
|
||||
|
||||
@ -507,6 +246,49 @@ const Listing = React.createClass({
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
initWithParams: function initWithParams(params) {
|
||||
const state = this.getInitialState();
|
||||
// check for url params
|
||||
if (params.splat) {
|
||||
params.splat.split('/').forEach((param) => {
|
||||
const [key, value] = this.getParam(param);
|
||||
const filters = {};
|
||||
switch (key) {
|
||||
case 'filter':
|
||||
value.split('&').forEach((pair) => {
|
||||
const [k, v] = pair.split('=');
|
||||
filters[k] = v;
|
||||
});
|
||||
|
||||
state.filter = filters;
|
||||
break;
|
||||
default:
|
||||
state[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// limit per page
|
||||
if (this.props.limit !== undefined) {
|
||||
state.limit = Math.abs(Number(this.props.limit));
|
||||
}
|
||||
|
||||
// sort by
|
||||
if (state.sort_by === null && this.props.sort_by !== undefined) {
|
||||
state.sort_by = this.props.sort_by;
|
||||
}
|
||||
|
||||
// sort order
|
||||
if (state.sort_order === null && this.props.sort_order !== undefined) {
|
||||
state.sort_order = this.props.sort_order;
|
||||
}
|
||||
|
||||
this.setState(state, () => {
|
||||
this.getItems();
|
||||
});
|
||||
},
|
||||
|
||||
handleRestoreItem: function handleRestoreItem(id) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
@ -535,6 +317,7 @@ const Listing = React.createClass({
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
handleTrashItem: function handleTrashItem(id) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
@ -563,6 +346,7 @@ const Listing = React.createClass({
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
handleDeleteItem: function handleDeleteItem(id) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
@ -591,6 +375,7 @@ const Listing = React.createClass({
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
handleEmptyTrash: function handleEmptyTrash() {
|
||||
return this.handleBulkAction('all', {
|
||||
action: 'delete',
|
||||
@ -611,6 +396,7 @@ const Listing = React.createClass({
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
handleBulkAction: function handleBulkAction(selectedIds, params) {
|
||||
if (
|
||||
this.state.selection === false
|
||||
@ -651,6 +437,7 @@ const Listing = React.createClass({
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleSearch: function handleSearch(search) {
|
||||
this.setState({
|
||||
search,
|
||||
@ -661,6 +448,7 @@ const Listing = React.createClass({
|
||||
this.setParams();
|
||||
});
|
||||
},
|
||||
|
||||
handleSort: function handleSort(sortBy, sortOrder = 'asc') {
|
||||
this.setState({
|
||||
sort_by: sortBy,
|
||||
@ -669,6 +457,7 @@ const Listing = React.createClass({
|
||||
this.setParams();
|
||||
});
|
||||
},
|
||||
|
||||
handleSelectItem: function handleSelectItem(id, isChecked) {
|
||||
let selectedIds = this.state.selected_ids;
|
||||
let selection = false;
|
||||
@ -690,6 +479,7 @@ const Listing = React.createClass({
|
||||
selected_ids: selectedIds,
|
||||
});
|
||||
},
|
||||
|
||||
handleSelectItems: function handleSelectItems(isChecked) {
|
||||
if (isChecked === false) {
|
||||
this.clearSelection();
|
||||
@ -702,6 +492,7 @@ const Listing = React.createClass({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleSelectAll: function handleSelectAll() {
|
||||
if (this.state.selection === 'all') {
|
||||
this.clearSelection();
|
||||
@ -712,12 +503,14 @@ const Listing = React.createClass({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearSelection: function clearSelection() {
|
||||
this.setState({
|
||||
selection: false,
|
||||
selected_ids: [],
|
||||
});
|
||||
},
|
||||
|
||||
handleFilter: function handleFilter(filters) {
|
||||
this.setState({
|
||||
filter: filters,
|
||||
@ -726,6 +519,7 @@ const Listing = React.createClass({
|
||||
this.setParams();
|
||||
});
|
||||
},
|
||||
|
||||
handleGroup: function handleGroup(group) {
|
||||
// reset search
|
||||
jQuery('#search_input').val('');
|
||||
@ -739,6 +533,7 @@ const Listing = React.createClass({
|
||||
this.setParams();
|
||||
});
|
||||
},
|
||||
|
||||
handleSetPage: function handleSetPage(page) {
|
||||
this.setState({
|
||||
page,
|
||||
@ -748,13 +543,16 @@ const Listing = React.createClass({
|
||||
this.setParams();
|
||||
});
|
||||
},
|
||||
|
||||
handleRenderItem: function handleRenderItem(item, actions) {
|
||||
const render = this.props.onRenderItem(item, actions, this.state.meta);
|
||||
return render.props.children;
|
||||
},
|
||||
|
||||
handleRefreshItems: function handleRefreshItems() {
|
||||
this.getItems();
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
const items = this.state.items;
|
||||
const sortBy = this.state.sort_by;
|
||||
|
68
assets/js/src/listing/listing_column.jsx
Normal file
68
assets/js/src/listing/listing_column.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingColumn extends React.Component {
|
||||
handleSort = () => {
|
||||
const sortBy = this.props.column.name;
|
||||
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
|
||||
this.props.onSort(sortBy, sortOrder);
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = classNames(
|
||||
'manage-column',
|
||||
{ 'column-primary': this.props.column.is_primary },
|
||||
{ sortable: this.props.column.sortable },
|
||||
this.props.column.sorted,
|
||||
{ sorted: (this.props.sort_by === this.props.column.name) }
|
||||
);
|
||||
let label;
|
||||
|
||||
if (this.props.column.sortable === true) {
|
||||
label = (
|
||||
<a
|
||||
onClick={this.handleSort}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span>{ this.props.column.label }</span>
|
||||
<span className="sorting-indicator" />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
label = this.props.column.label;
|
||||
}
|
||||
return (
|
||||
<th
|
||||
role="columnheader"
|
||||
className={classes}
|
||||
id={this.props.column.name}
|
||||
scope="col"
|
||||
width={this.props.column.width || null}
|
||||
>{label}</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingColumn.propTypes = {
|
||||
column: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
sorted: PropTypes.string,
|
||||
is_primary: PropTypes.bool,
|
||||
sortable: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
}).isRequired,
|
||||
sort_by: PropTypes.string,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ListingColumn.defaultProps = {
|
||||
sort_by: undefined,
|
||||
};
|
||||
|
||||
module.exports = ListingColumn;
|
215
assets/js/src/listing/listing_item.jsx
Normal file
215
assets/js/src/listing/listing_item.jsx
Normal file
@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class ListingItem extends React.Component {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
handleSelectItem = (e) => {
|
||||
this.props.onSelectItem(
|
||||
parseInt(e.target.value, 10),
|
||||
e.target.checked
|
||||
);
|
||||
|
||||
return !e.target.checked;
|
||||
};
|
||||
|
||||
handleRestoreItem = (id) => {
|
||||
this.props.onRestoreItem(id);
|
||||
};
|
||||
|
||||
handleTrashItem = (id) => {
|
||||
this.props.onTrashItem(id);
|
||||
};
|
||||
|
||||
handleDeleteItem = (id) => {
|
||||
this.props.onDeleteItem(id);
|
||||
};
|
||||
|
||||
handleToggleItem = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
};
|
||||
|
||||
render() {
|
||||
let checkbox = false;
|
||||
|
||||
if (this.props.is_selectable === true) {
|
||||
checkbox = (
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
|
||||
`Select ${this.props.item[this.props.columns[0].name]}`
|
||||
}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={this.props.item.id}
|
||||
checked={
|
||||
this.props.item.selected || this.props.selection === 'all'
|
||||
}
|
||||
onChange={this.handleSelectItem}
|
||||
disabled={this.props.selection === 'all'}
|
||||
id={`listing-row-checkbox-${this.props.item.id}`}
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
const customActions = this.props.item_actions;
|
||||
let itemActions = false;
|
||||
|
||||
if (customActions.length > 0) {
|
||||
let isFirst = true;
|
||||
itemActions = customActions
|
||||
.filter(action => action.display === undefined || action.display(this.props.item))
|
||||
.map((action, index) => {
|
||||
let customAction = null;
|
||||
|
||||
if (action.name === 'trash') {
|
||||
customAction = (
|
||||
<span key={`action-${action.name}`} className="trash">
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleTrashItem(this.props.item.id)}
|
||||
>
|
||||
{MailPoet.I18n.t('moveToTrash')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
} else if (action.refresh) {
|
||||
customAction = (
|
||||
<span
|
||||
onClick={this.props.onRefreshItems}
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
role="button"
|
||||
tabIndex={index}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else if (action.link) {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={
|
||||
(action.onClick !== undefined)
|
||||
? () => action.onClick(this.props.item, this.props.onRefreshItems)
|
||||
: false
|
||||
}
|
||||
>{ action.label }</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (customAction !== null && isFirst === true) {
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return customAction;
|
||||
});
|
||||
} else {
|
||||
itemActions = (
|
||||
<span className="edit">
|
||||
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let actions;
|
||||
|
||||
if (this.props.group === 'trash') {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
<span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleRestoreItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('restore')}</a>
|
||||
</span>
|
||||
{ ' | ' }
|
||||
<span className="delete">
|
||||
<a
|
||||
className="submitdelete"
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleDeleteItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('deletePermanently')}</a>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
{ itemActions }
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
|
||||
|
||||
return (
|
||||
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
|
||||
{ checkbox }
|
||||
{ this.props.onRenderItem(this.props.item, actions) }
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingItem.propTypes = {
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
onRestoreItem: PropTypes.func.isRequired,
|
||||
onTrashItem: PropTypes.func.isRequired,
|
||||
onDeleteItem: PropTypes.func.isRequired,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onRefreshItems: PropTypes.func.isRequired,
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = ListingItem;
|
140
assets/js/src/listing/listing_items.jsx
Normal file
140
assets/js/src/listing/listing_items.jsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ListingItem from 'listing/listing_item.jsx';
|
||||
|
||||
class ListingItems extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
|
||||
render() {
|
||||
if (this.props.items.length === 0) {
|
||||
let message;
|
||||
if (this.props.loading === true) {
|
||||
message = (this.props.messages.onLoadingItems
|
||||
&& this.props.messages.onLoadingItems(this.props.group))
|
||||
|| MailPoet.I18n.t('loadingItems');
|
||||
} else {
|
||||
message = (this.props.messages.onNoItemsFound
|
||||
&& this.props.messages.onNoItemsFound(this.props.group))
|
||||
|| MailPoet.I18n.t('noItemsFound');
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className="no-items">
|
||||
<td
|
||||
colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
className="colspanchange"
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
const selectAllClasses = classNames(
|
||||
'mailpoet_select_all',
|
||||
{ mailpoet_hidden: (
|
||||
this.props.selection === false
|
||||
|| (this.props.count <= this.props.limit)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className={selectAllClasses}>
|
||||
<td colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
>
|
||||
{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLabel')
|
||||
: MailPoet.I18n.t('selectedAllLabel').replace(
|
||||
'%d',
|
||||
this.props.count.toLocaleString()
|
||||
)
|
||||
}
|
||||
|
||||
<a
|
||||
onClick={this.props.onSelectAll}
|
||||
href="javascript:;"
|
||||
>{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLink')
|
||||
: MailPoet.I18n.t('clearSelection')
|
||||
}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.props.items.map((item) => {
|
||||
const renderItem = item;
|
||||
renderItem.id = parseInt(item.id, 10);
|
||||
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
|
||||
let key = `item-${renderItem.id}-${item.id}`;
|
||||
if (typeof this.props.getListingItemKey === 'function') {
|
||||
key = this.props.getListingItemKey(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListingItem
|
||||
columns={this.props.columns}
|
||||
onSelectItem={this.props.onSelectItem}
|
||||
onRenderItem={this.props.onRenderItem}
|
||||
onDeleteItem={this.props.onDeleteItem}
|
||||
onRestoreItem={this.props.onRestoreItem}
|
||||
onTrashItem={this.props.onTrashItem}
|
||||
onRefreshItems={this.props.onRefreshItems}
|
||||
selection={this.props.selection}
|
||||
is_selectable={this.props.is_selectable}
|
||||
item_actions={this.props.item_actions}
|
||||
group={this.props.group}
|
||||
key={key}
|
||||
item={renderItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingItems.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
messages: PropTypes.shape({
|
||||
onLoadingItems: PropTypes.func,
|
||||
onNoItemsFound: PropTypes.func,
|
||||
}).isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
onSelectAll: PropTypes.func.isRequired,
|
||||
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
getListingItemKey: PropTypes.func,
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
onDeleteItem: PropTypes.func.isRequired,
|
||||
onRestoreItem: PropTypes.func.isRequired,
|
||||
onTrashItem: PropTypes.func.isRequired,
|
||||
onRefreshItems: PropTypes.func.isRequired,
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
};
|
||||
|
||||
ListingItems.defaultProps = {
|
||||
getListingItemKey: undefined,
|
||||
};
|
||||
|
||||
module.exports = ListingItems;
|
@ -1,56 +1,62 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ListingPages = React.createClass({
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
page: null,
|
||||
};
|
||||
},
|
||||
setPage: function setPage(page) {
|
||||
class ListingPages extends React.Component {
|
||||
state = {
|
||||
page: null,
|
||||
};
|
||||
|
||||
setPage = (page) => {
|
||||
this.setState({
|
||||
page: null,
|
||||
}, () => {
|
||||
this.props.onSetPage(this.constrainPage(page));
|
||||
});
|
||||
},
|
||||
setFirstPage: function setFirstPage() {
|
||||
};
|
||||
|
||||
setFirstPage = () => {
|
||||
this.setPage(1);
|
||||
},
|
||||
setLastPage: function setLastPage() {
|
||||
};
|
||||
|
||||
setLastPage = () => {
|
||||
this.setPage(this.getLastPage());
|
||||
},
|
||||
setPreviousPage: function setPreviousPage() {
|
||||
};
|
||||
|
||||
setPreviousPage = () => {
|
||||
this.setPage(this.constrainPage(
|
||||
parseInt(this.props.page, 10) - 1)
|
||||
);
|
||||
},
|
||||
setNextPage: function setNextPage() {
|
||||
};
|
||||
|
||||
setNextPage = () => {
|
||||
this.setPage(this.constrainPage(
|
||||
parseInt(this.props.page, 10) + 1)
|
||||
);
|
||||
},
|
||||
constrainPage: function constrainPage(page) {
|
||||
return Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
|
||||
},
|
||||
handleSetManualPage: function handleSetManualPage(e) {
|
||||
};
|
||||
|
||||
getLastPage = () => Math.ceil(this.props.count / this.props.limit);
|
||||
|
||||
handleSetManualPage = (e) => {
|
||||
if (e.which === 13) {
|
||||
this.setPage(this.state.page);
|
||||
}
|
||||
},
|
||||
handleChangeManualPage: function handleChangeManualPage(e) {
|
||||
};
|
||||
|
||||
handleChangeManualPage = (e) => {
|
||||
this.setState({
|
||||
page: e.target.value,
|
||||
});
|
||||
},
|
||||
handleBlurManualPage: function handleBlurManualPage(e) {
|
||||
};
|
||||
|
||||
handleBlurManualPage = (e) => {
|
||||
this.setPage(e.target.value);
|
||||
},
|
||||
getLastPage: function getLastPage() {
|
||||
return Math.ceil(this.props.count / this.props.limit);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
constrainPage = page => Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
|
||||
|
||||
render() {
|
||||
if (this.props.count === 0) {
|
||||
return false;
|
||||
}
|
||||
@ -181,7 +187,17 @@ const ListingPages = React.createClass({
|
||||
{ pagination }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ListingPages.propTypes = {
|
||||
onSetPage: PropTypes.func.isRequired,
|
||||
page: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
]).isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
module.exports = ListingPages;
|
||||
|
13
assets/js/src/loading.jsx
Normal file
13
assets/js/src/loading.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div className="mailpoet_loading">
|
||||
<div className="mailpoet_modal_loading mailpoet_modal_loading_1" />
|
||||
<div className="mailpoet_modal_loading mailpoet_modal_loading_2" />
|
||||
<div className="mailpoet_modal_loading mailpoet_modal_loading_3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Loading;
|
@ -87,7 +87,7 @@ define('modal', ['mailpoet', 'jquery'],
|
||||
'<div class="mailpoet_popup_body clearfix"></div>' +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
loading: '<div id="mailpoet_loading" style="display:none;">' +
|
||||
loading: '<div id="mailpoet_loading" class="mailpoet_loading" style="display:none;">' +
|
||||
'<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>' +
|
||||
'<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>' +
|
||||
'<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>' +
|
||||
|
@ -33,9 +33,9 @@ define('mp2migrator', ['mailpoet', 'jquery'], function (mp, jQuery) {
|
||||
jQuery('#logger').html('');
|
||||
result.split('\n').forEach(function (resultRow) {
|
||||
var row = resultRow;
|
||||
if (row.substr(0, 7) === '[ERROR]' || row.substr(0, 9) === '[WARNING]' || row === MailPoet.I18n.t('import_stopped_by_user')) {
|
||||
if (row.substr(0, 7) === '[ERROR]' || row.substr(0, 9) === '[WARNING]' || row.toLowerCase() === MailPoet.I18n.t('import_stopped_by_user').toLowerCase()) {
|
||||
row = '<span class="error_msg">' + row + '</span>'; // Mark the errors in red
|
||||
} else if (row === MailPoet.I18n.t('import_complete')) { // Test if the import is complete
|
||||
} else if (row.toLowerCase() === MailPoet.I18n.t('import_complete').toLowerCase()) { // Test if the import is complete
|
||||
jQuery('#import-actions').hide();
|
||||
jQuery('#upgrade-completed').show();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import InAppAnnouncement from 'in_app_announcements/in_app_announcement.jsx';
|
||||
@ -19,8 +20,8 @@ const BackgroundImageAnnouncement = props => (
|
||||
);
|
||||
|
||||
BackgroundImageAnnouncement.propTypes = {
|
||||
username: React.PropTypes.string.isRequired,
|
||||
videoUrl: React.PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
videoUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = BackgroundImageAnnouncement;
|
||||
|
@ -39,6 +39,7 @@ define([
|
||||
defaults: function () {
|
||||
return this._getDefaults({
|
||||
type: 'container',
|
||||
columnLayout: false,
|
||||
orientation: 'vertical',
|
||||
image: {
|
||||
src: null,
|
||||
@ -163,6 +164,8 @@ define([
|
||||
this.renderOptions = _.defaults(options.renderOptions || {}, {});
|
||||
},
|
||||
onRender: function () {
|
||||
var classIrregular = '';
|
||||
var columnLayout;
|
||||
this.toolsView = new Module.ContainerBlockToolsView({
|
||||
model: this.model,
|
||||
tools: {
|
||||
@ -183,7 +186,13 @@ define([
|
||||
// Sets child container orientation HTML class here,
|
||||
// as child CollectionView won't have access to model
|
||||
// and will overwrite existing region element instead
|
||||
this.$('> .mailpoet_container').attr('class', 'mailpoet_container mailpoet_container_' + this.model.get('orientation'));
|
||||
columnLayout = this.model.get('columnLayout');
|
||||
if (typeof columnLayout === 'string') {
|
||||
classIrregular = 'mailpoet_irregular_width_contents_container column_layout_' + columnLayout;
|
||||
}
|
||||
this.$('> .mailpoet_container').attr('class',
|
||||
'mailpoet_container mailpoet_container_' + this.model.get('orientation') + ' ' + classIrregular
|
||||
);
|
||||
},
|
||||
showTools: function () {
|
||||
if (this.renderOptions.depth === 1 && !this.$el.hasClass('mailpoet_container_layer_active')) {
|
||||
@ -353,6 +362,48 @@ define([
|
||||
}
|
||||
});
|
||||
|
||||
Module.TwoColumn12ContainerWidgetView = base.WidgetView.extend({
|
||||
className: base.WidgetView.prototype.className + ' mailpoet_droppable_layout_block',
|
||||
getTemplate: function () { return window.templates.twoColumn12LayoutInsertion; },
|
||||
behaviors: {
|
||||
DraggableBehavior: {
|
||||
cloneOriginal: true,
|
||||
drop: function () {
|
||||
var block = new Module.ContainerBlockModel({
|
||||
orientation: 'horizontal',
|
||||
blocks: [
|
||||
new Module.ContainerBlockModel(),
|
||||
new Module.ContainerBlockModel()
|
||||
]
|
||||
});
|
||||
block.set('columnLayout', '1_2');
|
||||
return block;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Module.TwoColumn21ContainerWidgetView = base.WidgetView.extend({
|
||||
className: base.WidgetView.prototype.className + ' mailpoet_droppable_layout_block',
|
||||
getTemplate: function () { return window.templates.twoColumn21LayoutInsertion; },
|
||||
behaviors: {
|
||||
DraggableBehavior: {
|
||||
cloneOriginal: true,
|
||||
drop: function () {
|
||||
var block = new Module.ContainerBlockModel({
|
||||
orientation: 'horizontal',
|
||||
blocks: [
|
||||
new Module.ContainerBlockModel(),
|
||||
new Module.ContainerBlockModel()
|
||||
]
|
||||
});
|
||||
block.set('columnLayout', '2_1');
|
||||
return block;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App.on('before:start', function (BeforeStartApp) {
|
||||
BeforeStartApp.registerBlockType('container', {
|
||||
blockModel: Module.ContainerBlockModel,
|
||||
@ -376,6 +427,18 @@ define([
|
||||
priority: 100,
|
||||
widgetView: Module.ThreeColumnContainerWidgetView
|
||||
});
|
||||
|
||||
BeforeStartApp.registerLayoutWidget({
|
||||
name: 'twoColumn12Layout',
|
||||
priority: 100,
|
||||
widgetView: Module.TwoColumn12ContainerWidgetView
|
||||
});
|
||||
|
||||
BeforeStartApp.registerLayoutWidget({
|
||||
name: 'twoColumn21Layout',
|
||||
priority: 100,
|
||||
widgetView: Module.TwoColumn21ContainerWidgetView
|
||||
});
|
||||
});
|
||||
|
||||
return Module;
|
||||
|
@ -266,7 +266,6 @@ define([
|
||||
},
|
||||
validateNewsletter: function (jsonObject) {
|
||||
var body = '';
|
||||
var contents;
|
||||
if (!App._contentContainer.isValid()) {
|
||||
this.showValidationError(App._contentContainer.validationError);
|
||||
return;
|
||||
@ -282,10 +281,9 @@ define([
|
||||
return;
|
||||
}
|
||||
|
||||
contents = JSON.stringify(jsonObject);
|
||||
if ((App.getNewsletter().get('type') === 'notification') &&
|
||||
contents.indexOf('"type":"automatedLatestContent"') < 0 &&
|
||||
contents.indexOf('"type":"automatedLatestContentLayout"') < 0
|
||||
body.indexOf('"type":"automatedLatestContent"') < 0 &&
|
||||
body.indexOf('"type":"automatedLatestContentLayout"') < 0
|
||||
) {
|
||||
this.showValidationError(MailPoet.I18n.t('automatedLatestContentMissing'));
|
||||
return;
|
||||
|
@ -10,8 +10,7 @@ const displayTutorial = () => {
|
||||
return;
|
||||
}
|
||||
MailPoet.Modal.popup({
|
||||
title: MailPoet.I18n.t('tutorialVideoTitle'),
|
||||
template: `<video style="height:640px;" src="${window.config.dragDemoUrl}" controls autoplay></video>`,
|
||||
template: `<div class="mailpoet_drag_and_drop_tutorial"><h2>${MailPoet.I18n.t('tutorialVideoTitle')}</h2><video style="height:640px;" src="${window.config.dragDemoUrl}" controls autoplay></video></div>`,
|
||||
onCancel: () => {
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
const Breadcrumb = React.createClass({
|
||||
getInitialState: function getInitialState() {
|
||||
const steps = this.props.steps || [
|
||||
class Breadcrumb extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const steps = props.steps || [
|
||||
{
|
||||
name: 'type',
|
||||
label: MailPoet.I18n.t('selectType'),
|
||||
@ -24,12 +26,14 @@ const Breadcrumb = React.createClass({
|
||||
label: MailPoet.I18n.t('send'),
|
||||
},
|
||||
];
|
||||
return {
|
||||
|
||||
this.state = {
|
||||
step: null,
|
||||
steps,
|
||||
};
|
||||
},
|
||||
render: function render() {
|
||||
}
|
||||
|
||||
render() {
|
||||
const steps = this.state.steps.map((step, index) => {
|
||||
const stepClasses = classNames(
|
||||
{ mailpoet_current: (this.props.step === step.name) }
|
||||
@ -58,8 +62,17 @@ const Breadcrumb = React.createClass({
|
||||
{ steps }
|
||||
</p>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Breadcrumb.propTypes = {
|
||||
steps: PropTypes.arrayOf(PropTypes.object),
|
||||
step: PropTypes.string,
|
||||
};
|
||||
|
||||
Breadcrumb.defaultProps = {
|
||||
steps: undefined,
|
||||
step: null,
|
||||
};
|
||||
|
||||
module.exports = Breadcrumb;
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { Link } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx';
|
||||
@ -157,8 +159,17 @@ const newsletterActions = [
|
||||
},
|
||||
];
|
||||
|
||||
const NewsletterListNotification = React.createClass({
|
||||
const NewsletterListNotification = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
|
||||
displayName: 'NewsletterListNotification',
|
||||
|
||||
propTypes: {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
},
|
||||
|
||||
mixins: [MailerMixin, CronMixin],
|
||||
|
||||
updateStatus: function updateStatus(e) {
|
||||
// make the event persist so that we can still override the selected value
|
||||
// in the ajax callback
|
||||
@ -185,6 +196,7 @@ const NewsletterListNotification = React.createClass({
|
||||
e.target.value = response.status;
|
||||
});
|
||||
},
|
||||
|
||||
renderStatus: function renderStatus(newsletter) {
|
||||
return (
|
||||
<select
|
||||
@ -197,6 +209,7 @@ const NewsletterListNotification = React.createClass({
|
||||
</select>
|
||||
);
|
||||
},
|
||||
|
||||
renderSettings: function renderSettings(newsletter) {
|
||||
let sendingFrequency;
|
||||
|
||||
@ -265,6 +278,7 @@ const NewsletterListNotification = React.createClass({
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
renderHistoryLink: function renderHistoryLink(newsletter) {
|
||||
const childrenCount = Number((newsletter.children_count));
|
||||
if (childrenCount === 0) {
|
||||
@ -274,10 +288,12 @@ const NewsletterListNotification = React.createClass({
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
data-automation-id={`history-${newsletter.id}`}
|
||||
to={`/notification/history/${newsletter.id}`}
|
||||
>{ MailPoet.I18n.t('viewHistory') }</Link>
|
||||
);
|
||||
},
|
||||
|
||||
renderItem: function renderItem(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -311,6 +327,7 @@ const NewsletterListNotification = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx';
|
||||
@ -57,8 +59,16 @@ let newsletterActions = [
|
||||
Hooks.addFilter('mailpoet_newsletters_listings_notification_history_actions', StatisticsMixin.addStatsCTAAction);
|
||||
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletterActions);
|
||||
|
||||
const NewsletterListNotificationHistory = React.createClass({
|
||||
const NewsletterListNotificationHistory = createReactClass({ // eslint-disable-line react/prefer-es6-class, max-len
|
||||
displayName: 'NewsletterListNotificationHistory',
|
||||
|
||||
propTypes: {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
},
|
||||
|
||||
mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin],
|
||||
|
||||
renderItem: function renderItem(newsletter, actions, meta) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -96,6 +106,7 @@ const NewsletterListNotificationHistory = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx';
|
||||
@ -172,8 +174,16 @@ let newsletterActions = [
|
||||
Hooks.addFilter('mailpoet_newsletters_listings_standard_actions', StatisticsMixin.addStatsCTAAction);
|
||||
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletterActions);
|
||||
|
||||
const NewsletterListStandard = React.createClass({
|
||||
const NewsletterListStandard = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
displayName: 'NewsletterListStandard',
|
||||
|
||||
propTypes: {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
},
|
||||
|
||||
mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin],
|
||||
|
||||
renderItem: function renderItem(newsletter, actions, meta) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -212,6 +222,7 @@ const NewsletterListStandard = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -3,30 +3,30 @@ import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingTabs extends React.Component {
|
||||
state = {
|
||||
tab: null,
|
||||
tabs: Hooks.applyFilters('mailpoet_newsletters_listings_tabs', [
|
||||
{
|
||||
name: 'standard',
|
||||
label: MailPoet.I18n.t('tabStandardTitle'),
|
||||
link: '/standard',
|
||||
},
|
||||
{
|
||||
name: 'welcome',
|
||||
label: MailPoet.I18n.t('tabWelcomeTitle'),
|
||||
link: '/welcome',
|
||||
},
|
||||
{
|
||||
name: 'notification',
|
||||
label: MailPoet.I18n.t('tabNotificationTitle'),
|
||||
link: '/notification',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const ListingTabs = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
tab: null,
|
||||
tabs: Hooks.applyFilters('mailpoet_newsletters_listings_tabs', [
|
||||
{
|
||||
name: 'standard',
|
||||
label: MailPoet.I18n.t('tabStandardTitle'),
|
||||
link: '/standard',
|
||||
},
|
||||
{
|
||||
name: 'welcome',
|
||||
label: MailPoet.I18n.t('tabWelcomeTitle'),
|
||||
link: '/welcome',
|
||||
},
|
||||
{
|
||||
name: 'notification',
|
||||
label: MailPoet.I18n.t('tabNotificationTitle'),
|
||||
link: '/notification',
|
||||
},
|
||||
]),
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const tabs = this.state.tabs.map((tab) => {
|
||||
const tabClasses = classNames(
|
||||
@ -38,6 +38,7 @@ const ListingTabs = React.createClass({
|
||||
<Link
|
||||
key={`tab-${tab.label}`}
|
||||
className={tabClasses}
|
||||
data-automation-id={`tab-${tab.label}`}
|
||||
to={tab.link}
|
||||
onClick={() => MailPoet.trackEvent(`Tab Emails > ${tab.name} clicked`,
|
||||
{ 'MailPoet Free version': window.mailpoet_version }
|
||||
@ -51,7 +52,11 @@ const ListingTabs = React.createClass({
|
||||
{ tabs }
|
||||
</h2>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ListingTabs.propTypes = {
|
||||
tab: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = ListingTabs;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx';
|
||||
@ -153,8 +155,16 @@ let newsletterActions = [
|
||||
Hooks.addFilter('mailpoet_newsletters_listings_welcome_notification_actions', StatisticsMixin.addStatsCTAAction);
|
||||
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletterActions);
|
||||
|
||||
const NewsletterListWelcome = React.createClass({
|
||||
const NewsletterListWelcome = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
displayName: 'NewsletterListWelcome',
|
||||
|
||||
propTypes: {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
},
|
||||
|
||||
mixins: [StatisticsMixin, MailerMixin, CronMixin],
|
||||
|
||||
updateStatus: function updateStatus(e) {
|
||||
// make the event persist so that we can still override the selected value
|
||||
// in the ajax callback
|
||||
@ -181,6 +191,7 @@ const NewsletterListWelcome = React.createClass({
|
||||
e.target.value = response.status;
|
||||
});
|
||||
},
|
||||
|
||||
renderStatus: function renderStatus(newsletter) {
|
||||
const totalSent = (parseInt(newsletter.total_sent, 10)) ?
|
||||
MailPoet.I18n.t('sentToXSubscribers')
|
||||
@ -203,6 +214,7 @@ const NewsletterListWelcome = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderSettings: function renderSettings(newsletter) {
|
||||
let sendingEvent;
|
||||
let sendingDelay;
|
||||
@ -278,6 +290,7 @@ const NewsletterListWelcome = React.createClass({
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
renderItem: function renderItem(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -316,6 +329,7 @@ const NewsletterListWelcome = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router';
|
||||
import { IndexRedirect, Route, Router, useRouterHistory } from 'react-router';
|
||||
import { createHashHistory } from 'history';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import _ from 'underscore';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import NewsletterTypes from 'newsletters/types.jsx';
|
||||
import NewsletterTemplates from 'newsletters/templates.jsx';
|
||||
import NewsletterSend from 'newsletters/send.jsx';
|
||||
import NewsletterCongratulate from 'newsletters/send/congratulate/congratulate.jsx';
|
||||
import NewsletterTypeStandard from 'newsletters/types/standard.jsx';
|
||||
import NewsletterTypeNotification from 'newsletters/types/notification/notification.jsx';
|
||||
import NewsletterTypeWelcome from 'newsletters/types/welcome/welcome.jsx';
|
||||
@ -19,11 +21,15 @@ import NewsletterListNotificationHistory from 'newsletters/listings/notification
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
const App = React.createClass({
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const container = document.getElementById('newsletters_container');
|
||||
|
||||
@ -84,6 +90,11 @@ if (container) {
|
||||
path: 'template/:id',
|
||||
component: NewsletterTemplates,
|
||||
},
|
||||
/* congratulate */
|
||||
{
|
||||
path: 'send/congratulate/:id',
|
||||
component: NewsletterCongratulate,
|
||||
},
|
||||
/* Sending options */
|
||||
{
|
||||
path: 'send/:id',
|
||||
@ -93,7 +104,7 @@ if (container) {
|
||||
|
||||
routes = Hooks.applyFilters('mailpoet_newsletters_before_router', [...routes, ...getAutomaticEmailsRoutes()]);
|
||||
|
||||
const mailpoetListing = ReactDOM.render(( // eslint-disable-line react/no-render-return-value
|
||||
window.mailpoet_listing = ReactDOM.render(( // eslint-disable-line react/no-render-return-value
|
||||
<Router history={history}>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRedirect to="standard" />
|
||||
@ -110,6 +121,4 @@ if (container) {
|
||||
</Route>
|
||||
</Router>
|
||||
), container);
|
||||
|
||||
window.mailpoet_listing = mailpoetListing;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import MailPoet from 'mailpoet';
|
||||
import _ from 'underscore';
|
||||
import Breadcrumb from 'newsletters/breadcrumb.jsx';
|
||||
@ -10,11 +11,21 @@ import HelpTooltip from 'help-tooltip.jsx';
|
||||
import jQuery from 'jquery';
|
||||
import { fromUrl } from 'common/thumbnail.jsx';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const NewsletterSend = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
const NewsletterSend = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
displayName: 'NewsletterSend',
|
||||
|
||||
propTypes: {
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
router: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
fields: [],
|
||||
@ -22,14 +33,26 @@ const NewsletterSend = React.createClass({
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.loadItem(this.props.params.id);
|
||||
jQuery('#mailpoet_newsletter').parsley();
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function componentWillReceiveProps(props) {
|
||||
this.loadItem(props.params.id);
|
||||
},
|
||||
|
||||
getFieldsByNewsletter: function getFieldsByNewsletter(newsletter) {
|
||||
const type = this.getSubtype(newsletter);
|
||||
return type.getFields(newsletter);
|
||||
},
|
||||
|
||||
getSendButtonOptions: function getSendButtonOptions() {
|
||||
const type = this.getSubtype(this.state.item);
|
||||
return type.getSendButtonOptions(this.state.item);
|
||||
},
|
||||
|
||||
getSubtype: function getSubtype(newsletter) {
|
||||
switch (newsletter.type) {
|
||||
case 'notification': return NotificationNewsletterFields;
|
||||
@ -37,16 +60,11 @@ const NewsletterSend = React.createClass({
|
||||
default: return Hooks.applyFilters('mailpoet_newsletters_send_newsletter_fields', StandardNewsletterFields, newsletter);
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function isValid() {
|
||||
return jQuery('#mailpoet_newsletter').parsley().isValid();
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.loadItem(this.props.params.id);
|
||||
jQuery('#mailpoet_newsletter').parsley();
|
||||
},
|
||||
componentWillReceiveProps: function componentWillReceiveProps(props) {
|
||||
this.loadItem(props.params.id);
|
||||
},
|
||||
|
||||
loadItem: function loadItem(id) {
|
||||
this.setState({ loading: true });
|
||||
|
||||
@ -72,6 +90,7 @@ const NewsletterSend = React.createClass({
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
saveTemplate: function saveTemplate(response, done) {
|
||||
fromUrl(response.meta.preview_url)
|
||||
.then((thumbnail) => {
|
||||
@ -97,6 +116,7 @@ const NewsletterSend = React.createClass({
|
||||
this.showError({ errors: [err] });
|
||||
});
|
||||
},
|
||||
|
||||
handleSend: function handleSend(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -124,6 +144,7 @@ const NewsletterSend = React.createClass({
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
},
|
||||
|
||||
sendNewsletter: function sendNewsletter(newsletter) {
|
||||
return MailPoet.Ajax.post(
|
||||
Hooks.applyFilters(
|
||||
@ -141,6 +162,11 @@ const NewsletterSend = React.createClass({
|
||||
).done((response) => {
|
||||
// save template in recently sent category
|
||||
this.saveTemplate(newsletter, () => {
|
||||
if (window.mailpoet_show_congratulate_after_first_newsletter) {
|
||||
MailPoet.Modal.loading(false);
|
||||
this.context.router.push(`/send/congratulate/${this.state.item.id}`);
|
||||
return;
|
||||
}
|
||||
// redirect to listing based on newsletter type
|
||||
this.context.router.push(Hooks.applyFilters('mailpoet_newsletters_send_server_request_response_redirect', `/${this.state.item.type || ''}`, this.state.item));
|
||||
const customResponse = Hooks.applyFilters('mailpoet_newsletters_send_server_request_response', this.state.item, response);
|
||||
@ -171,6 +197,7 @@ const NewsletterSend = React.createClass({
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
},
|
||||
|
||||
activateNewsletter: function activateEmail(newsletter) {
|
||||
return MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
@ -183,6 +210,11 @@ const NewsletterSend = React.createClass({
|
||||
}).done((response) => {
|
||||
// save template in recently sent category
|
||||
this.saveTemplate(newsletter, () => {
|
||||
if (window.mailpoet_show_congratulate_after_first_newsletter) {
|
||||
MailPoet.Modal.loading(false);
|
||||
this.context.router.push(`/send/congratulate/${this.state.item.id}`);
|
||||
return;
|
||||
}
|
||||
// redirect to listing based on newsletter type
|
||||
this.context.router.push(`/${this.state.item.type || ''}`);
|
||||
const opts = this.state.item.options;
|
||||
@ -213,6 +245,7 @@ const NewsletterSend = React.createClass({
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
},
|
||||
|
||||
handleResume: function handleResume(e) {
|
||||
e.preventDefault();
|
||||
if (!this.isValid()) {
|
||||
@ -251,6 +284,7 @@ const NewsletterSend = React.createClass({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
handleSave: function handleSave(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -264,6 +298,7 @@ const NewsletterSend = React.createClass({
|
||||
this.showError(err);
|
||||
});
|
||||
},
|
||||
|
||||
handleRedirectToDesign: function handleRedirectToDesign(e) {
|
||||
e.preventDefault();
|
||||
const redirectTo = e.target.href;
|
||||
@ -278,6 +313,7 @@ const NewsletterSend = React.createClass({
|
||||
this.showError(err);
|
||||
});
|
||||
},
|
||||
|
||||
saveNewsletter: function saveNewsletter() {
|
||||
const data = this.state.item;
|
||||
data.queue = undefined;
|
||||
@ -298,10 +334,9 @@ const NewsletterSend = React.createClass({
|
||||
endpoint: 'newsletters',
|
||||
action: 'save',
|
||||
data: newsletterData,
|
||||
}).always(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
showError: (response) => {
|
||||
if (response.errors.length > 0) {
|
||||
MailPoet.Notice.error(
|
||||
@ -310,6 +345,7 @@ const NewsletterSend = React.createClass({
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
handleFormChange: function handleFormChange(e) {
|
||||
const item = this.state.item;
|
||||
const field = e.target.name;
|
||||
@ -321,6 +357,7 @@ const NewsletterSend = React.createClass({
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
render: function render() {
|
||||
const isPaused = this.state.item.status === 'sending'
|
||||
&& this.state.item.queue
|
||||
|
136
assets/js/src/newsletters/send/congratulate/congratulate.jsx
Normal file
136
assets/js/src/newsletters/send/congratulate/congratulate.jsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import moment from 'moment';
|
||||
|
||||
import Success from './success.jsx';
|
||||
import Fail from './fail.jsx';
|
||||
import Loading from './loading.jsx';
|
||||
|
||||
const SECONDS_WAITING_FOR_SUCCESS = 20;
|
||||
const SECONDS_MINIMUIM_LOADING_SCREEN_DISPLAYED = 6;
|
||||
|
||||
function successPageClosed() {
|
||||
return MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'settings',
|
||||
action: 'set',
|
||||
data: { show_congratulate_after_first_newsletter: false },
|
||||
}).always(() => {
|
||||
window.location = window.mailpoet_main_page;
|
||||
});
|
||||
}
|
||||
|
||||
function renderSuccess(newsletter, testingPassed) {
|
||||
if (testingPassed) {
|
||||
MailPoet.trackEvent('Cron testing done', {
|
||||
'Cron is working': 'true',
|
||||
});
|
||||
}
|
||||
return (<Success
|
||||
illustrationImageUrl={window.mailpoet_congratulations_success_image}
|
||||
successClicked={successPageClosed}
|
||||
newsletter={newsletter}
|
||||
/>);
|
||||
}
|
||||
|
||||
function renderFail() {
|
||||
MailPoet.trackEvent('Cron testing done', {
|
||||
'Cron is working': 'false',
|
||||
});
|
||||
return (<Fail
|
||||
failClicked={() => {
|
||||
window.location = window.mailpoet_main_page;
|
||||
}}
|
||||
/>);
|
||||
}
|
||||
|
||||
function renderLoading(showRichLoadingScreen) {
|
||||
return (<Loading
|
||||
illustrationImageUrl={window.mailpoet_congratulations_loading_image}
|
||||
successClicked={successPageClosed}
|
||||
showRichLoadingScreen={showRichLoadingScreen}
|
||||
/>);
|
||||
}
|
||||
|
||||
class Congratulate extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
fail: false,
|
||||
newsletter: null,
|
||||
testingPassed: false,
|
||||
timeStart: moment(),
|
||||
minimumLoadingTimePassed: false,
|
||||
};
|
||||
this.tick = this.tick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadNewsletter(this.props.params.id);
|
||||
this.tick();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.loadNewsletter(props.params.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (moment().subtract(SECONDS_WAITING_FOR_SUCCESS, 'second').isAfter(this.state.timeStart)) {
|
||||
this.setState({ error: true, loading: false });
|
||||
}
|
||||
if (this.state.loading) {
|
||||
this.loadNewsletter(this.props.params.id);
|
||||
}
|
||||
if (moment().subtract(SECONDS_MINIMUIM_LOADING_SCREEN_DISPLAYED, 'seconds').isAfter(this.state.timeStart)) {
|
||||
this.setState({ minimumLoadingTimePassed: true });
|
||||
}
|
||||
if (this.state.loading || !this.state.minimumLoadingTimePassed) {
|
||||
setTimeout(this.tick, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
loadNewsletter(id) {
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'get',
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
.done(response => this.newsletterLoaded(response.data));
|
||||
}
|
||||
|
||||
newsletterLoaded(newsletter) {
|
||||
if ((newsletter.type !== 'standard') || (newsletter.status === 'scheduled')) {
|
||||
this.setState({ newsletter, loading: false, minimumLoadingTimePassed: true });
|
||||
} else if ((newsletter.status === 'sent') || (newsletter.status === 'sending')) {
|
||||
this.setState({ newsletter, loading: false, testingPassed: true });
|
||||
} else {
|
||||
this.setState({ newsletter });
|
||||
}
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.loading || !this.state.minimumLoadingTimePassed) {
|
||||
return renderLoading(!!this.state.newsletter);
|
||||
} else if (this.state.error) {
|
||||
return renderFail();
|
||||
}
|
||||
return renderSuccess(this.state.newsletter, this.state.testingPassed);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="newsletter_congratulate_page">{this.renderContent()}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
Congratulate.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
module.exports = Congratulate;
|
34
assets/js/src/newsletters/send/congratulate/fail.jsx
Normal file
34
assets/js/src/newsletters/send/congratulate/fail.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import MailPoet from '../../../mailpoet';
|
||||
|
||||
function Fail(props) {
|
||||
return (
|
||||
<div className="mailpoet_centered">
|
||||
<h1>{MailPoet.I18n.t('congratulationsSendFailHeader')}</h1>
|
||||
<p>
|
||||
{ ReactStringReplace(
|
||||
MailPoet.I18n.t('congratulationsSendFailExplain'),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
(match, i) => (
|
||||
<a
|
||||
key={i}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://kb.mailpoet.com/article/231-sending-does-not-work"
|
||||
>{ match }</a>
|
||||
)
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<button className="button" onClick={props.failClicked}>{MailPoet.I18n.t('close')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Fail.propTypes = {
|
||||
failClicked: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = Fail;
|
32
assets/js/src/newsletters/send/congratulate/loading.jsx
Normal file
32
assets/js/src/newsletters/send/congratulate/loading.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from '../../../mailpoet';
|
||||
import LoadingDots from '../../../loading.jsx';
|
||||
|
||||
function renderRichData(showRichData, illustrationImageUrl) {
|
||||
if (showRichData) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mailpoet_newsletter_loading_header">{MailPoet.I18n.t('congratulationsLoadingHeader')}</h1>
|
||||
<img src={illustrationImageUrl} alt="" width="800px" height="266px" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
function Loading(props) {
|
||||
return (
|
||||
<div className="mailpoet_newsletter_loading">
|
||||
<LoadingDots />
|
||||
{renderRichData(props.showRichLoadingScreen, props.illustrationImageUrl)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
illustrationImageUrl: PropTypes.string.isRequired,
|
||||
showRichLoadingScreen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
module.exports = Loading;
|
38
assets/js/src/newsletters/send/congratulate/success.jsx
Normal file
38
assets/js/src/newsletters/send/congratulate/success.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from '../../../mailpoet';
|
||||
|
||||
function renderHeader(newsletter) {
|
||||
if (newsletter.type === 'welcome') {
|
||||
return MailPoet.I18n.t('congratulationsWelcomeEmailSuccessHeader');
|
||||
} else if (newsletter.type === 'notification') {
|
||||
return MailPoet.I18n.t('congratulationsPostNotificationSuccessHeader');
|
||||
} else if (newsletter.type === 'automatic') {
|
||||
return MailPoet.I18n.t('congratulationsWooSuccessHeader');
|
||||
} else if (newsletter.status === 'scheduled') {
|
||||
return MailPoet.I18n.t('congratulationsScheduleSuccessHeader');
|
||||
}
|
||||
return MailPoet.I18n.t('congratulationsSendSuccessHeader');
|
||||
}
|
||||
|
||||
function Success(props) {
|
||||
return (
|
||||
<div className="mailpoet_congratulate_success">
|
||||
<h1>{renderHeader(props.newsletter)}</h1>
|
||||
<img src={props.illustrationImageUrl} alt="" width="750" height="250" />
|
||||
<button className="button" onClick={props.successClicked}>{MailPoet.I18n.t('close')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Success.propTypes = {
|
||||
successClicked: PropTypes.func.isRequired,
|
||||
illustrationImageUrl: PropTypes.string.isRequired,
|
||||
newsletter: PropTypes.shape({
|
||||
status: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
||||
module.exports = Success;
|
172
assets/js/src/newsletters/send/date_text.jsx
Normal file
172
assets/js/src/newsletters/send/date_text.jsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const datepickerTranslations = {
|
||||
closeText: MailPoet.I18n.t('close'),
|
||||
currentText: MailPoet.I18n.t('today'),
|
||||
nextText: MailPoet.I18n.t('next'),
|
||||
prevText: MailPoet.I18n.t('previous'),
|
||||
monthNames: [
|
||||
MailPoet.I18n.t('january'),
|
||||
MailPoet.I18n.t('february'),
|
||||
MailPoet.I18n.t('march'),
|
||||
MailPoet.I18n.t('april'),
|
||||
MailPoet.I18n.t('may'),
|
||||
MailPoet.I18n.t('june'),
|
||||
MailPoet.I18n.t('july'),
|
||||
MailPoet.I18n.t('august'),
|
||||
MailPoet.I18n.t('september'),
|
||||
MailPoet.I18n.t('october'),
|
||||
MailPoet.I18n.t('november'),
|
||||
MailPoet.I18n.t('december'),
|
||||
],
|
||||
monthNamesShort: [
|
||||
MailPoet.I18n.t('januaryShort'),
|
||||
MailPoet.I18n.t('februaryShort'),
|
||||
MailPoet.I18n.t('marchShort'),
|
||||
MailPoet.I18n.t('aprilShort'),
|
||||
MailPoet.I18n.t('mayShort'),
|
||||
MailPoet.I18n.t('juneShort'),
|
||||
MailPoet.I18n.t('julyShort'),
|
||||
MailPoet.I18n.t('augustShort'),
|
||||
MailPoet.I18n.t('septemberShort'),
|
||||
MailPoet.I18n.t('octoberShort'),
|
||||
MailPoet.I18n.t('novemberShort'),
|
||||
MailPoet.I18n.t('decemberShort'),
|
||||
],
|
||||
dayNames: [
|
||||
MailPoet.I18n.t('sunday'),
|
||||
MailPoet.I18n.t('monday'),
|
||||
MailPoet.I18n.t('tuesday'),
|
||||
MailPoet.I18n.t('wednesday'),
|
||||
MailPoet.I18n.t('thursday'),
|
||||
MailPoet.I18n.t('friday'),
|
||||
MailPoet.I18n.t('saturday'),
|
||||
],
|
||||
dayNamesShort: [
|
||||
MailPoet.I18n.t('sundayShort'),
|
||||
MailPoet.I18n.t('mondayShort'),
|
||||
MailPoet.I18n.t('tuesdayShort'),
|
||||
MailPoet.I18n.t('wednesdayShort'),
|
||||
MailPoet.I18n.t('thursdayShort'),
|
||||
MailPoet.I18n.t('fridayShort'),
|
||||
MailPoet.I18n.t('saturdayShort'),
|
||||
],
|
||||
dayNamesMin: [
|
||||
MailPoet.I18n.t('sundayMin'),
|
||||
MailPoet.I18n.t('mondayMin'),
|
||||
MailPoet.I18n.t('tuesdayMin'),
|
||||
MailPoet.I18n.t('wednesdayMin'),
|
||||
MailPoet.I18n.t('thursdayMin'),
|
||||
MailPoet.I18n.t('fridayMin'),
|
||||
MailPoet.I18n.t('saturdayMin'),
|
||||
],
|
||||
};
|
||||
|
||||
class DateText extends React.Component {
|
||||
componentDidMount() {
|
||||
const $element = jQuery(this.dateInput);
|
||||
const that = this;
|
||||
if ($element.datepicker) {
|
||||
// Override jQuery UI datepicker Date parsing and formatting
|
||||
jQuery.datepicker.parseDate = function parseDate(format, value) {
|
||||
// Transform string format to Date object
|
||||
return MailPoet.Date.toDate(value, {
|
||||
parseFormat: this.props.displayFormat,
|
||||
format,
|
||||
});
|
||||
};
|
||||
jQuery.datepicker.formatDate = function formatDate(format, value) {
|
||||
// Transform Date object to string format
|
||||
const newValue = MailPoet.Date.format(value, {
|
||||
format,
|
||||
});
|
||||
return newValue;
|
||||
};
|
||||
|
||||
$element.datepicker(_.extend({
|
||||
dateFormat: this.props.displayFormat,
|
||||
isRTL: false,
|
||||
onSelect: function onSelect(value) {
|
||||
that.onChange({
|
||||
target: {
|
||||
name: that.getFieldName(),
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
}, datepickerTranslations));
|
||||
|
||||
this.datepickerInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.datepickerInitialized) {
|
||||
jQuery(this.dateInput).datepicker('destroy');
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (event) => {
|
||||
const changeEvent = event;
|
||||
// Swap display format to storage format
|
||||
const displayDate = changeEvent.target.value;
|
||||
const storageDate = this.getStorageDate(displayDate);
|
||||
|
||||
changeEvent.target.value = storageDate;
|
||||
this.props.onChange(changeEvent);
|
||||
};
|
||||
|
||||
getFieldName = () => this.props.name || 'date';
|
||||
|
||||
getDisplayDate = (date) => {
|
||||
const formatting = {
|
||||
parseFormat: this.props.storageFormat,
|
||||
format: this.props.displayFormat,
|
||||
};
|
||||
return MailPoet.Date.format(date, formatting);
|
||||
};
|
||||
|
||||
getStorageDate = (date) => {
|
||||
const formatting = {
|
||||
parseFormat: this.props.displayFormat,
|
||||
format: this.props.storageFormat,
|
||||
};
|
||||
return MailPoet.Date.format(date, formatting);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
size="30"
|
||||
name={this.getFieldName()}
|
||||
value={this.getDisplayDate(this.props.value)}
|
||||
readOnly
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.onChange}
|
||||
ref={(c) => { this.dateInput = c; }}
|
||||
{...this.props.validation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateText.propTypes = {
|
||||
displayFormat: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string,
|
||||
storageFormat: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
validation: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
DateText.defaultProps = {
|
||||
name: 'date',
|
||||
};
|
||||
|
||||
module.exports = DateText;
|
93
assets/js/src/newsletters/send/date_time.jsx
Normal file
93
assets/js/src/newsletters/send/date_time.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import DateText from 'newsletters/send/date_text.jsx';
|
||||
import TimeSelect from 'newsletters/send/time_select.jsx';
|
||||
|
||||
class DateTime extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.buildStateFromProps(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState(this.buildStateFromProps(nextProps));
|
||||
}
|
||||
|
||||
getDateTime = () => [this.state.date, this.state.time].join(this.DATE_TIME_SEPARATOR);
|
||||
|
||||
DATE_TIME_SEPARATOR = ' ';
|
||||
|
||||
buildStateFromProps = (props) => {
|
||||
const value = props.value || this.props.defaultDateTime;
|
||||
const [date, time] = value.split(this.DATE_TIME_SEPARATOR);
|
||||
return {
|
||||
date,
|
||||
time,
|
||||
};
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
const newState = {};
|
||||
newState[event.target.name] = event.target.value;
|
||||
|
||||
this.setState(newState, this.propagateChange);
|
||||
};
|
||||
|
||||
propagateChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange({
|
||||
target: {
|
||||
name: this.props.name || '',
|
||||
value: this.getDateTime(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<DateText
|
||||
name="date"
|
||||
value={this.state.date}
|
||||
onChange={this.handleChange}
|
||||
displayFormat={this.props.dateDisplayFormat}
|
||||
storageFormat={this.props.dateStorageFormat}
|
||||
disabled={this.props.disabled}
|
||||
validation={this.props.dateValidation}
|
||||
/>
|
||||
<TimeSelect
|
||||
name="time"
|
||||
value={this.state.time}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.disabled}
|
||||
validation={this.props.timeValidation}
|
||||
timeOfDayItems={this.props.timeOfDayItems}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime.propTypes = {
|
||||
defaultDateTime: PropTypes.string.isRequired,
|
||||
dateDisplayFormat: PropTypes.string.isRequired,
|
||||
dateStorageFormat: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
dateValidation: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
timeValidation: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
timeOfDayItems: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
DateTime.defaultProps = {
|
||||
onChange: undefined,
|
||||
name: '',
|
||||
disabled: false,
|
||||
timeValidation: undefined,
|
||||
};
|
||||
|
||||
module.exports = DateTime;
|
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Hooks from 'wp-js-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import DateTime from 'newsletters/send/date_time.jsx';
|
||||
|
||||
const currentTime = window.mailpoet_current_time || '00:00';
|
||||
const defaultDateTime = `${window.mailpoet_current_date} 00:00:00`;
|
||||
@ -10,248 +12,33 @@ const timeOfDayItems = window.mailpoet_schedule_time_of_day;
|
||||
const dateDisplayFormat = window.mailpoet_date_display_format;
|
||||
const dateStorageFormat = window.mailpoet_date_storage_format;
|
||||
|
||||
const datepickerTranslations = {
|
||||
closeText: MailPoet.I18n.t('close'),
|
||||
currentText: MailPoet.I18n.t('today'),
|
||||
nextText: MailPoet.I18n.t('next'),
|
||||
prevText: MailPoet.I18n.t('previous'),
|
||||
monthNames: [
|
||||
MailPoet.I18n.t('january'),
|
||||
MailPoet.I18n.t('february'),
|
||||
MailPoet.I18n.t('march'),
|
||||
MailPoet.I18n.t('april'),
|
||||
MailPoet.I18n.t('may'),
|
||||
MailPoet.I18n.t('june'),
|
||||
MailPoet.I18n.t('july'),
|
||||
MailPoet.I18n.t('august'),
|
||||
MailPoet.I18n.t('september'),
|
||||
MailPoet.I18n.t('october'),
|
||||
MailPoet.I18n.t('november'),
|
||||
MailPoet.I18n.t('december'),
|
||||
],
|
||||
monthNamesShort: [
|
||||
MailPoet.I18n.t('januaryShort'),
|
||||
MailPoet.I18n.t('februaryShort'),
|
||||
MailPoet.I18n.t('marchShort'),
|
||||
MailPoet.I18n.t('aprilShort'),
|
||||
MailPoet.I18n.t('mayShort'),
|
||||
MailPoet.I18n.t('juneShort'),
|
||||
MailPoet.I18n.t('julyShort'),
|
||||
MailPoet.I18n.t('augustShort'),
|
||||
MailPoet.I18n.t('septemberShort'),
|
||||
MailPoet.I18n.t('octoberShort'),
|
||||
MailPoet.I18n.t('novemberShort'),
|
||||
MailPoet.I18n.t('decemberShort'),
|
||||
],
|
||||
dayNames: [
|
||||
MailPoet.I18n.t('sunday'),
|
||||
MailPoet.I18n.t('monday'),
|
||||
MailPoet.I18n.t('tuesday'),
|
||||
MailPoet.I18n.t('wednesday'),
|
||||
MailPoet.I18n.t('thursday'),
|
||||
MailPoet.I18n.t('friday'),
|
||||
MailPoet.I18n.t('saturday'),
|
||||
],
|
||||
dayNamesShort: [
|
||||
MailPoet.I18n.t('sundayShort'),
|
||||
MailPoet.I18n.t('mondayShort'),
|
||||
MailPoet.I18n.t('tuesdayShort'),
|
||||
MailPoet.I18n.t('wednesdayShort'),
|
||||
MailPoet.I18n.t('thursdayShort'),
|
||||
MailPoet.I18n.t('fridayShort'),
|
||||
MailPoet.I18n.t('saturdayShort'),
|
||||
],
|
||||
dayNamesMin: [
|
||||
MailPoet.I18n.t('sundayMin'),
|
||||
MailPoet.I18n.t('mondayMin'),
|
||||
MailPoet.I18n.t('tuesdayMin'),
|
||||
MailPoet.I18n.t('wednesdayMin'),
|
||||
MailPoet.I18n.t('thursdayMin'),
|
||||
MailPoet.I18n.t('fridayMin'),
|
||||
MailPoet.I18n.t('saturdayMin'),
|
||||
],
|
||||
};
|
||||
|
||||
const DateText = React.createClass({
|
||||
onChange: function onChange(event) {
|
||||
const changeEvent = event;
|
||||
// Swap display format to storage format
|
||||
const displayDate = changeEvent.target.value;
|
||||
const storageDate = this.getStorageDate(displayDate);
|
||||
|
||||
changeEvent.target.value = storageDate;
|
||||
this.props.onChange(changeEvent);
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
const $element = jQuery(this.dateInput);
|
||||
const that = this;
|
||||
if ($element.datepicker) {
|
||||
// Override jQuery UI datepicker Date parsing and formatting
|
||||
jQuery.datepicker.parseDate = function parseDate(format, value) {
|
||||
// Transform string format to Date object
|
||||
return MailPoet.Date.toDate(value, {
|
||||
parseFormat: dateDisplayFormat,
|
||||
format,
|
||||
});
|
||||
};
|
||||
jQuery.datepicker.formatDate = function formatDate(format, value) {
|
||||
// Transform Date object to string format
|
||||
const newValue = MailPoet.Date.format(value, {
|
||||
format,
|
||||
});
|
||||
return newValue;
|
||||
};
|
||||
|
||||
$element.datepicker(_.extend({
|
||||
dateFormat: this.props.displayFormat,
|
||||
isRTL: false,
|
||||
onSelect: function onSelect(value) {
|
||||
that.onChange({
|
||||
target: {
|
||||
name: that.getFieldName(),
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
}, datepickerTranslations));
|
||||
|
||||
this.datepickerInitialized = true;
|
||||
}
|
||||
},
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
if (this.datepickerInitialized) {
|
||||
jQuery(this.dateInput).datepicker('destroy');
|
||||
}
|
||||
},
|
||||
getFieldName: function getFieldName() {
|
||||
return this.props.name || 'date';
|
||||
},
|
||||
getDisplayDate: function getDisplayDate(date) {
|
||||
return MailPoet.Date.format(date, {
|
||||
parseFormat: this.props.storageFormat,
|
||||
format: this.props.displayFormat,
|
||||
});
|
||||
},
|
||||
getStorageDate: function getStorageDate(date) {
|
||||
return MailPoet.Date.format(date, {
|
||||
parseFormat: this.props.displayFormat,
|
||||
format: this.props.storageFormat,
|
||||
});
|
||||
},
|
||||
render: function render() {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
size="10"
|
||||
name={this.getFieldName()}
|
||||
value={this.getDisplayDate(this.props.value)}
|
||||
readOnly
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.onChange}
|
||||
ref={(c) => { this.dateInput = c; }}
|
||||
{...this.props.validation}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const TimeSelect = React.createClass({
|
||||
render: function render() {
|
||||
const options = Object.keys(timeOfDayItems).map(
|
||||
value => (
|
||||
<option
|
||||
key={`option-${timeOfDayItems[value]}`}
|
||||
value={value}
|
||||
>
|
||||
{ timeOfDayItems[value] }
|
||||
</option>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<select
|
||||
name={this.props.name || 'time'}
|
||||
value={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
{...this.props.validation}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const DateTime = React.createClass({
|
||||
DATE_TIME_SEPARATOR: ' ',
|
||||
getInitialState: function getInitialState() {
|
||||
return this.buildStateFromProps(this.props);
|
||||
},
|
||||
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
|
||||
this.setState(this.buildStateFromProps(nextProps));
|
||||
},
|
||||
buildStateFromProps: function buildStateFromProps(props) {
|
||||
const value = props.value || defaultDateTime;
|
||||
const [date, time] = value.split(this.DATE_TIME_SEPARATOR);
|
||||
return {
|
||||
date,
|
||||
time,
|
||||
class StandardScheduling extends React.Component {
|
||||
getCurrentValue = () => {
|
||||
const schedulingOptions = {
|
||||
isScheduled: '0',
|
||||
scheduledAt: defaultDateTime,
|
||||
};
|
||||
},
|
||||
handleChange: function handleChange(event) {
|
||||
const newState = {};
|
||||
newState[event.target.name] = event.target.value;
|
||||
|
||||
this.setState(newState, this.propagateChange);
|
||||
},
|
||||
propagateChange: function propagateChange() {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange({
|
||||
target: {
|
||||
name: this.props.name || '',
|
||||
value: this.getDateTime(),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
getDateTime: function getDateTime() {
|
||||
return [this.state.date, this.state.time].join(this.DATE_TIME_SEPARATOR);
|
||||
},
|
||||
render: function render() {
|
||||
return (
|
||||
<span>
|
||||
<DateText
|
||||
name="date"
|
||||
value={this.state.date}
|
||||
onChange={this.handleChange}
|
||||
displayFormat={dateDisplayFormat}
|
||||
storageFormat={dateStorageFormat}
|
||||
disabled={this.props.disabled}
|
||||
validation={this.props.dateValidation}
|
||||
/>
|
||||
<TimeSelect
|
||||
name="time"
|
||||
value={this.state.time}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.disabled}
|
||||
validation={this.props.timeValidation}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const StandardScheduling = React.createClass({
|
||||
getCurrentValue: function getCurrentValue() {
|
||||
return _.defaults(
|
||||
this.props.item[this.props.field.name] || {},
|
||||
{
|
||||
isScheduled: '0',
|
||||
scheduledAt: defaultDateTime,
|
||||
}
|
||||
schedulingOptions
|
||||
);
|
||||
},
|
||||
handleValueChange: function handleValueChange(event) {
|
||||
};
|
||||
|
||||
getDateValidation = () => ({
|
||||
'data-parsley-required': true,
|
||||
'data-parsley-required-message': MailPoet.I18n.t('noScheduledDateError'),
|
||||
'data-parsley-errors-container': '#mailpoet_scheduling',
|
||||
});
|
||||
|
||||
isScheduled = () => this.getCurrentValue().isScheduled === '1';
|
||||
|
||||
handleCheckboxChange = (event) => {
|
||||
const changeEvent = event;
|
||||
changeEvent.target.value = this.isScheduledInput.checked ? '1' : '0';
|
||||
return this.handleValueChange(changeEvent);
|
||||
};
|
||||
|
||||
handleValueChange = (event) => {
|
||||
const oldValue = this.getCurrentValue();
|
||||
const newValue = {};
|
||||
newValue[event.target.name] = event.target.value;
|
||||
@ -262,23 +49,9 @@ const StandardScheduling = React.createClass({
|
||||
value: _.extend({}, oldValue, newValue),
|
||||
},
|
||||
});
|
||||
},
|
||||
handleCheckboxChange: function handleCheckboxChange(event) {
|
||||
const changeEvent = event;
|
||||
changeEvent.target.value = this.isScheduledInput.checked ? '1' : '0';
|
||||
return this.handleValueChange(changeEvent);
|
||||
},
|
||||
isScheduled: function isScheduled() {
|
||||
return this.getCurrentValue().isScheduled === '1';
|
||||
},
|
||||
getDateValidation: function getDateValidation() {
|
||||
return {
|
||||
'data-parsley-required': true,
|
||||
'data-parsley-required-message': MailPoet.I18n.t('noScheduledDateError'),
|
||||
'data-parsley-errors-container': '#mailpoet_scheduling',
|
||||
};
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
let schedulingOptions;
|
||||
|
||||
if (this.isScheduled()) {
|
||||
@ -290,6 +63,10 @@ const StandardScheduling = React.createClass({
|
||||
onChange={this.handleValueChange}
|
||||
disabled={this.props.field.disabled}
|
||||
dateValidation={this.getDateValidation()}
|
||||
defaultDateTime={defaultDateTime}
|
||||
timeOfDayItems={timeOfDayItems}
|
||||
dateDisplayFormat={dateDisplayFormat}
|
||||
dateStorageFormat={dateStorageFormat}
|
||||
/>
|
||||
|
||||
<span>
|
||||
@ -313,8 +90,21 @@ const StandardScheduling = React.createClass({
|
||||
{schedulingOptions}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StandardScheduling.propTypes = {
|
||||
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
}).isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
StandardScheduling.defaultProps = {
|
||||
item: {},
|
||||
};
|
||||
|
||||
let fields = [
|
||||
{
|
||||
|
46
assets/js/src/newsletters/send/time_select.jsx
Normal file
46
assets/js/src/newsletters/send/time_select.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class TimeSelect extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||
render() {
|
||||
const options = Object.keys(this.props.timeOfDayItems).map(
|
||||
value => (
|
||||
<option
|
||||
key={`option-${this.props.timeOfDayItems[value]}`}
|
||||
value={value}
|
||||
>
|
||||
{ this.props.timeOfDayItems[value] }
|
||||
</option>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<select
|
||||
name={this.props.name || 'time'}
|
||||
value={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
{...this.props.validation}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSelect.propTypes = {
|
||||
timeOfDayItems: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
validation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
TimeSelect.defaultProps = {
|
||||
name: 'time',
|
||||
disabled: false,
|
||||
validation: {},
|
||||
};
|
||||
|
||||
module.exports = TimeSelect;
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Breadcrumb from 'newsletters/breadcrumb.jsx';
|
||||
@ -5,11 +6,12 @@ import Hooks from 'wp-js-hooks';
|
||||
import _ from 'underscore';
|
||||
import 'react-router';
|
||||
|
||||
const NewsletterTypes = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
setupNewsletter: function setupNewsletter(type) {
|
||||
class NewsletterTypes extends React.Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
setupNewsletter = (type) => {
|
||||
if (type !== undefined) {
|
||||
this.context.router.push(`/new/${type}`);
|
||||
MailPoet.trackEvent('Emails > Type selected', {
|
||||
@ -17,8 +19,32 @@ const NewsletterTypes = React.createClass({
|
||||
'Email type': type,
|
||||
});
|
||||
}
|
||||
},
|
||||
createNewsletter: function createNewsletter(type) {
|
||||
};
|
||||
|
||||
getAutomaticEmails = () => {
|
||||
if (!window.mailpoet_automatic_emails) return [];
|
||||
|
||||
return _.map(window.mailpoet_automatic_emails, (automaticEmail) => {
|
||||
const email = automaticEmail;
|
||||
const onClick = _.partial(this.setupNewsletter, automaticEmail.slug);
|
||||
email.action = (() => (
|
||||
<div>
|
||||
<a
|
||||
className="button button-primary"
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{ MailPoet.I18n.t('setUp') }
|
||||
</a>
|
||||
</div>
|
||||
))();
|
||||
|
||||
return email;
|
||||
});
|
||||
};
|
||||
|
||||
createNewsletter = (type) => {
|
||||
MailPoet.trackEvent('Emails > Type selected', {
|
||||
'MailPoet Free version': window.mailpoet_version,
|
||||
'Email type': type,
|
||||
@ -41,30 +67,9 @@ const NewsletterTypes = React.createClass({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
getAutomaticEmails: function getAutomaticEmails() {
|
||||
if (!window.mailpoet_automatic_emails) return [];
|
||||
};
|
||||
|
||||
return _.map(window.mailpoet_automatic_emails, (automaticEmail) => {
|
||||
const email = automaticEmail;
|
||||
const onClick = _.partial(this.setupNewsletter, automaticEmail.slug);
|
||||
email.action = (() => (
|
||||
<div>
|
||||
<a
|
||||
className="button button-primary"
|
||||
onClick={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{ MailPoet.I18n.t('setUp') }
|
||||
</a>
|
||||
</div>
|
||||
))();
|
||||
|
||||
return email;
|
||||
});
|
||||
},
|
||||
render: function render() {
|
||||
render() {
|
||||
const createStandardNewsletter = _.partial(this.createNewsletter, 'standard');
|
||||
const createNotificationNewsletter = _.partial(this.setupNewsletter, 'notification');
|
||||
const createWelcomeNewsletter = _.partial(this.setupNewsletter, 'welcome');
|
||||
@ -169,7 +174,7 @@ const NewsletterTypes = React.createClass({
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewsletterTypes;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Breadcrumb from 'newsletters/breadcrumb.jsx';
|
||||
@ -10,27 +11,28 @@ const field = {
|
||||
component: Scheduling,
|
||||
};
|
||||
|
||||
const NewsletterNotification = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
options: {
|
||||
intervalType: 'daily',
|
||||
timeOfDay: 0,
|
||||
weekDay: 1,
|
||||
monthDay: 0,
|
||||
nthWeekDay: 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
handleValueChange: function handleValueChange(event) {
|
||||
class NewsletterNotification extends React.Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
options: {
|
||||
intervalType: 'daily',
|
||||
timeOfDay: 0,
|
||||
weekDay: 1,
|
||||
monthDay: 0,
|
||||
nthWeekDay: 1,
|
||||
},
|
||||
};
|
||||
|
||||
handleValueChange = (event) => {
|
||||
const state = this.state;
|
||||
state[event.target.name] = event.target.value;
|
||||
this.setState(state);
|
||||
},
|
||||
handleNext: function handleNext() {
|
||||
};
|
||||
|
||||
handleNext = () => {
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
@ -49,11 +51,13 @@ const NewsletterNotification = React.createClass({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
showTemplateSelection: function showTemplateSelection(newsletterId) {
|
||||
};
|
||||
|
||||
showTemplateSelection = (newsletterId) => {
|
||||
this.context.router.push(`/template/${newsletterId}`);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>{MailPoet.I18n.t('postNotificationNewsletterTypeTitle')}</h1>
|
||||
@ -77,8 +81,8 @@ const NewsletterNotification = React.createClass({
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewsletterNotification;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import _ from 'underscore';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'form/fields/select.jsx';
|
||||
import {
|
||||
intervalValues,
|
||||
@ -34,11 +35,10 @@ const nthWeekDayField = {
|
||||
values: nthWeekDayValues,
|
||||
};
|
||||
|
||||
const NotificationScheduling = React.createClass({
|
||||
getCurrentValue: function getCurrentValue() {
|
||||
return (this.props.item[this.props.field.name] || {});
|
||||
},
|
||||
handleValueChange: function handleValueChange(name, value) {
|
||||
class NotificationScheduling extends React.Component {
|
||||
getCurrentValue = () => this.props.item[this.props.field.name] || {};
|
||||
|
||||
handleValueChange = (name, value) => {
|
||||
const oldValue = this.getCurrentValue();
|
||||
const newValue = {};
|
||||
|
||||
@ -50,38 +50,15 @@ const NotificationScheduling = React.createClass({
|
||||
value: _.extend({}, oldValue, newValue),
|
||||
},
|
||||
});
|
||||
},
|
||||
handleIntervalChange: function handleIntervalChange(event) {
|
||||
return this.handleValueChange(
|
||||
'intervalType',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleTimeOfDayChange: function handleTimeOfDayChange(event) {
|
||||
return this.handleValueChange(
|
||||
'timeOfDay',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleWeekDayChange: function handleWeekDayChange(event) {
|
||||
return this.handleValueChange(
|
||||
'weekDay',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleMonthDayChange: function handleMonthDayChange(event) {
|
||||
return this.handleValueChange(
|
||||
'monthDay',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleNthWeekDayChange: function handleNthWeekDayChange(event) {
|
||||
return this.handleValueChange(
|
||||
'nthWeekDay',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
handleIntervalChange = event => this.handleValueChange('intervalType', event.target.value);
|
||||
handleTimeOfDayChange = event => this.handleValueChange('timeOfDay', event.target.value);
|
||||
handleWeekDayChange = event => this.handleValueChange('weekDay', event.target.value);
|
||||
handleMonthDayChange = event => this.handleValueChange('monthDay', event.target.value);
|
||||
handleNthWeekDayChange = event => this.handleValueChange('nthWeekDay', event.target.value);
|
||||
|
||||
render() {
|
||||
const value = this.getCurrentValue();
|
||||
let timeOfDaySelection;
|
||||
let weekDaySelection;
|
||||
@ -143,7 +120,15 @@ const NotificationScheduling = React.createClass({
|
||||
{timeOfDaySelection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NotificationScheduling.propTypes = {
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = NotificationScheduling;
|
||||
|
@ -1,15 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import Breadcrumb from 'newsletters/breadcrumb.jsx';
|
||||
|
||||
const NewsletterStandard = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
showTemplateSelection: function showTemplateSelection(newsletterId) {
|
||||
this.context.router.push(`/template/${newsletterId}`);
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
class NewsletterStandard extends React.Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// No options for this type, create a newsletter upon mounting
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
@ -28,16 +27,21 @@ const NewsletterStandard = React.createClass({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function render() {
|
||||
}
|
||||
|
||||
showTemplateSelection = (newsletterId) => {
|
||||
this.context.router.push(`/template/${newsletterId}`);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>{MailPoet.I18n.t('regularNewsletterTypeTitle')}</h1>
|
||||
<Breadcrumb step="type" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewsletterStandard;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import MailPoet from 'mailpoet';
|
||||
import Select from 'form/fields/select.jsx';
|
||||
import Text from 'form/fields/text.jsx';
|
||||
import { timeDelayValues } from 'newsletters/scheduling/common.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const availableRoles = window.mailpoet_roles || {};
|
||||
const availableSegments = _.filter(
|
||||
@ -47,14 +48,14 @@ const afterTimeTypeField = {
|
||||
values: timeDelayValues,
|
||||
};
|
||||
|
||||
const WelcomeScheduling = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
getCurrentValue: function getCurrentValue() {
|
||||
return (this.props.item[this.props.field.name] || {});
|
||||
},
|
||||
handleValueChange: function handleValueChange(name, value) {
|
||||
class WelcomeScheduling extends React.Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
getCurrentValue = () => this.props.item[this.props.field.name] || {};
|
||||
|
||||
handleValueChange = (name, value) => {
|
||||
const oldValue = this.getCurrentValue();
|
||||
const newValue = {};
|
||||
|
||||
@ -66,38 +67,15 @@ const WelcomeScheduling = React.createClass({
|
||||
value: _.extend({}, oldValue, newValue),
|
||||
},
|
||||
});
|
||||
},
|
||||
handleEventChange: function handleEventChange(event) {
|
||||
return this.handleValueChange(
|
||||
'event',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleSegmentChange: function handleSegmentChange(event) {
|
||||
return this.handleValueChange(
|
||||
'segment',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleRoleChange: function handleRoleChange(event) {
|
||||
return this.handleValueChange(
|
||||
'role',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleAfterTimeNumberChange: function handleAfterTimeNumberChange(event) {
|
||||
return this.handleValueChange(
|
||||
'afterTimeNumber',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleAfterTimeTypeChange: function handleAfterTimeTypeChange(event) {
|
||||
return this.handleValueChange(
|
||||
'afterTimeType',
|
||||
event.target.value
|
||||
);
|
||||
},
|
||||
handleNext: function handleNext() {
|
||||
};
|
||||
|
||||
handleEventChange = event => this.handleValueChange('event', event.target.value);
|
||||
handleSegmentChange = event => this.handleValueChange('segment', event.target.value);
|
||||
handleRoleChange = event => this.handleValueChange('role', event.target.value);
|
||||
handleAfterTimeNumberChange = event => this.handleValueChange('afterTimeNumber', event.target.value);
|
||||
handleAfterTimeTypeChange = event => this.handleValueChange('afterTimeType', event.target.value);
|
||||
|
||||
handleNext = () => {
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
@ -116,11 +94,13 @@ const WelcomeScheduling = React.createClass({
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
showTemplateSelection: function showTemplateSelection(newsletterId) {
|
||||
};
|
||||
|
||||
showTemplateSelection = (newsletterId) => {
|
||||
this.context.router.push(`/template/${newsletterId}`);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
const value = this.getCurrentValue();
|
||||
let roleSegmentSelection;
|
||||
let timeNumber;
|
||||
@ -171,7 +151,15 @@ const WelcomeScheduling = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeScheduling.propTypes = {
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
onValueChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = WelcomeScheduling;
|
||||
|
@ -1,12 +1,27 @@
|
||||
function displayPoll() {
|
||||
if (window.mailpoet_display_nps_poll && window.satismeter) {
|
||||
if (
|
||||
window.mailpoet_display_nps_poll
|
||||
&& window.satismeter
|
||||
&& window.mailpoet_installed_at_isoFormat
|
||||
) {
|
||||
// New users poll
|
||||
window.satismeter({
|
||||
writeKey: '6L479eVPXk7pBn6S',
|
||||
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
|
||||
traits: {
|
||||
name: window.mailpoet_current_wp_user.user_nicename,
|
||||
email: window.mailpoet_current_wp_user.user_email,
|
||||
createdAt: window.mailpoet_settings.installed_at,
|
||||
createdAt: window.mailpoet_installed_at_isoFormat,
|
||||
},
|
||||
});
|
||||
// Old users poll
|
||||
window.satismeter({
|
||||
writeKey: 'k0aJAsQAWI2ERyGv',
|
||||
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
|
||||
traits: {
|
||||
name: window.mailpoet_current_wp_user.user_nicename,
|
||||
email: window.mailpoet_current_wp_user.user_email,
|
||||
createdAt: window.mailpoet_installed_at_isoFormat,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import MailPoet from 'mailpoet';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
|
||||
@ -191,8 +192,8 @@ const itemActions = [
|
||||
},
|
||||
];
|
||||
|
||||
const SegmentList = React.createClass({
|
||||
renderItem: function renderItem(segment, actions) {
|
||||
class SegmentList extends React.Component {
|
||||
renderItem = (segment, actions) => {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
@ -248,8 +249,9 @@ const SegmentList = React.createClass({
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
@ -272,7 +274,12 @@ const SegmentList = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SegmentList.propTypes = {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
module.exports = SegmentList;
|
||||
|
@ -2,17 +2,22 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
|
||||
import { createHashHistory } from 'history';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import SegmentList from 'segments/list.jsx';
|
||||
import SegmentForm from 'segments/form.jsx';
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
const App = React.createClass({
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const container = document.getElementById('segments_container');
|
||||
|
||||
|
14
assets/js/src/settings/announcement.jsx
Normal file
14
assets/js/src/settings/announcement.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import Announcement from './new_subscriber_announcement.jsx';
|
||||
|
||||
const container = document.getElementById('new_subscriber_announcement');
|
||||
|
||||
if (container) {
|
||||
ReactDOM.render(
|
||||
<Announcement
|
||||
installedAt={window.mailpoet_installed_at}
|
||||
imageUrl={window.mailpoet_new_subscriber_announcement_image}
|
||||
/>, container
|
||||
);
|
||||
}
|
30
assets/js/src/settings/new_subscriber_announcement.jsx
Normal file
30
assets/js/src/settings/new_subscriber_announcement.jsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import moment from 'moment';
|
||||
import InAppAnnouncement from 'in_app_announcements/in_app_announcement.jsx';
|
||||
|
||||
const NewSubscriberNotificationAnnouncement = props => (
|
||||
<InAppAnnouncement
|
||||
validUntil={moment(props.installedAt).add(3, 'months').toDate()}
|
||||
height="700px"
|
||||
showOnlyOnceSlug="new_subscriber_notification"
|
||||
showToNewUser={false}
|
||||
>
|
||||
<div className="new_subscriber_notification_announcement">
|
||||
<h1>{MailPoet.I18n.t('announcementHeader')}</h1>
|
||||
<img src={props.imageUrl} width="600px" height="460px" alt="" />
|
||||
<p>
|
||||
{MailPoet.I18n.t('announcementParagraph1')}<br />
|
||||
{MailPoet.I18n.t('announcementParagraph2')}
|
||||
</p>
|
||||
</div>
|
||||
</InAppAnnouncement>
|
||||
);
|
||||
|
||||
NewSubscriberNotificationAnnouncement.propTypes = {
|
||||
installedAt: PropTypes.string.isRequired,
|
||||
imageUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = NewSubscriberNotificationAnnouncement;
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
import Form from 'form/form.jsx';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
|
||||
@ -175,8 +176,8 @@ function afterFormContent() {
|
||||
);
|
||||
}
|
||||
|
||||
const SubscriberForm = React.createClass({
|
||||
render: function render() {
|
||||
class SubscriberForm extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
@ -194,7 +195,11 @@ const SubscriberForm = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SubscriberForm.propTypes = {
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
module.exports = SubscriberForm;
|
||||
|
@ -4,6 +4,7 @@ import { Link } from 'react-router';
|
||||
import jQuery from 'jquery';
|
||||
import MailPoet from 'mailpoet';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import Selection from 'form/fields/selection.jsx';
|
||||
@ -242,8 +243,8 @@ const itemActions = [
|
||||
},
|
||||
];
|
||||
|
||||
const SubscriberList = React.createClass({
|
||||
getSegmentFromId: function getSegmentFromId(segmentId) {
|
||||
class SubscriberList extends React.Component {
|
||||
getSegmentFromId = (segmentId) => {
|
||||
let result = false;
|
||||
window.mailpoet_segments.forEach((segment) => {
|
||||
if (segment.id === segmentId) {
|
||||
@ -251,8 +252,9 @@ const SubscriberList = React.createClass({
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
renderItem: function renderItem(subscriber, actions) {
|
||||
};
|
||||
|
||||
renderItem = (subscriber, actions) => {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
@ -333,8 +335,9 @@ const SubscriberList = React.createClass({
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: function render() {
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
@ -368,7 +371,12 @@ const SubscriberList = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SubscriberList.propTypes = {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
module.exports = SubscriberList;
|
||||
|
@ -2,16 +2,21 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
|
||||
import { createHashHistory } from 'history';
|
||||
import PropTypes from 'prop-types';
|
||||
import SubscriberList from 'subscribers/list.jsx';
|
||||
import SubscriberForm from 'subscribers/form.jsx';
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
const App = React.createClass({
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
const container = document.getElementById('subscribers_container');
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SteppedProgressBar from '../common/stepped_progess_bar.jsx';
|
||||
|
||||
@ -13,9 +14,9 @@ const WelcomeWizardHeader = props => (
|
||||
);
|
||||
|
||||
WelcomeWizardHeader.propTypes = {
|
||||
current_step: React.PropTypes.number.isRequired,
|
||||
steps_count: React.PropTypes.number.isRequired,
|
||||
logo_src: React.PropTypes.string.isRequired,
|
||||
current_step: PropTypes.number.isRequired,
|
||||
steps_count: PropTypes.number.isRequired,
|
||||
logo_src: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = WelcomeWizardHeader;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
@ -62,7 +63,7 @@ const WelcomeWizardHelpInfoStep = props => (
|
||||
module.exports = WelcomeWizardHelpInfoStep;
|
||||
|
||||
WelcomeWizardHelpInfoStep.propTypes = {
|
||||
next: React.PropTypes.func.isRequired,
|
||||
next: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = WelcomeWizardHelpInfoStep;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
@ -12,7 +13,7 @@ const WelcomeWizardMigratedUserStep = props => (
|
||||
);
|
||||
|
||||
WelcomeWizardMigratedUserStep.propTypes = {
|
||||
next: React.PropTypes.func.isRequired,
|
||||
next: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = WelcomeWizardMigratedUserStep;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import jQuery from 'jquery';
|
||||
@ -47,13 +48,13 @@ const WelcomeWizardSenderStep = props => (
|
||||
);
|
||||
|
||||
WelcomeWizardSenderStep.propTypes = {
|
||||
finish: React.PropTypes.func.isRequired,
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
update_sender: React.PropTypes.func.isRequired,
|
||||
submit_sender: React.PropTypes.func.isRequired,
|
||||
sender: React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
address: React.PropTypes.string,
|
||||
finish: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
update_sender: PropTypes.func.isRequired,
|
||||
submit_sender: PropTypes.func.isRequired,
|
||||
sender: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
address: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
@ -48,8 +49,8 @@ const WelcomeWizardUsageTrackingStep = props => (
|
||||
module.exports = WelcomeWizardUsageTrackingStep;
|
||||
|
||||
WelcomeWizardUsageTrackingStep.propTypes = {
|
||||
allow_action: React.PropTypes.func.isRequired,
|
||||
allow_text: React.PropTypes.string.isRequired,
|
||||
skip_action: React.PropTypes.func.isRequired,
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
allow_action: PropTypes.func.isRequired,
|
||||
allow_text: PropTypes.string.isRequired,
|
||||
skip_action: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user