Compare commits
433 Commits
Author | SHA1 | Date | |
---|---|---|---|
fdcd343ed5 | |||
42bec13574 | |||
7c4afd5aa8 | |||
c4952a636a | |||
e54f1d6263 | |||
4c5e7095b1 | |||
bb9604f22f | |||
2bec938336 | |||
3dd37fa5b7 | |||
5224009301 | |||
deb2c74a3a | |||
ba58022259 | |||
a699b0db8e | |||
9a1cd26a40 | |||
5f07b2a2b2 | |||
f930db422e | |||
b546344e42 | |||
dde3b159ea | |||
9a74dec86a | |||
dce1b1403c | |||
ffc7773a6a | |||
34d94edd3e | |||
8861909859 | |||
4bdec5165d | |||
5bd994c01c | |||
cd3e2556dd | |||
4dd0218ce1 | |||
9930f2915c | |||
5547613f31 | |||
6342402e41 | |||
24cca1ba04 | |||
7ce53adc33 | |||
84ca7143ff | |||
5c08d22736 | |||
558d709a18 | |||
3cc385dd52 | |||
e843d25c8b | |||
a471e4d8c4 | |||
26a6f1be32 | |||
53434d23b8 | |||
83f5c6cc46 | |||
ed56df2398 | |||
8b78fdd4b4 | |||
baa4d369af | |||
1140ee3129 | |||
804ad97036 | |||
b4bba9869e | |||
8567614697 | |||
52795c3972 | |||
b7cccdae95 | |||
873c295e99 | |||
e3b02a9a64 | |||
18b1d9799f | |||
c22d23f91c | |||
e30e1847e2 | |||
73d15c8c44 | |||
bf1ddbedbc | |||
4687fcd44f | |||
5f88a27ccb | |||
bfc2a7d2af | |||
c1e9f86a32 | |||
5dfda9f3e2 | |||
77eaa7e710 | |||
a39b73041c | |||
a841671695 | |||
a362b7e014 | |||
bf74a506b7 | |||
5f30c7a511 | |||
7ef341e38a | |||
6f1e5c9a98 | |||
d54129670d | |||
f19de77f70 | |||
cfc98c7aac | |||
e3f13dd986 | |||
79e11fe939 | |||
2356c62be3 | |||
07fa471ac3 | |||
5d63e9a462 | |||
f060219da3 | |||
5a3dc190d6 | |||
1a5952a876 | |||
8ca1a0a9b3 | |||
6dbc50eb23 | |||
1e1153bd77 | |||
24849afb7a | |||
2a09646e8d | |||
70081a846d | |||
1c138a7d4b | |||
33fb02e2ce | |||
ae08a9e18e | |||
e2e07bba42 | |||
6b238618bf | |||
e37e26e380 | |||
b5690f3d6c | |||
4a19e4db46 | |||
d4bc448ac6 | |||
1bf751978c | |||
5d3c851d02 | |||
877fd9d7da | |||
c3251f4092 | |||
fbaf37ed0c | |||
2ecf5fc2fa | |||
6127336eab | |||
d8770675a4 | |||
ea1bbf7b70 | |||
2982fa3277 | |||
1093d089fc | |||
7b12e00ae4 | |||
13a79d0b77 | |||
16217db6cd | |||
2a8dca6454 | |||
c8ac0ac4a7 | |||
b877bd4577 | |||
59315d03c7 | |||
a01afc27e8 | |||
3886dd83d1 | |||
72aa540b23 | |||
697e7eba9a | |||
922beb790f | |||
a5b737ac86 | |||
f89b645ce2 | |||
cf1982a410 | |||
8d29c2df49 | |||
d874375c25 | |||
efab3be9ae | |||
98e056bec7 | |||
9a5fbfbb24 | |||
d828754f94 | |||
fdbc6ea603 | |||
90ff2db0d1 | |||
968cbb67d8 | |||
e03e375942 | |||
d625457174 | |||
574c1cdd8f | |||
c58c613959 | |||
f4346c8957 | |||
543465d9de | |||
1642a5477c | |||
e36f105387 | |||
6411182705 | |||
1ede10fce6 | |||
baea06abf5 | |||
39b431b7b3 | |||
866c22c336 | |||
de2b0c34b6 | |||
838903bc41 | |||
aee63444fe | |||
d389595d72 | |||
0fe260f605 | |||
67b6d2279f | |||
af203ba29e | |||
d0b1ff3aef | |||
fd1ac46207 | |||
4ffdd5bed5 | |||
ccb070c500 | |||
fcd12b41b5 | |||
449fde75a7 | |||
bbd27a5b3e | |||
f09616dc46 | |||
55f10a403c | |||
bd5ea1041d | |||
25013e8c8c | |||
fcfcbcc4d7 | |||
f9588484a6 | |||
1a0796c627 | |||
847d199352 | |||
5c3a9fe9b2 | |||
aba68d2305 | |||
9e5f1d0ff0 | |||
370de8050a | |||
c961e0a363 | |||
601b3e775b | |||
417c0897e8 | |||
02af3d6c2e | |||
42dce4280a | |||
0d1b51c7ba | |||
9f2ae81835 | |||
293c1f4a46 | |||
ee54440c03 | |||
ea07bbe9fa | |||
f7722e4349 | |||
7a0a6413d5 | |||
457d361ee9 | |||
3885194b48 | |||
7f78c387cf | |||
7e9149f2aa | |||
02489a61a6 | |||
456417a467 | |||
964f3ca69d | |||
c7f10a18c4 | |||
8432cfa904 | |||
50d77741b7 | |||
4814e0a8e8 | |||
d102556d28 | |||
839265663e | |||
5148e2dca7 | |||
5214a25a6f | |||
08f62f4590 | |||
6791375807 | |||
9bbdc160ea | |||
c37173a30e | |||
2ae02db229 | |||
641a1cda47 | |||
7fde0b6bdc | |||
54a721b751 | |||
402cf63e6b | |||
45b344727c | |||
41a99f15f6 | |||
97e160bf67 | |||
76048224d1 | |||
218de96024 | |||
b05e6d414c | |||
0b2b211b08 | |||
5d1f49b1bd | |||
6532cc1bec | |||
3a383fdd82 | |||
7c71255231 | |||
a1ef5f9568 | |||
e7534132d3 | |||
d1fddb85f7 | |||
24a63f6448 | |||
08e8e88502 | |||
1f6ce667e5 | |||
0dbfb196e2 | |||
4b5afeac54 | |||
bb24838881 | |||
0d2c82ad72 | |||
72b392fcd5 | |||
8319c1e4b6 | |||
c02e71c6e6 | |||
0792bae953 | |||
4389d3ca5f | |||
15e4635fe3 | |||
da58a5c416 | |||
87f35abf8b | |||
a6cbf99ee5 | |||
2faf0739a3 | |||
cc2f908236 | |||
ebe8d5d478 | |||
4aa323b612 | |||
fbd27afedf | |||
ca308321fd | |||
c9aa53df8b | |||
fc11b2f97e | |||
18501385dc | |||
c917505170 | |||
0b14a03236 | |||
2b5d089a79 | |||
5445bf9305 | |||
98d9952a44 | |||
f899eb1c16 | |||
e93befc965 | |||
c8278dde2a | |||
1d4ec47b25 | |||
9c5dca0206 | |||
73f51523f3 | |||
d3abbddf73 | |||
56b90247b2 | |||
4a8ac483f6 | |||
af396021db | |||
0f7a3c3e00 | |||
8a29519b31 | |||
261f2eaa25 | |||
4a0ee78467 | |||
de4e76b65d | |||
ffecdf8528 | |||
ef89cae94a | |||
6a5207a366 | |||
13e06fe0cb | |||
57c02a2fd3 | |||
ac473c412e | |||
679d586d2b | |||
85e7997197 | |||
d225f4e044 | |||
e7b32bdd4a | |||
b89a931e1a | |||
c170641d83 | |||
35d9a6b2d3 | |||
6777a9b560 | |||
bf5d27cc79 | |||
7f525068b3 | |||
243e1bdac8 | |||
6671c573b8 | |||
75fce3800c | |||
be220c4782 | |||
b891ca4164 | |||
ba45281324 | |||
45e1db5893 | |||
db254986ee | |||
8e9174442c | |||
c44758b90d | |||
16807fe9e3 | |||
d742164f21 | |||
aa030fde9c | |||
869697e025 | |||
d6afe1c269 | |||
977f9beb87 | |||
2df7d2f686 | |||
d3e2bcdf34 | |||
476ee1ede9 | |||
4f01d54742 | |||
bb1d461c54 | |||
dfae17ed94 | |||
f745975c03 | |||
2ba1696d53 | |||
91e72ee646 | |||
f178ea5f7a | |||
cacb44444c | |||
ddb63bfa9f | |||
b525f9f876 | |||
138354cfd8 | |||
95ed809db0 | |||
28f497ec61 | |||
272e875643 | |||
e3fbf2421d | |||
791a0631b5 | |||
2c2c9dba6a | |||
eedc18bb7f | |||
5deea8a724 | |||
b979c76445 | |||
b02aa1e1a6 | |||
86562e99b9 | |||
f59945d483 | |||
7e5d632148 | |||
8d05da2c37 | |||
2b44684c38 | |||
ccfdd229ec | |||
0ac617abf7 | |||
b2a5f47da7 | |||
c9a464c4c9 | |||
ace82c9abb | |||
c6164ad88f | |||
78460bd248 | |||
39ca2343b8 | |||
1fbf29b7f6 | |||
7fe4104af8 | |||
3e0b5b0f76 | |||
7f6159501a | |||
81b1c70a28 | |||
9a7e99602a | |||
7f428c0c7a | |||
ec58c24228 | |||
e8d3edcc23 | |||
03bf653432 | |||
83c7f7bc7f | |||
a9a35333c4 | |||
a7bda3f465 | |||
e0f6751538 | |||
db112dc03f | |||
e706110230 | |||
b2718940df | |||
3ef32787cc | |||
5b0e7128c6 | |||
31aa18868b | |||
10d4eb5d7f | |||
6b12255bb8 | |||
84a6401590 | |||
381565fc1f | |||
deaf293273 | |||
fb05cc4297 | |||
155445b313 | |||
ce319e69c6 | |||
c30fe1305b | |||
351c4ca565 | |||
6896758f13 | |||
c4cd053b34 | |||
8d4799b050 | |||
da4d72be50 | |||
5042cdd15e | |||
3b5d737143 | |||
1e1ef12d42 | |||
09160a58ff | |||
3d4155f6cc | |||
e53505354a | |||
1396a979f5 | |||
6525f3a55c | |||
f27326b590 | |||
5985a10c8f | |||
3684530056 | |||
81051e8295 | |||
22010874b7 | |||
7d2fa9ff16 | |||
ae13fcc771 | |||
932512b720 | |||
f6fbcee0d0 | |||
15de269da0 | |||
2f6dc17228 | |||
831fb46c48 | |||
026aa684a5 | |||
5f0f28a5c2 | |||
ef477bc156 | |||
955731eeb5 | |||
c141c9ea15 | |||
27e759b525 | |||
f88ec9bb82 | |||
4f7b90bba3 | |||
6faa8e30e1 | |||
4c51001558 | |||
7dfb55e103 | |||
06bd80eb85 | |||
67264710ed | |||
2e6248b63b | |||
e2a494e98e | |||
7c373a85ba | |||
052e175b76 | |||
2357e7493d | |||
89010304da | |||
dae3080f08 | |||
c41ab7464c | |||
c83a3b7d8d | |||
ea996b30a7 | |||
5b154ce26e | |||
30aad243aa | |||
ef161311bf | |||
7da2d2f7c8 | |||
55fb3f6c82 | |||
c44230b49f | |||
e2db0542ff | |||
8aab1708c1 | |||
d998e48b62 | |||
c484514d88 | |||
569df2be9c | |||
03bc8cee03 | |||
b313c74901 | |||
bc502d8046 | |||
47ccae2e6b | |||
0fcef3c209 | |||
4e9acb524d | |||
4406d72980 | |||
664594d1bd | |||
db6f0d14b1 | |||
1801f522f6 | |||
e5b0b4ae1d |
@ -92,7 +92,7 @@ executors:
|
||||
wpcli_php_latest:
|
||||
<<: *default_job_config
|
||||
docker:
|
||||
- image: mailpoet/wordpress:8.1_20220309.1
|
||||
- image: mailpoet/wordpress:8.1_20220718.1
|
||||
|
||||
wpcli_php_mysql_oldest:
|
||||
<<: *default_job_config
|
||||
@ -103,7 +103,7 @@ executors:
|
||||
wpcli_php_mysql_latest:
|
||||
<<: *default_job_config
|
||||
docker:
|
||||
- image: mailpoet/wordpress:8.1_20220309.1
|
||||
- image: mailpoet/wordpress:8.1_20220718.1
|
||||
- image: cimg/mysql:8.0
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_520_ci
|
||||
|
||||
@ -115,6 +115,9 @@ jobs:
|
||||
steps:
|
||||
- checkout:
|
||||
path: /home/circleci/mailpoet
|
||||
- run:
|
||||
name: 'Set PNPM store directory'
|
||||
command: pnpm config set store-dir ~/.pnpm-store
|
||||
- run:
|
||||
name: 'Compute checksum for prefixer'
|
||||
command: find prefixer -type f -not -path 'prefixer/build/*' -not -path 'prefixer/vendor/*' | sort | xargs cat | sha512sum > prefixer-checksum
|
||||
@ -130,8 +133,8 @@ jobs:
|
||||
key: composer-prefixed-{{ checksum "prefixer-checksum" }}
|
||||
- restore_cache:
|
||||
keys:
|
||||
- npm-{{ checksum "package-lock.json" }}
|
||||
- npm- # fallback to most recent npm-* if not found by checksum
|
||||
- pnpm-{{ checksum "../pnpm-lock.yaml" }}
|
||||
- pnpm- # fallback to most recent pnpm-* if not found by checksum
|
||||
- run:
|
||||
name: 'Set up test environment'
|
||||
command: |
|
||||
@ -170,9 +173,9 @@ jobs:
|
||||
- prefixer/vendor
|
||||
- vendor-prefixed
|
||||
- save_cache:
|
||||
key: npm-{{ checksum "package-lock.json" }}
|
||||
key: pnpm-{{ checksum "../pnpm-lock.yaml" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- ~/.pnpm-store
|
||||
- run:
|
||||
name: Download additional WP Plugins for tests
|
||||
command: |
|
||||
@ -185,16 +188,17 @@ jobs:
|
||||
command: |
|
||||
(printenv | grep WP_TEST_ > .env) || true
|
||||
- persist_to_workspace:
|
||||
root: /home/circleci/mailpoet
|
||||
root: /home/circleci
|
||||
paths:
|
||||
- .
|
||||
- .node
|
||||
- mailpoet
|
||||
build_premium:
|
||||
executor: wpcli_php_latest
|
||||
resource_class: medium
|
||||
working_directory: /home/circleci/mailpoet
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- add_ssh_keys
|
||||
- run:
|
||||
name: 'Install Premium plugin'
|
||||
@ -226,9 +230,11 @@ jobs:
|
||||
paths:
|
||||
- mailpoet-premium/vendor
|
||||
- persist_to_workspace:
|
||||
root: /home/circleci/mailpoet
|
||||
root: /home/circleci
|
||||
paths:
|
||||
- .
|
||||
- .node
|
||||
- mailpoet
|
||||
|
||||
static_analysis:
|
||||
executor: wpcli_php_latest
|
||||
resource_class: medium
|
||||
@ -239,7 +245,7 @@ jobs:
|
||||
default: 70200
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Static analysis'
|
||||
command: ./do qa:phpstan --php-version=<< parameters.php_version >>
|
||||
@ -248,7 +254,7 @@ jobs:
|
||||
working_directory: /home/circleci/mailpoet/mailpoet
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'QA Frontend Assets'
|
||||
command: ./do qa:frontend-assets
|
||||
@ -257,7 +263,7 @@ jobs:
|
||||
working_directory: /home/circleci/mailpoet/mailpoet
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'QA PHP'
|
||||
command: ./do qa:php
|
||||
@ -266,7 +272,7 @@ jobs:
|
||||
working_directory: /home/circleci/mailpoet/mailpoet
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'QA PHP'
|
||||
command: ./do qa:php
|
||||
@ -274,7 +280,7 @@ jobs:
|
||||
executor: wpcli_php_max_wporg
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'QA PHP'
|
||||
command: ./do qa:php-max-wporg
|
||||
@ -283,7 +289,7 @@ jobs:
|
||||
working_directory: /home/circleci/mailpoet/mailpoet
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Preparing test results folder'
|
||||
command: mkdir test-results
|
||||
@ -341,7 +347,7 @@ jobs:
|
||||
WORDPRESS_IMAGE_VERSION: << parameters.wordpress_image_version >>
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Set up virtual host'
|
||||
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
|
||||
@ -415,7 +421,7 @@ jobs:
|
||||
executor: << parameters.executor >>
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Set up virtual host'
|
||||
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
|
||||
@ -474,7 +480,7 @@ jobs:
|
||||
codeception_integration --steps --debug -vvv --html --xml
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Pull test docker images'
|
||||
# Pull docker images with 3 retries
|
||||
@ -495,7 +501,7 @@ jobs:
|
||||
resource_class: medium+
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /home/circleci/mailpoet
|
||||
at: /home/circleci
|
||||
- run:
|
||||
name: 'Set up environment'
|
||||
command: |
|
||||
|
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
save-workspace-protocol=rolling
|
||||
strict-peer-dependencies=false
|
@ -6,6 +6,7 @@ _output
|
||||
composer.json
|
||||
composer.lock
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
vendor
|
||||
vendor-prefixed
|
||||
/.mp_svn
|
||||
|
10
README.md
10
README.md
@ -12,6 +12,16 @@ If you'd like to use the plugin code directly, see details in [the plugin's read
|
||||
3. Run `./do start` to start the stack.
|
||||
4. Go to http://localhost:8888 to see the dashboard of the dev environment.
|
||||
|
||||
## ✅ Additional dependencies
|
||||
|
||||
Even though it possible to run everything using Docker, in the development workflow,
|
||||
it may be faster and more convenient to run some tasks outside the container. Therefore,
|
||||
the following tools are recommended:
|
||||
|
||||
1. **PHP** as per `composer.json` requirements.
|
||||
2. **Node.js**, as specified by `.nvmrc`. For automatic management use [nvm](https://github.com/nvm-sh/nvm), [FNM](https://github.com/Schniz/fnm), or [Volta](https://github.com/volta-cli/volta).
|
||||
3. **pnpm**, as specified in `package.json`. For automatic setup enable [Corepack](https://nodejs.org/docs/latest-v17.x/api/corepack.html) using `corepack enable`.
|
||||
|
||||
## 🔍 PHPStorm setup for XDebug
|
||||
|
||||
In `Languages & Preferences > PHP > Servers` set path mappings:
|
||||
|
@ -9,9 +9,10 @@ RUN apt-get update \
|
||||
&& docker-php-ext-install pdo_mysql \
|
||||
&& pecl install xdebug-2.9.8 && \
|
||||
\
|
||||
# Install NodeJS + NPM
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
# Install NodeJS, enable Corepack
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | bash - && \
|
||||
apt-get install -y nodejs build-essential && \
|
||||
corepack enable && \
|
||||
\
|
||||
# Install WP-CLI
|
||||
curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
@ -35,5 +36,6 @@ RUN cat /tmp/xdebug.ini >> $XDEBUGINI_PATH
|
||||
# allow .htaccess files (between <Directory /var/www/> and </Directory>, which is WordPress installation)
|
||||
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
|
||||
|
||||
# ensure existing content in /var/www/html respects UID and GID
|
||||
RUN chown -R ${UID}:${GID} /var/www/html
|
||||
# ensure existing content in /var/www/html respects UID and GID, give Node permissions for Corepack
|
||||
RUN chown -R ${UID}:${GID} /var/www/html && \
|
||||
mkdir -p /.node && chown -R ${UID}:${GID} /.node
|
||||
|
@ -7,9 +7,10 @@ ARG GID=1000
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git zlib1g-dev libzip-dev zip wget gnupg msmtp libpng-dev gettext subversion \
|
||||
&& \
|
||||
# Install NodeJS + NPM
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
# Install NodeJS, enable Corepack
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | bash - && \
|
||||
apt-get install -y nodejs build-essential && \
|
||||
corepack enable && \
|
||||
\
|
||||
# Install WP-CLI
|
||||
curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
@ -39,5 +40,6 @@ RUN docker-php-ext-install pdo_mysql
|
||||
# allow .htaccess files (between <Directory /var/www/> and </Directory>, which is WordPress installation)
|
||||
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
|
||||
|
||||
# ensure existing content in /var/www/html respects UID and GID
|
||||
RUN chown -R ${UID}:${GID} /var/www/html
|
||||
# ensure existing content in /var/www/html respects UID and GID, give Node permissions for Corepack
|
||||
RUN chown -R ${UID}:${GID} /var/www/html && \
|
||||
mkdir -p /.node && chown -R ${UID}:${GID} /.node
|
||||
|
@ -7,9 +7,10 @@ ARG GID=1000
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git zlib1g-dev libzip-dev zip wget gnupg msmtp libpng-dev gettext subversion \
|
||||
&& \
|
||||
# Install NodeJS + NPM
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
# Install NodeJS, enable Corepack
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | bash - && \
|
||||
apt-get install -y nodejs build-essential && \
|
||||
corepack enable && \
|
||||
\
|
||||
# Install WP-CLI
|
||||
curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
@ -39,5 +40,6 @@ RUN docker-php-ext-install pdo_mysql
|
||||
# allow .htaccess files (between <Directory /var/www/> and </Directory>, which is WordPress installation)
|
||||
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
|
||||
|
||||
# ensure existing content in /var/www/html respects UID and GID
|
||||
RUN chown -R ${UID}:${GID} /var/www/html
|
||||
# ensure existing content in /var/www/html respects UID and GID, give Node permissions for Corepack
|
||||
RUN chown -R ${UID}:${GID} /var/www/html && \
|
||||
mkdir -p /.node && chown -R ${UID}:${GID} /.node
|
||||
|
@ -49,10 +49,14 @@ services:
|
||||
MAILPOET_DEV_SITE: 1
|
||||
volumes:
|
||||
- './wordpress:/var/www/html'
|
||||
- './eslint-config:/var/www/html/wp-content/plugins/eslint-config'
|
||||
- './tsconfig.base.json:/var/www/html/wp-content/plugins/tsconfig.base.json:ro'
|
||||
- './package.json:/var/www/html/wp-content/plugins/package.json'
|
||||
- './pnpm-lock.yaml:/var/www/html/wp-content/plugins/pnpm-lock.yaml'
|
||||
- './pnpm-workspace.yaml:/var/www/html/wp-content/plugins/pnpm-workspace.yaml'
|
||||
- './patches:/var/www/html/wp-content/plugins/patches'
|
||||
- './mailpoet:/var/www/html/wp-content/plugins/mailpoet'
|
||||
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
||||
- './packages:/var/www/html/wp-content/plugins/packages'
|
||||
- './templates:/var/www/templates'
|
||||
|
||||
test_wordpress:
|
||||
@ -76,9 +80,9 @@ services:
|
||||
WORDPRESS_DB_PASSWORD: wordpress
|
||||
PHP_IDE_CONFIG: 'serverName=Mailpoet'
|
||||
volumes:
|
||||
- './eslint-config:/var/www/html/wp-content/plugins/eslint-config'
|
||||
- './mailpoet:/var/www/html/wp-content/plugins/mailpoet'
|
||||
- './mailpoet-premium:/var/www/html/wp-content/plugins/mailpoet-premium'
|
||||
- './packages:/var/www/html/wp-content/plugins/packages'
|
||||
|
||||
smtp:
|
||||
container_name: mp-mailhog
|
||||
|
7057
eslint-config/package-lock.json
generated
7057
eslint-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,9 +5,7 @@
|
||||
"@babel/preset-env"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"babel-plugin-typescript-to-proptypes",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
@ -15,18 +13,7 @@
|
||||
"corejs": 3
|
||||
}
|
||||
],
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-syntax-import-meta",
|
||||
"@babel/plugin-proposal-json-strings",
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
"@babel/plugin-proposal-function-sent",
|
||||
"@babel/plugin-proposal-export-namespace-from",
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-throw-expressions"
|
||||
]
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
v16.13.0
|
||||
v17.9.1
|
||||
|
@ -23,9 +23,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->taskExecStack()
|
||||
->stopOnFail()
|
||||
->exec('./tools/vendor/composer.phar install')
|
||||
->exec('npm ci --prefer-offline')
|
||||
->exec('cd .. && npm ci --prefer-offline')
|
||||
->exec('cd ../eslint-config && npm ci --prefer-offline')
|
||||
->exec('cd .. && pnpm install --frozen-lockfile --prefer-offline')
|
||||
->addCode([$this, 'cleanupCachedFiles'])
|
||||
->run();
|
||||
}
|
||||
@ -41,7 +39,7 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $this->taskExecStack()
|
||||
->stopOnFail()
|
||||
->exec('./tools/vendor/composer.phar update')
|
||||
->exec('npm update')
|
||||
->exec('pnpm update')
|
||||
->run();
|
||||
}
|
||||
|
||||
@ -52,8 +50,8 @@ class RoboFile extends \Robo\Tasks {
|
||||
$file = $changedFile->getResource()->getResource();
|
||||
$this->taskExecStack()
|
||||
->stopOnFail()
|
||||
->exec('npm run scss')
|
||||
->exec('npm run autoprefixer')
|
||||
->exec('pnpm run scss')
|
||||
->exec('pnpm run autoprefixer')
|
||||
->run();
|
||||
})
|
||||
->run();
|
||||
@ -92,9 +90,9 @@ class RoboFile extends \Robo\Tasks {
|
||||
// Clean up folder from previous files
|
||||
array_map('unlink', glob("assets/dist/css/*.*"));
|
||||
|
||||
$this->_exec('npm run stylelint -- "assets/css/src/**/*.scss"');
|
||||
$this->_exec('npm run scss');
|
||||
$compilationResult = $this->_exec('npm run autoprefixer');
|
||||
$this->_exec('pnpm run stylelint -- "assets/css/src/**/*.scss"');
|
||||
$this->_exec('pnpm run scss');
|
||||
$compilationResult = $this->_exec('pnpm run autoprefixer');
|
||||
|
||||
// Create manifest file
|
||||
$manifest = [];
|
||||
@ -415,11 +413,11 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function qaLintJavascript() {
|
||||
return $this->_exec('npm run check-types && npm run lint');
|
||||
return $this->_exec('pnpm run check-types && pnpm run lint');
|
||||
}
|
||||
|
||||
public function qaLintCss() {
|
||||
return $this->_exec('npm run stylelint-check -- "assets/css/src/**/*.scss"');
|
||||
return $this->_exec('pnpm run stylelint-check -- "assets/css/src/**/*.scss"');
|
||||
}
|
||||
|
||||
public function qaCodeSniffer(array $filesToCheck, $opts = ['severity' => 'all']) {
|
||||
@ -554,11 +552,11 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function storybookBuild() {
|
||||
return $this->_exec('npm run build-storybook');
|
||||
return $this->_exec('pnpm run build-storybook');
|
||||
}
|
||||
|
||||
public function storybookWatch() {
|
||||
return $this->_exec('npm run storybook');
|
||||
return $this->_exec('pnpm run storybook');
|
||||
}
|
||||
|
||||
public function svnCheckout() {
|
||||
|
@ -1,14 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'mailpoet-icon';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAYcAAsAAAAABdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIFHGNtYXAAAAFoAAAAVAAAAFQXVtKHZ2FzcAAAAbwAAAAIAAAACAAAABBnbHlmAAABxAAAAhQAAAIU239UpGhlYWQAAAPYAAAANgAAADYYSCB9aGhlYQAABBAAAAAkAAAAJAelA8ZobXR4AAAENAAAABQAAAAUCeMAAGxvY2EAAARIAAAADAAAAAwAKAEebWF4cAAABFQAAAAgAAAAIAAIAMJuYW1lAAAEdAAAAYYAAAGGmUoJ+3Bvc3QAAAX8AAAAIAAAACAAAwAAAAMC8gGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6QADwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADgAAAAKAAgAAgACAAEAIOkA//3//wAAAAAAIOkA//3//wAB/+MXBAADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAgAA/8AD4wPAAGQAvwAAEzI2Nz4BNTQwOQEREx4BFzEeATM4ATMyNjcjPgE3MRMRHAExFBYXMR4BMzI2Nz4BNTA0OQERPAE1NCYnMS4BIyoBIyIGBzMOAQcVCwEuASc1LgEjKgEjIgYHMw4BFREUFhceATMFJy4BIyIGBzEOASsBOAExIgYHMQcnLgErAS4BJxcuASMiBgcxBw4BFTEcATEUFhcxHgEXMTM4ATEyFhcdARQWMzI2PQE+ATM4ATEzPgE3Iz4BNTwBOQEuASc19xEYBgYHbwQNCAgXEAEKEwkBCg0DdggGBxcPEhcHBgcMCgodEQEDAg8dDQEMEQSBhgQPCgscEwEDAQ8bDAELDAcGBxcQAtkQBg8JCRAHECoYjDJZIQcHIVkyjRgpEQEHEAkJDwYQCAoICCBSL40sRAwUJCMUDEQsjS9TIAEICAEKCAF5BwgHEQoBASv+2QsRBwcGBAUFEQoBSP6+AQILEwcHCAgHBxIKAQHdAQEBDBYGBwcHBggWDgH+gwGADRQHAQcHBwcHGBD+LA4UBwYHtAwFBQYGDhAmIQgHISYBEA4BBgcGBQwGEwsBAQsSBh0hATUoAQEEGRkEASk1ASEdBhMKAQELEwYBAAEAAAAAAACzy1ndXw889QALBAAAAAAA2qNuAAAAAADao24AAAD/wAPjA8AAAAAIAAIAAAAAAAAAAQAAA8D/wAAABAAAAAAAA+MAAQAAAAAAAAAAAAAAAAAAAAUEAAAAAAAAAAAAAAACAAAAA+MAAAAAAAAACgAUAB4BCgABAAAABQDAAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEABwAAAAEAAAAAAAIABwBgAAEAAAAAAAMABwA2AAEAAAAAAAQABwB1AAEAAAAAAAUACwAVAAEAAAAAAAYABwBLAAEAAAAAAAoAGgCKAAMAAQQJAAEADgAHAAMAAQQJAAIADgBnAAMAAQQJAAMADgA9AAMAAQQJAAQADgB8AAMAAQQJAAUAFgAgAAMAAQQJAAYADgBSAAMAAQQJAAoANACkaWNvbW9vbgBpAGMAbwBtAG8AbwBuVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwaWNvbW9vbgBpAGMAbwBtAG8AbwBuaWNvbW9vbgBpAGMAbwBtAG8AbwBuUmVndWxhcgBSAGUAZwB1AGwAYQByaWNvbW9vbgBpAGMAbwBtAG8AbwBuRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==') format('woff');
|
||||
}
|
||||
|
||||
/* menu icon */
|
||||
#adminmenu #toplevel_page_mailpoet-newsletters .wp-menu-image:before {
|
||||
content: '\e900';
|
||||
font-family: 'mailpoet-icon';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// See: https://github.com/WordPress/gutenberg/blob/af7da80dd54d7fe52772890e2cc1b65073db9655/packages/block-editor/src/components/block-icon/style.scss
|
||||
|
||||
.block-editor-block-icon {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
|
||||
&.has-colors {
|
||||
svg {
|
||||
fill: currentColor;
|
||||
|
||||
// Optimize for high contrast modes.
|
||||
// See also https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/.
|
||||
|
||||
@media (forced-colors: active) {
|
||||
fill: CanvasText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icons with width/height attributes below 20px will be sized up to 20px,
|
||||
// and icons with width/height attributes above 24px will be sized down to
|
||||
// 24px. Icons with width/height >=20px and <=24px will display at the
|
||||
// indicated size.
|
||||
// See: https://github.com/WordPress/gutenberg/pull/9828
|
||||
svg {
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
min-height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#mailpoet_automation_editor {
|
||||
.components-notice {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
.components-panel__body-title.mailpoet-automation-panel-plain-body-title {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
padding: 16px 48px 16px 16px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-action {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 8px 16px;
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-step-name-dropdown {
|
||||
display: block;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-text {
|
||||
padding-left: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.mailpoet-automation-panel-plain-body-title-action {
|
||||
margin-top: -10px;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-step-name-popover {
|
||||
margin-top: -25px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mailpoet-step-name-input {
|
||||
min-width: 208px;
|
||||
}
|
||||
.mailpoet-deactive {
|
||||
color: #757575;
|
||||
font-style: italic;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// See: https://github.com/WordPress/gutenberg/blob/af7da80dd54d7fe52772890e2cc1b65073db9655/packages/block-editor/src/components/block-card/style.scss
|
||||
|
||||
.block-editor-block-card {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.block-editor-block-card__content {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.block-editor-block-card__title {
|
||||
font-weight: 500;
|
||||
|
||||
&.block-editor-block-card__title {
|
||||
line-height: 24px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-editor-block-card__description {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.block-editor-block-card .block-editor-block-icon {
|
||||
flex: 0 0 24px;
|
||||
height: 24px;
|
||||
margin-left: 0;
|
||||
margin-right: 12px;
|
||||
width: 24px;
|
||||
}
|
@ -13,7 +13,8 @@
|
||||
text-align: left;
|
||||
width: 280px;
|
||||
|
||||
&:focus {
|
||||
&:focus,
|
||||
&.selected-step {
|
||||
box-shadow:
|
||||
0 0 0 1px #fbfbfb, // space
|
||||
0 0 0 calc(var(--wp-admin-border-width-focus) + 1px) var(--wp-admin-theme-color), // focus ring
|
||||
@ -31,4 +32,14 @@
|
||||
|
||||
.mailpoet-automation-editor-step-subtitle {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mailpoet-automation-colored-icon {
|
||||
border-radius: 50%;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
@import './mailpoet/button';
|
||||
@import './mailpoet/edit';
|
||||
@import './mailpoet/thumbnail';
|
@ -0,0 +1,37 @@
|
||||
.components-button.mailpoet-automation-button-sidebar-primary,
|
||||
.components-button.mailpoet-automation-button-sidebar-primary.has-text,
|
||||
.components-button.mailpoet-automation-button-sidebar-primary.has-icon {
|
||||
background: #1d2327;
|
||||
width: 100%;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1d2327;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: rgba(255, 255, 255, .4);
|
||||
}
|
||||
|
||||
&.is-busy {
|
||||
--background-color-1: #2c3236;
|
||||
--background-color-2: #535659;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
-45deg,
|
||||
var(--background-color-1) 33%,
|
||||
var(--background-color-2) 33%,
|
||||
var(--background-color-2) 70%,
|
||||
var(--background-color-1) 70%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.components-button.mailpoet-automation-button-centered,
|
||||
.components-button.mailpoet-automation-button-centered.has-text,
|
||||
.components-button.mailpoet-automation-button-centered.has-icon {
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.mailpoet-automation-email-content-separator {
|
||||
height: 16px;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
.mailpoet-automation-thumbnail-box {
|
||||
align-items: center;
|
||||
background: #f6f7f7;
|
||||
border: 1px solid #2c3338;
|
||||
display: flex;
|
||||
height: 280px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mailpoet-automation-thumbnail-spinner {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-thumbnail-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mailpoet-automation-thumbnail-image {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 192px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-thumbnail-buttons {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
@ -83,13 +83,14 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
// Adjustments for family-font-select in popover
|
||||
.mailpoet_toolbar_item {
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
|
||||
.mailpoet-font-family-select {
|
||||
width: $grid-column-small;
|
||||
height: 48px;
|
||||
|
||||
.components-input-control__container .components-custom-select-control__button {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
// Force rendering of select arrow on the right
|
||||
|
@ -22,18 +22,31 @@
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
.block-editor-panel-color-gradient-settings {
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-font-family-select {
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
.mailpoet-font-family-select {
|
||||
.components-flex {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.components-custom-select-control__label {
|
||||
font-weight: bold;
|
||||
.components-input-control__container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.components-custom-select-control__label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,3 +88,33 @@ p.sender_email_address_warning:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_manage_sender_domain {
|
||||
.mailpoet_table_header {
|
||||
font-weight: 700 !important;
|
||||
text-align: center !important; // to prevent being overwritten by widefat table classes
|
||||
}
|
||||
|
||||
.dns_record_type_column {
|
||||
font-weight: 550 !important;
|
||||
text-align: center !important; // to prevent being overwritten by widefat table classes
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-premium-modal.components-modal__frame {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mailpoet-premium-modal-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.mailpoet-premium-modal-error {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $grid-gap-half;
|
||||
}
|
||||
|
@ -92,17 +92,16 @@ h1.title.mailpoet-newsletter-listing-heading {
|
||||
margin-bottom: $grid-gap;
|
||||
}
|
||||
|
||||
.mailpoet-newsletter-listing-heading-wrapper {
|
||||
.mailpoet-top-bar-logo {
|
||||
cursor: pointer;
|
||||
left: 17px;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
z-index: 1;
|
||||
#mailpoet_editor_steps_heading {
|
||||
.mailpoet-top-bar {
|
||||
left: 0;
|
||||
|
||||
svg {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
.mailpoet-top-bar-logo {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mailpoet-top-bar-beamer {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
#logger {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-top: 1px #aba9a9 solid;
|
||||
font-size: .85em;
|
||||
height: 300px;
|
||||
margin-top: 20px;
|
||||
overflow: scroll;
|
||||
padding: 2px;
|
||||
resize: both;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#progressbar {
|
||||
background-color: #d8d8d8;
|
||||
border-radius: 5px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
$progressbar_color: #fecf23;
|
||||
$progressbar_gradient_to_color: #fd9215;
|
||||
|
||||
.ui-progressbar .ui-progressbar-value {
|
||||
background-color: $progressbar_color;
|
||||
background-image: linear-gradient(to bottom, $progressbar_color, $progressbar_gradient_to_color);
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mailpoet_progress_label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.error_msg {
|
||||
color: #f00;
|
||||
}
|
@ -65,6 +65,18 @@ $form-line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset fieldset styles in form for backward compatibility. */
|
||||
.mailpoet_paragraph {
|
||||
fieldset,
|
||||
legend {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_textarea {
|
||||
height: auto;
|
||||
}
|
||||
|
@ -85,6 +85,8 @@ $beamer-dot-size: 8px;
|
||||
border: none;
|
||||
color: $color-wordpress-grey-dark;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
@ -61,7 +61,7 @@
|
||||
margin-top: $grid-gap;
|
||||
}
|
||||
|
||||
&:not(.mailpoet-full-width) + .mailpoet-form-input:not(.mailpoet-full-width),
|
||||
&:not(.mailpoet-full-width) + .mailpoet-form-input:not(.mailpoet-full-width):not(.mailpoet-form-select),
|
||||
&:not(.mailpoet-full-width) + .mailpoet-button:not(.mailpoet-full-width) {
|
||||
margin-left: $grid-gap;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
border: 1px solid $color-input-border !important;
|
||||
border-radius: $form-control-border-radius !important;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
min-height: $form-control-height;
|
||||
min-width: 0;
|
||||
@ -11,6 +10,7 @@
|
||||
// To align the left padding with the other inputs
|
||||
input[type=text].components-form-token-field__input {
|
||||
margin-left: 0;
|
||||
min-height: 30px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
// For better fit when the last item is active
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Utilities
|
||||
// Helpers and overrides.
|
||||
|
||||
@import 'components-admin/menu';
|
||||
@import 'components-admin/3rd-party-plugins/members';
|
||||
|
@ -4,9 +4,17 @@
|
||||
@import '../../../node_modules/@wordpress/block-editor/build-style/style'; // for inserter styles
|
||||
@import 'settings/colors';
|
||||
@import './components-automation-editor/add-step-button';
|
||||
@import './components-automation-editor/block-icon';
|
||||
@import './components-automation-editor/dropdown';
|
||||
@import './components-automation-editor/empty-workflow';
|
||||
@import './components-automation-editor/panel';
|
||||
@import './components-automation-editor/separator';
|
||||
@import './components-automation-editor/status';
|
||||
@import './components-automation-editor/step';
|
||||
@import './components-automation-editor/step-card';
|
||||
@import './components-automation-editor/workflow';
|
||||
@import './components-automation-editor/notices';
|
||||
|
||||
// integrations
|
||||
|
||||
@import './components-automation-integrations/mailpoet';
|
||||
|
@ -74,7 +74,6 @@
|
||||
@import 'components-plugin/pages-custom';
|
||||
@import 'components-plugin/premium-page';
|
||||
@import 'components-plugin/menu';
|
||||
@import 'components-plugin/mp2-migrator';
|
||||
@import 'components-plugin/newsletter';
|
||||
@import 'components-plugin/newsletter-templates';
|
||||
@import 'components-plugin/newsletter-types';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { api } from './config';
|
||||
import { api } from '../config';
|
||||
|
||||
const API_URL = `${api.root}/mailpoet/v1/automation`;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { api } from '../../config';
|
||||
import { api } from '../config';
|
||||
|
||||
export * from './hooks';
|
||||
|
||||
const apiUrl = `${api.root}/mailpoet/v1/automation/`;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
CreateWorkflowFromTemplateButton,
|
||||
} from './testing';
|
||||
import { useMutation, useQuery } from './api';
|
||||
import { WorkflowListingNotices } from './listing/workflow-listing-notices';
|
||||
|
||||
function Workflows(): JSX.Element {
|
||||
const { data, loading, error } = useQuery<{ data: Workflow[] }>('workflows');
|
||||
@ -34,6 +35,7 @@ function RecreateSchemaButton(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<WorkflowListingNotices />
|
||||
<button
|
||||
className="button button-link-delete"
|
||||
type="button"
|
||||
@ -81,11 +83,15 @@ function App(): JSX.Element {
|
||||
<Workflows />
|
||||
<div style={{ marginTop: 30, display: 'grid', gridGap: 8 }}>
|
||||
<CreateTestingWorkflowButton />
|
||||
<CreateWorkflowFromTemplateButton template="delayed-email-after-signup">
|
||||
<CreateWorkflowFromTemplateButton slug="simple-welcome-email">
|
||||
Create testing workflow from template (welcome email)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton template="welcome-email-sequence">
|
||||
Create testing workflow from template (welcome sequence)
|
||||
<CreateWorkflowFromTemplateButton slug="welcome-email-sequence">
|
||||
Create testing workflow from template (welcome sequence, only premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<CreateWorkflowFromTemplateButton slug="advanced-welcome-email-sequence">
|
||||
Create testing workflow from template (advanced welcome sequence, only
|
||||
premium)
|
||||
</CreateWorkflowFromTemplateButton>
|
||||
<RecreateSchemaButton />
|
||||
<DeleteSchemaButton />
|
||||
|
@ -0,0 +1,70 @@
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { StoreDescriptor, useDispatch, useSelect } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { confirmAlert } from '../../../../common/confirm_alert';
|
||||
import { store } from '../../store';
|
||||
import { Workflow } from '../workflow/types';
|
||||
import { MailPoet } from '../../../../mailpoet';
|
||||
import { LISTING_NOTICE_PARAMETERS } from '../../../listing/workflow-listing-notices';
|
||||
|
||||
export function TrashButton(): JSX.Element {
|
||||
const { createErrorNotice } = useDispatch(noticesStore as StoreDescriptor);
|
||||
const { workflow } = useSelect(
|
||||
(select) => ({
|
||||
workflow: select(store).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const trash = () => {
|
||||
apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
...workflow,
|
||||
status: 'trash',
|
||||
},
|
||||
})
|
||||
.then(({ data }: { data: Workflow }) => {
|
||||
if (data.status !== 'trash') {
|
||||
void createErrorNotice('An error occurred!', {
|
||||
explicitDismiss: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
|
||||
[LISTING_NOTICE_PARAMETERS.workflowDeleted]: workflow.id,
|
||||
});
|
||||
})
|
||||
.catch((): void => {
|
||||
void createErrorNotice('An error occurred!', {
|
||||
explicitDismiss: true,
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
isSecondary
|
||||
isDestructive
|
||||
onClick={() => {
|
||||
confirmAlert({
|
||||
title: 'Delete workflow',
|
||||
message: `You are about to delete the “${workflow.name}” workflow`,
|
||||
cancelLabel: 'Cancel',
|
||||
confirmLabel: 'Yes, delete',
|
||||
onConfirm: () => {
|
||||
trash();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Move to Trash
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -32,6 +32,16 @@ function UpdateButton(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function SaveDraftButton(): JSX.Element {
|
||||
const { save } = useDispatch(store);
|
||||
|
||||
return (
|
||||
<Button isTertiary onClick={save}>
|
||||
{__('Save Draft')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header(): JSX.Element {
|
||||
const { setWorkflowName } = useDispatch(store);
|
||||
const { workflowName, workflowStatus } = useSelect(
|
||||
@ -75,7 +85,7 @@ export function Header(): JSX.Element {
|
||||
|
||||
<div className="edit-site-header_end">
|
||||
<div className="edit-site-header__actions">
|
||||
<Button isTertiary>{__('Save Draft')}</Button>
|
||||
<SaveDraftButton />
|
||||
{workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />}
|
||||
{workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />}
|
||||
<PinnedItems.Slot scope={storeName} />
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type ColoredIconProps = {
|
||||
width: string;
|
||||
height: string;
|
||||
background: string;
|
||||
foreground: string;
|
||||
icon: ComponentType;
|
||||
};
|
||||
|
||||
export function ColoredIcon({
|
||||
foreground,
|
||||
background,
|
||||
...iconProps
|
||||
}: ColoredIconProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className="mailpoet-automation-colored-icon"
|
||||
style={{
|
||||
width: iconProps.width,
|
||||
height: iconProps.height,
|
||||
backgroundColor: background,
|
||||
fill: foreground,
|
||||
}}
|
||||
>
|
||||
<Icon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
export function DelayIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="41"
|
||||
height="41"
|
||||
viewBox="0 0 41 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.796875 20.5C0.796875 9.45431 9.75118 0.5 20.7969 0.5C31.8426 0.5 40.7969 9.45431 40.7969 20.5C40.7969 31.5457 31.8426 40.5 20.7969 40.5C9.75118 40.5 0.796875 31.5457 0.796875 20.5Z"
|
||||
fill="#F7EDF7"
|
||||
/>
|
||||
<path
|
||||
d="M20.7972 11.1665C15.6639 11.1665 11.4639 15.3665 11.4639 20.4998C11.4639 25.6332 15.6639 29.8332 20.7972 29.8332C25.9305 29.8332 30.1305 25.6332 30.1305 20.4998C30.1305 15.3665 25.9305 11.1665 20.7972 11.1665ZM20.7972 27.9665C16.6812 27.9665 13.3305 24.6158 13.3305 20.4998C13.3305 16.3838 16.6812 13.0332 20.7972 13.0332C24.9132 13.0332 28.2639 16.3838 28.2639 20.4998C28.2639 24.6158 24.9132 27.9665 20.7972 27.9665ZM21.2639 15.8332H19.8639V21.4332L24.7172 24.4198L25.4639 23.2065L21.2639 20.6865V15.8332Z"
|
||||
fill="#7F54B3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
export function EmailIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="41"
|
||||
height="41"
|
||||
viewBox="0 0 41 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.5 20.5C0.5 9.45431 9.45431 0.5 20.5 0.5C31.5457 0.5 40.5 9.45431 40.5 20.5C40.5 31.5457 31.5457 40.5 20.5 40.5C9.45431 40.5 0.5 31.5457 0.5 20.5Z"
|
||||
fill="#FCF9E8"
|
||||
/>
|
||||
<path
|
||||
d="M27.4998 14.375H13.4998C12.6944 14.375 12.0415 15.0279 12.0415 15.8333V25.1667C12.0415 25.9721 12.6944 26.625 13.4998 26.625H27.4998C28.3053 26.625 28.9582 25.9721 28.9582 25.1667V15.8333C28.9582 15.0279 28.3053 14.375 27.4998 14.375Z"
|
||||
stroke="#996800"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M12.3335 14.6665L20.5002 21.6665L28.6668 14.6665"
|
||||
stroke="#996800"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,3 +1,2 @@
|
||||
export { DelayIcon } from './delay';
|
||||
export { EmailIcon } from './email';
|
||||
export { TriggerIcon } from './trigger';
|
||||
export { ColoredIcon } from './colored-icon';
|
||||
|
@ -1,24 +1,8 @@
|
||||
export function TriggerIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="41"
|
||||
viewBox="0 0 40 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 20.5C0 9.45431 8.95431 0.5 20 0.5C31.0457 0.5 40 9.45431 40 20.5C40 31.5457 31.0457 40.5 20 40.5C8.95431 40.5 0 31.5457 0 20.5Z"
|
||||
fill="#F0F6FC"
|
||||
/>
|
||||
<path
|
||||
d="M24.3163 12.5667L30.8496 19.1C31.6663 19.8 31.6663 21.2 30.7329 22.0167L24.1996 28.55C23.8496 28.9 23.2663 29.1333 22.7996 29.1333C22.3329 29.1333 21.7496 28.9 21.3996 28.55L14.8663 22.0167C14.0496 21.2 14.0496 19.9167 14.8663 19.1L21.3996 12.5667C22.2163 11.75 23.4996 11.75 24.3163 12.5667Z"
|
||||
fill="#2271B1"
|
||||
/>
|
||||
<path
|
||||
d="M17.6663 12.9167L10.3163 20.2667C10.1996 20.3833 10.1996 20.6167 10.4329 20.6167L17.7829 27.9667L16.4996 29.25L9.14961 22.0167C8.33294 21.2 8.33294 19.9167 9.14961 19.1L16.4996 11.75L17.6663 12.9167Z"
|
||||
fill="#2271B1"
|
||||
/>
|
||||
<svg viewBox="0 0 24 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.1135 1.06667L22.6469 7.6C23.4635 8.3 23.4635 9.7 22.5302 10.5167L15.9969 17.05C15.6469 17.4 15.0635 17.6333 14.5968 17.6333C14.1302 17.6333 13.5468 17.4 13.1968 17.05L6.66352 10.5167C5.84685 9.7 5.84685 8.41667 6.66352 7.6L13.1968 1.06667C14.0135 0.25 15.2968 0.25 16.1135 1.06667Z" />
|
||||
<path d="M9.46352 1.41667L2.11352 8.76667C1.99685 8.88333 1.99685 9.11667 2.23018 9.11667L9.58018 16.4667L8.29685 17.75L0.946851 10.5167C0.130184 9.7 0.130184 8.41667 0.946851 7.6L8.29685 0.25L9.46352 1.41667Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type Item = {
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
icon: JSX.Element;
|
||||
description: string;
|
||||
isDisabled: boolean;
|
||||
icon: ComponentType | JSX.Element;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/block-editor/src/components/block-icon/index.js
|
||||
|
||||
type Props = {
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
export function StepIcon({ icon }: Props): JSX.Element {
|
||||
return <span className="block-editor-block-icon">{icon}</span>;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { StepIcon } from './step_icon';
|
||||
import { Item } from './item';
|
||||
import { StepIcon } from '../step-icon';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/block-editor/src/components/inserter/preview-panel.js
|
||||
|
||||
|
@ -46,9 +46,9 @@ export function StepList({
|
||||
<InserterListboxRow key={i}>
|
||||
{row.map((item, j) => (
|
||||
<InserterListItem
|
||||
key={item.id}
|
||||
key={item.key}
|
||||
item={item}
|
||||
className={getBlockMenuDefaultClassName(item.id)}
|
||||
className={getBlockMenuDefaultClassName(item.key)}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
isDraggable={isDraggable}
|
||||
|
@ -3,8 +3,8 @@ import { ComponentProps } from 'react';
|
||||
import { useRef, memo } from '@wordpress/element';
|
||||
import { ENTER } from '@wordpress/keycodes';
|
||||
import { Item } from './item';
|
||||
import { StepIcon } from './step_icon';
|
||||
import { InserterListboxItem } from '../inserter-listbox/listbox_item';
|
||||
import { StepIcon } from '../step-icon';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/628ae68152f572d0b395bb15c0f71b8821e7f130/packages/block-editor/src/components/inserter-list-item/index.js
|
||||
|
||||
|
@ -22,7 +22,7 @@ export function KeyboardShortcuts(): null {
|
||||
const { registerShortcut } = useDispatch(keyboardShortcutsStore);
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut({
|
||||
void registerShortcut({
|
||||
name: 'mailpoet/automation-editor/toggle-fullscreen',
|
||||
category: 'global',
|
||||
description: __('Toggle fullscreen mode.'),
|
||||
@ -32,7 +32,7 @@ export function KeyboardShortcuts(): null {
|
||||
},
|
||||
});
|
||||
|
||||
registerShortcut({
|
||||
void registerShortcut({
|
||||
name: 'mailpoet/automation-editor/toggle-sidebar',
|
||||
category: 'global',
|
||||
description: __('Show or hide the settings sidebar.'),
|
||||
|
@ -0,0 +1,2 @@
|
||||
export * from './plain-body-title';
|
||||
export * from './title-action-button';
|
@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function PlainBodyTitle({ title, children }: Props): JSX.Element {
|
||||
return (
|
||||
<h2 className="components-panel__body-title mailpoet-automation-panel-plain-body-title">
|
||||
<div className="mailpoet-automation-panel-plain-body-title-text">
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { Dropdown, TextControl } from '@wordpress/components';
|
||||
import { edit, Icon } from '@wordpress/icons';
|
||||
import { PlainBodyTitle } from './plain-body-title';
|
||||
import { TitleActionButton } from './title-action-button';
|
||||
|
||||
type Props = {
|
||||
currentName: string;
|
||||
defaultName: string;
|
||||
update: (value: string) => void;
|
||||
};
|
||||
export function StepName({
|
||||
currentName,
|
||||
defaultName,
|
||||
update,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<Dropdown
|
||||
className="mailpoet-step-name-dropdown"
|
||||
contentClassName="mailpoet-step-name-popover"
|
||||
position="bottom left"
|
||||
renderToggle={({ isOpen, onToggle }) => (
|
||||
<PlainBodyTitle
|
||||
title={currentName.length > 0 ? currentName : defaultName}
|
||||
>
|
||||
<TitleActionButton
|
||||
onClick={onToggle}
|
||||
aria-expanded={isOpen}
|
||||
aria-label="Edit step name"
|
||||
>
|
||||
<Icon icon={edit} size={16} />
|
||||
</TitleActionButton>
|
||||
</PlainBodyTitle>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<TextControl
|
||||
label="Step name"
|
||||
className="mailpoet-step-name-input"
|
||||
placeholder={defaultName}
|
||||
value={currentName}
|
||||
onChange={update}
|
||||
help="Give the automation step a name that indicates its purpose. E.g
|
||||
“Abandoned cart recovery”. This name will be displayed only to you and not to the clients."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Button } from '@wordpress/components';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
type Props = ComponentProps<typeof Button>;
|
||||
|
||||
export function TitleActionButton(props: Props): JSX.Element {
|
||||
return (
|
||||
<div className="mailpoet-automation-panel-plain-body-title-action">
|
||||
<Button {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { PanelBody } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { store } from '../../../store';
|
||||
import { StepCard } from '../../step-card';
|
||||
|
||||
export function StepSidebar(): JSX.Element {
|
||||
const { selectedStep } = useSelect(
|
||||
const { selectedStep, selectedStepType } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
selectedStepType: select(store).getSelectedStepType(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@ -14,20 +16,36 @@ export function StepSidebar(): JSX.Element {
|
||||
return <PanelBody>No step selected.</PanelBody>;
|
||||
}
|
||||
|
||||
if (!selectedStepType) {
|
||||
return <PanelBody>Unknown step type.</PanelBody>;
|
||||
}
|
||||
|
||||
const Edit = selectedStepType.edit;
|
||||
|
||||
return (
|
||||
<PanelBody>
|
||||
<div>
|
||||
<strong>ID:</strong> {selectedStep.id}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Type:</strong> {selectedStep.type}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Key:</strong> {selectedStep.key}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Args:</strong> {JSON.stringify(selectedStep.args)}
|
||||
</div>
|
||||
</PanelBody>
|
||||
<div className="block-editor-block-inspector">
|
||||
<StepCard
|
||||
title={selectedStepType.title}
|
||||
description={selectedStepType.description}
|
||||
icon={selectedStepType.icon}
|
||||
/>
|
||||
|
||||
<Edit />
|
||||
|
||||
<PanelBody title="Debug info" initialOpen={false}>
|
||||
<div>
|
||||
<strong>ID:</strong> {selectedStep.id}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Type:</strong> {selectedStep.type}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Key:</strong> {selectedStep.key}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Args:</strong> {JSON.stringify(selectedStep.args)}
|
||||
</div>
|
||||
</PanelBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PanelBody } from '@wordpress/components';
|
||||
import { PanelBody, PanelRow } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { store } from '../../../store';
|
||||
import { TrashButton } from '../../actions/trash-button';
|
||||
|
||||
export function WorkflowSidebar(): JSX.Element {
|
||||
const { workflowData } = useSelect(
|
||||
@ -10,26 +11,44 @@ export function WorkflowSidebar(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelBody>
|
||||
<div>
|
||||
<strong>{workflowData.name}</strong>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<strong>ID:</strong> {workflowData.id}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Status:</strong> {workflowData.status}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Created:</strong>{' '}
|
||||
{new Date(Date.parse(workflowData.created_at)).toLocaleString()}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Updated:</strong>{' '}
|
||||
{new Date(Date.parse(workflowData.updated_at)).toLocaleString()}
|
||||
</div>
|
||||
<PanelBody title="Automation details" initialOpen>
|
||||
<PanelRow>
|
||||
<strong>Date added</strong>{' '}
|
||||
{new Date(Date.parse(workflowData.created_at)).toLocaleDateString(
|
||||
undefined,
|
||||
dateOptions,
|
||||
)}
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<strong>Activated</strong>{' '}
|
||||
{workflowData.status === 'active' &&
|
||||
new Date(Date.parse(workflowData.updated_at)).toLocaleDateString(
|
||||
undefined,
|
||||
dateOptions,
|
||||
)}
|
||||
{workflowData.status !== 'active' &&
|
||||
workflowData.activated_at &&
|
||||
new Date(Date.parse(workflowData.activated_at)).toLocaleDateString(
|
||||
undefined,
|
||||
dateOptions,
|
||||
)}
|
||||
{workflowData.status !== 'active' && !workflowData.activated_at && (
|
||||
<span className="mailpoet-deactive">Not activated yet.</span>
|
||||
)}
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<strong>Author</strong> {workflowData.author.name}
|
||||
</PanelRow>
|
||||
<PanelRow>
|
||||
<TrashButton />
|
||||
</PanelRow>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { StepIcon } from '../step-icon';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/af7da80dd54d7fe52772890e2cc1b65073db9655/packages/block-editor/src/components/block-card/index.js
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: JSX.Element | ComponentType;
|
||||
};
|
||||
|
||||
export function StepCard({ title, description, icon }: Props): JSX.Element {
|
||||
return (
|
||||
<div className="block-editor-block-card">
|
||||
<StepIcon icon={icon} />
|
||||
<div className="block-editor-block-card__content">
|
||||
<h2 className="block-editor-block-card__title">{title}</h2>
|
||||
<span className="block-editor-block-card__description">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
// See: https://github.com/WordPress/gutenberg/blob/af7da80dd54d7fe52772890e2cc1b65073db9655/packages/block-editor/src/components/block-icon/index.js
|
||||
|
||||
type Props = {
|
||||
icon: ComponentType | JSX.Element;
|
||||
};
|
||||
|
||||
export function StepIcon({ icon }: Props): JSX.Element {
|
||||
return (
|
||||
<span className="block-editor-block-icon">
|
||||
<Icon icon={icon} />
|
||||
</span>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import {
|
||||
__unstableUseCompositeState as useCompositeState,
|
||||
} from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { EditorNotices } from '@wordpress/editor';
|
||||
import { WorkflowCompositeContext } from './context';
|
||||
import { EmptyWorkflow } from './empty-workflow';
|
||||
import { Separator } from './separator';
|
||||
@ -12,9 +13,10 @@ import { InserterPopover } from '../inserter-popover';
|
||||
import { store } from '../../store';
|
||||
|
||||
export function Workflow(): JSX.Element {
|
||||
const { workflowData } = useSelect(
|
||||
const { workflowData, selectedStep } = useSelect(
|
||||
(select) => ({
|
||||
workflowData: select(store).getWorkflowData(),
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@ -61,6 +63,7 @@ export function Workflow(): JSX.Element {
|
||||
|
||||
return (
|
||||
<WorkflowCompositeContext.Provider value={compositeState}>
|
||||
<EditorNotices />
|
||||
<Composite
|
||||
state={compositeState}
|
||||
role="tree"
|
||||
@ -72,7 +75,10 @@ export function Workflow(): JSX.Element {
|
||||
{steps.map((step, i) => (
|
||||
<Fragment key={step.id}>
|
||||
{i > 0 && <Separator />}
|
||||
<Step step={step} key={step.id} />
|
||||
<Step
|
||||
step={step}
|
||||
isSelected={selectedStep && step.id === selectedStep.id}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
<div />
|
||||
|
@ -1,64 +1,60 @@
|
||||
import { useContext } from 'react';
|
||||
import { __unstableCompositeItem as CompositeItem } from '@wordpress/components';
|
||||
import { useDispatch, useRegistry } from '@wordpress/data';
|
||||
import { useDispatch, useRegistry, select } from '@wordpress/data';
|
||||
import { WorkflowCompositeContext } from './context';
|
||||
import { Step as StepType } from './types';
|
||||
import { DelayIcon, EmailIcon, TriggerIcon } from '../icons';
|
||||
import { Step as StepData } from './types';
|
||||
import { stepSidebarKey, store } from '../../store';
|
||||
import { TriggerIcon, ColoredIcon } from '../icons';
|
||||
|
||||
// mocked data
|
||||
function getIcon(step: StepType): JSX.Element | null {
|
||||
function getIcon(step: StepData): JSX.Element | null {
|
||||
// mocked data
|
||||
if (step.type === 'trigger') {
|
||||
return <TriggerIcon />;
|
||||
return (
|
||||
<ColoredIcon
|
||||
foreground="#2271b1"
|
||||
background="#f0f6fc"
|
||||
width="23px"
|
||||
height="23px"
|
||||
icon={TriggerIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step.key === 'core:delay') {
|
||||
return <DelayIcon />;
|
||||
}
|
||||
|
||||
if (step.key === 'mailpoet:send-email') {
|
||||
return <EmailIcon />;
|
||||
}
|
||||
|
||||
return null;
|
||||
const selectedStepType = select(store).getStepType(step.key);
|
||||
return selectedStepType ? (
|
||||
<ColoredIcon
|
||||
width="23px"
|
||||
height="23px"
|
||||
foreground={selectedStepType.foreground}
|
||||
background={selectedStepType.background}
|
||||
icon={selectedStepType.icon}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// mocked data
|
||||
function getTitle(step: StepType): string {
|
||||
function getTitle(step: StepData): string {
|
||||
// mocked data
|
||||
if (step.type === 'trigger') {
|
||||
return 'Trigger';
|
||||
}
|
||||
|
||||
if (step.key === 'core:delay') {
|
||||
return 'Delay';
|
||||
}
|
||||
|
||||
if (step.key === 'mailpoet:send-email') {
|
||||
return 'Send email';
|
||||
}
|
||||
|
||||
return '';
|
||||
const selectedStepType = select(store).getStepType(step.key);
|
||||
return selectedStepType ? selectedStepType.title : '';
|
||||
}
|
||||
|
||||
// mocked data
|
||||
function getSubtitle(step: StepType): string {
|
||||
function getSubtitle(step: StepData): JSX.Element | string {
|
||||
// mocked data
|
||||
if (step.key === 'mailpoet:segment:subscribed') {
|
||||
return 'Subscribed to segment';
|
||||
}
|
||||
if (step.key === 'core:delay') {
|
||||
return `${step.args.seconds as number} seconds`;
|
||||
}
|
||||
if (step.key === 'mailpoet:send-email') {
|
||||
return `Email ID: ${step.args.email_id as number}`;
|
||||
}
|
||||
return step.key;
|
||||
const selectedStepType = select(store).getStepType(step.key);
|
||||
return selectedStepType ? selectedStepType.subtitle(step) : null;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
step: StepType;
|
||||
step: StepData;
|
||||
isSelected: boolean;
|
||||
};
|
||||
|
||||
export function Step({ step }: Props): JSX.Element {
|
||||
export function Step({ step, isSelected }: Props): JSX.Element {
|
||||
const { openSidebar, selectStep } = useDispatch(store);
|
||||
const compositeState = useContext(WorkflowCompositeContext);
|
||||
const { batch } = useRegistry();
|
||||
@ -67,7 +63,9 @@ export function Step({ step }: Props): JSX.Element {
|
||||
<CompositeItem
|
||||
state={compositeState}
|
||||
role="treeitem"
|
||||
className="mailpoet-automation-editor-step"
|
||||
className={`mailpoet-automation-editor-step ${
|
||||
isSelected ? 'selected-step' : ''
|
||||
}`}
|
||||
key={step.id}
|
||||
focusable
|
||||
onClick={() =>
|
||||
|
@ -9,8 +9,13 @@ export type Step = {
|
||||
export type Workflow = {
|
||||
id?: number;
|
||||
name: string;
|
||||
status: 'active' | 'inactive' | 'draft';
|
||||
status: 'active' | 'inactive' | 'draft' | 'trash';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
activated_at: string;
|
||||
author: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
steps: Record<string, Step>;
|
||||
};
|
||||
|
@ -9,12 +9,18 @@ import {
|
||||
FullscreenMode,
|
||||
} from '@wordpress/interface';
|
||||
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import { Header } from './components/header';
|
||||
import { InserterSidebar } from './components/inserter-sidebar';
|
||||
import { KeyboardShortcuts } from './components/keyboard-shortcuts';
|
||||
import { Sidebar } from './components/sidebar';
|
||||
import { Workflow } from './components/workflow';
|
||||
import { store, storeName } from './store';
|
||||
import { initializeApi } from '../api';
|
||||
import { initialize as initializeCoreIntegration } from '../integrations/core';
|
||||
import { initialize as initializeMailPoetIntegration } from '../integrations/mailpoet';
|
||||
import { MailPoet } from '../../mailpoet';
|
||||
import { LISTING_NOTICE_PARAMETERS } from '../listing/workflow-listing-notices';
|
||||
|
||||
// See:
|
||||
// https://github.com/WordPress/gutenberg/blob/9601a33e30ba41bac98579c8d822af63dd961488/packages/edit-post/src/components/layout/index.js
|
||||
@ -26,12 +32,14 @@ function Editor(): JSX.Element {
|
||||
isInserterOpened,
|
||||
isSidebarOpened,
|
||||
showIconLabels,
|
||||
workflow,
|
||||
} = useSelect(
|
||||
(select) => ({
|
||||
isFullscreenActive: select(store).isFeatureActive('fullscreenMode'),
|
||||
isInserterOpened: select(store).isInserterSidebarOpened(),
|
||||
isSidebarOpened: select(store).isSidebarOpened(),
|
||||
showIconLabels: select(store).isFeatureActive('showIconLabels'),
|
||||
workflow: select(store).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@ -41,6 +49,12 @@ function Editor(): JSX.Element {
|
||||
'show-icon-labels': showIconLabels,
|
||||
});
|
||||
|
||||
if (workflow.status === 'trash') {
|
||||
window.location.href = addQueryArgs(MailPoet.urls.automationListing, {
|
||||
[LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted]: workflow.id,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ShortcutProvider>
|
||||
<SlotFillProvider>
|
||||
@ -75,6 +89,9 @@ function Editor(): JSX.Element {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const root = document.getElementById('mailpoet_automation_editor');
|
||||
if (root) {
|
||||
initializeApi();
|
||||
initializeCoreIntegration();
|
||||
initializeMailPoetIntegration();
|
||||
ReactDOM.render(<Editor />, root);
|
||||
}
|
||||
});
|
||||
|
@ -49,13 +49,27 @@ export function setWorkflowName(name) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* save() {
|
||||
const workflow = select(storeName).getWorkflowData();
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: workflow,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'SAVE',
|
||||
workflow: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function* activate() {
|
||||
const workflow = select(storeName).getWorkflowData();
|
||||
const data = yield apiFetch({
|
||||
path: `/workflows/${workflow.id}`,
|
||||
method: 'PUT',
|
||||
data: {
|
||||
name: workflow.name,
|
||||
...workflow,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
@ -65,3 +79,19 @@ export function* activate() {
|
||||
workflow: data.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function registerStepType(stepType) {
|
||||
return {
|
||||
type: 'REGISTER_STEP_TYPE',
|
||||
stepType,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateStepArgs(stepId, name, value) {
|
||||
return {
|
||||
type: 'UPDATE_STEP_ARGS',
|
||||
stepId,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
@ -1,36 +1,3 @@
|
||||
import { createReduxStore, register, StoreDescriptor } from '@wordpress/data';
|
||||
import { controls } from '@wordpress/data-controls';
|
||||
import * as actions from './actions';
|
||||
import { initializeApi } from './api';
|
||||
import { storeName } from './constants';
|
||||
import { initialState } from './initial_state';
|
||||
import { reducer } from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
import { State } from './types';
|
||||
import { OmitFirstArgs } from '../../../types';
|
||||
|
||||
export * from './constants';
|
||||
|
||||
type StoreType = Omit<StoreDescriptor, 'name'> & {
|
||||
name: typeof storeName;
|
||||
};
|
||||
|
||||
initializeApi();
|
||||
|
||||
export const store = createReduxStore<State>(storeName, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the "Action" type is missing thunks with "dispatch"
|
||||
actions: actions as any,
|
||||
controls,
|
||||
selectors,
|
||||
reducer,
|
||||
initialState,
|
||||
}) as StoreType;
|
||||
|
||||
type StoreKey = typeof storeName | StoreType;
|
||||
|
||||
declare module '@wordpress/data' {
|
||||
function select(key: StoreKey): OmitFirstArgs<typeof selectors>;
|
||||
function dispatch(key: StoreKey): typeof actions;
|
||||
}
|
||||
|
||||
register(store);
|
||||
export * from './register_step_type';
|
||||
export * from './store';
|
||||
|
@ -1,89 +1,12 @@
|
||||
import {
|
||||
atSymbol,
|
||||
backup,
|
||||
code,
|
||||
commentAuthorAvatar,
|
||||
commentEditLink,
|
||||
flipHorizontal,
|
||||
share,
|
||||
tag,
|
||||
} from '@wordpress/icons';
|
||||
import { AutomationEditorWindow, State } from './types';
|
||||
import { Item } from '../components/inserter/item';
|
||||
|
||||
declare let window: AutomationEditorWindow;
|
||||
|
||||
// mocked data
|
||||
const actionSteps: Item[] = [
|
||||
{
|
||||
id: 'mailpoet/automation-send-email',
|
||||
title: 'Send email',
|
||||
icon: atSymbol,
|
||||
description: 'Send an email.',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-update-contact',
|
||||
title: 'Update contact',
|
||||
icon: commentEditLink,
|
||||
description: 'Update contact information.',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-webhook',
|
||||
title: 'Webhook',
|
||||
icon: code,
|
||||
description: 'Trigger a webhook.',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-tag-untag',
|
||||
title: 'Tag/Untag',
|
||||
icon: tag,
|
||||
description: 'Add or remove tag',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-unsubscribe',
|
||||
title: 'Unsubscribe',
|
||||
icon: commentAuthorAvatar,
|
||||
description: 'Unsubscribe MailPoet subscriber.',
|
||||
isDisabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
// mocked data
|
||||
const logicalSteps: Item[] = [
|
||||
{
|
||||
id: 'mailpoet/automation-delay',
|
||||
title: 'Delay',
|
||||
icon: backup,
|
||||
description: 'Add a delay.',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-if-else',
|
||||
title: 'If/Else',
|
||||
icon: share,
|
||||
description: 'Execute a conditional statement.',
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
id: 'mailpoet/automation-a-b-test',
|
||||
title: 'A/B split test',
|
||||
icon: flipHorizontal,
|
||||
description: 'Run an A/B test.',
|
||||
isDisabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const initialState: State = {
|
||||
stepTypes: {},
|
||||
workflowData: { ...window.mailpoet_automation_workflow },
|
||||
workflowSaved: true,
|
||||
selectedStep: undefined,
|
||||
inserter: {
|
||||
actionSteps,
|
||||
logicalSteps,
|
||||
},
|
||||
inserterSidebar: {
|
||||
isOpened: false,
|
||||
},
|
||||
|
@ -28,12 +28,56 @@ export function reducer(state: State, action: Action): State {
|
||||
return {
|
||||
...state,
|
||||
workflowData: action.workflow,
|
||||
workflowSaved: false,
|
||||
};
|
||||
case 'SAVE':
|
||||
return {
|
||||
...state,
|
||||
workflowData: action.workflow,
|
||||
workflowSaved: true,
|
||||
};
|
||||
case 'ACTIVATE':
|
||||
return {
|
||||
...state,
|
||||
workflowData: action.workflow,
|
||||
workflowSaved: true,
|
||||
};
|
||||
case 'REGISTER_STEP_TYPE':
|
||||
return {
|
||||
...state,
|
||||
stepTypes: {
|
||||
...state.stepTypes,
|
||||
[action.stepType.key]: action.stepType,
|
||||
},
|
||||
};
|
||||
case 'UPDATE_STEP_ARGS': {
|
||||
const prevArgs = state.workflowData.steps[action.stepId].args ?? {};
|
||||
|
||||
const value =
|
||||
typeof action.value === 'function'
|
||||
? action.value(prevArgs[action.name] ?? undefined)
|
||||
: action.value;
|
||||
|
||||
const args = {
|
||||
...prevArgs,
|
||||
[action.name]: value,
|
||||
};
|
||||
|
||||
const step = { ...state.workflowData.steps[action.stepId], args };
|
||||
|
||||
return {
|
||||
...state,
|
||||
workflowData: {
|
||||
...state.workflowData,
|
||||
steps: {
|
||||
...state.workflowData.steps,
|
||||
[action.stepId]: step,
|
||||
},
|
||||
},
|
||||
workflowSaved: false,
|
||||
selectedStep: step,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { store } from './store';
|
||||
import { StepType } from './types';
|
||||
|
||||
export const registerStepType = (stepType: StepType): void => {
|
||||
dispatch(store).registerStepType(stepType);
|
||||
};
|
@ -2,7 +2,7 @@ import { createRegistrySelector } from '@wordpress/data';
|
||||
import { store as interfaceStore } from '@wordpress/interface';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
import { storeName } from './constants';
|
||||
import { Feature, State } from './types';
|
||||
import { Feature, State, StepType } from './types';
|
||||
import { Item } from '../components/inserter/item';
|
||||
import { Step, Workflow } from '../components/workflow/types';
|
||||
|
||||
@ -22,11 +22,15 @@ export function isInserterSidebarOpened(state: State): boolean {
|
||||
}
|
||||
|
||||
export function getInserterActionSteps(state: State): Item[] {
|
||||
return state.inserter.actionSteps;
|
||||
return Object.values(state.stepTypes).filter(
|
||||
({ group }) => group === 'actions',
|
||||
);
|
||||
}
|
||||
|
||||
export function getInserterLogicalSteps(state: State): Item[] {
|
||||
return state.inserter.logicalSteps;
|
||||
return Object.values(state.stepTypes).filter(
|
||||
({ group }) => group === 'logical',
|
||||
);
|
||||
}
|
||||
|
||||
export function getInserterPopoverAnchor(
|
||||
@ -39,6 +43,18 @@ export function getWorkflowData(state: State): Workflow {
|
||||
return state.workflowData;
|
||||
}
|
||||
|
||||
export function getWorkflowSaved(state: State): boolean {
|
||||
return state.workflowSaved;
|
||||
}
|
||||
|
||||
export function getSelectedStep(state: State): Step | undefined {
|
||||
return state.selectedStep;
|
||||
}
|
||||
|
||||
export function getStepType(state: State, key: string): StepType | undefined {
|
||||
return state.stepTypes[key] ?? undefined;
|
||||
}
|
||||
|
||||
export function getSelectedStepType(state: State): StepType | undefined {
|
||||
return getStepType(state, state.selectedStep?.key);
|
||||
}
|
||||
|
31
mailpoet/assets/js/src/automation/editor/store/store.ts
Normal file
31
mailpoet/assets/js/src/automation/editor/store/store.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { createReduxStore, register, StoreDescriptor } from '@wordpress/data';
|
||||
import { controls } from '@wordpress/data-controls';
|
||||
import * as actions from './actions';
|
||||
import { storeName } from './constants';
|
||||
import { initialState } from './initial_state';
|
||||
import { reducer } from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
import { State } from './types';
|
||||
import { OmitFirstArgs } from '../../../types';
|
||||
|
||||
type StoreType = Omit<StoreDescriptor, 'name'> & {
|
||||
name: typeof storeName;
|
||||
};
|
||||
|
||||
export const store = createReduxStore<State>(storeName, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the "Action" type is missing thunks with "dispatch"
|
||||
actions: actions as any,
|
||||
controls,
|
||||
selectors,
|
||||
reducer,
|
||||
initialState,
|
||||
}) as StoreType;
|
||||
|
||||
type StoreKey = typeof storeName | StoreType;
|
||||
|
||||
declare module '@wordpress/data' {
|
||||
function select(key: StoreKey): OmitFirstArgs<typeof selectors>;
|
||||
function dispatch(key: StoreKey): typeof actions;
|
||||
}
|
||||
|
||||
register(store);
|
@ -1,17 +1,29 @@
|
||||
import { Item } from '../components/inserter/item';
|
||||
import { ComponentType } from 'react';
|
||||
import { Step, Workflow } from '../components/workflow/types';
|
||||
|
||||
export interface AutomationEditorWindow extends Window {
|
||||
mailpoet_automation_workflow: Workflow;
|
||||
}
|
||||
|
||||
export type StepGroup = 'actions' | 'logical';
|
||||
|
||||
export type StepType = {
|
||||
key: string;
|
||||
group: StepGroup;
|
||||
title: string;
|
||||
description: string;
|
||||
subtitle: (step: Step) => JSX.Element | string;
|
||||
icon: ComponentType;
|
||||
edit: ComponentType;
|
||||
foreground: string;
|
||||
background: string;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
stepTypes: Record<string, StepType>;
|
||||
workflowData: Workflow;
|
||||
workflowSaved: boolean;
|
||||
selectedStep: Step | undefined;
|
||||
inserter: {
|
||||
actionSteps: Item[];
|
||||
logicalSteps: Item[];
|
||||
};
|
||||
inserterSidebar: {
|
||||
isOpened: boolean;
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { registerStepType } from '../../editor/store';
|
||||
import { step as DelayStep } from './steps/delay';
|
||||
|
||||
export const initialize = (): void => {
|
||||
registerStepType(DelayStep);
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
import {
|
||||
PanelBody,
|
||||
TextControl,
|
||||
SelectControl,
|
||||
Flex,
|
||||
FlexItem,
|
||||
} from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { PlainBodyTitle } from '../../../../editor/components/panel';
|
||||
import { store } from '../../../../editor/store';
|
||||
import { DelayTypeOptions } from './types/delayTypes';
|
||||
|
||||
export function Edit(): JSX.Element {
|
||||
const { selectedStep } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelBody opened>
|
||||
<PlainBodyTitle title="Wait for" />
|
||||
<Flex align="top">
|
||||
<FlexItem style={{ flex: '1 1 0' }}>
|
||||
<TextControl
|
||||
label=""
|
||||
type="number"
|
||||
placeholder="Number"
|
||||
value={(selectedStep.args.delay as string) ?? ''}
|
||||
onChange={(rawValue) => {
|
||||
const value: number =
|
||||
rawValue.length === 0 || parseInt(rawValue, 10) < 1
|
||||
? 1
|
||||
: parseInt(rawValue, 10);
|
||||
dispatch(store).updateStepArgs(selectedStep.id, 'delay', value);
|
||||
}}
|
||||
/>
|
||||
</FlexItem>
|
||||
<FlexItem style={{ flex: '1 1 0' }}>
|
||||
<SelectControl
|
||||
label=""
|
||||
value={(selectedStep.args.delay_type as string) ?? 'HOURS'}
|
||||
options={DelayTypeOptions}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'delay_type',
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export function Icon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.79683 0.666656C4.6635 0.666656 0.463501 4.86666 0.463501 9.99999C0.463501 15.1333 4.6635 19.3333 9.79683 19.3333C14.9302 19.3333 19.1302 15.1333 19.1302 9.99999C19.1302 4.86666 14.9302 0.666656 9.79683 0.666656ZM9.79683 17.4667C5.68083 17.4667 2.33017 14.116 2.33017 9.99999C2.33017 5.88399 5.68083 2.53332 9.79683 2.53332C13.9128 2.53332 17.2635 5.88399 17.2635 9.99999C17.2635 14.116 13.9128 17.4667 9.79683 17.4667ZM10.2635 5.33332H8.8635V10.9333L13.7168 13.92L14.4635 12.7067L10.2635 10.1867V5.33332Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { Icon } from './icon';
|
||||
import { Edit } from './edit';
|
||||
import { StepType } from '../../../../editor/store/types';
|
||||
import { DelayTypeOptions } from './types/delayTypes';
|
||||
|
||||
const getDelayInformation = (delayTypeValue: string, value: number): string =>
|
||||
DelayTypeOptions.reduce((previousValue, current): string => {
|
||||
if (current.value !== delayTypeValue) {
|
||||
return previousValue;
|
||||
}
|
||||
return current.subtitle(value);
|
||||
}, '');
|
||||
|
||||
export const step: StepType = {
|
||||
key: 'core:delay',
|
||||
group: 'actions',
|
||||
title: 'Delay',
|
||||
foreground: '#7F54B3',
|
||||
background: '#f7edf7',
|
||||
description: 'Wait some time before proceeding with the steps below',
|
||||
subtitle: (data): string => {
|
||||
if (!data.args.delay || !data.args.delay_type) {
|
||||
return 'Not set up yet.';
|
||||
}
|
||||
|
||||
return getDelayInformation(
|
||||
data.args.delay_type as string,
|
||||
data.args.delay as number,
|
||||
);
|
||||
},
|
||||
icon: Icon,
|
||||
edit: Edit,
|
||||
} as const;
|
@ -0,0 +1,25 @@
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
|
||||
export type DelayTypes = SelectControl.Option & {
|
||||
subtitle: (value: number) => string;
|
||||
};
|
||||
export const DelayTypeOptions: DelayTypes[] = [
|
||||
{
|
||||
label: 'Hours',
|
||||
subtitle: (value: number) =>
|
||||
`Wait for ${value} ${value === 1 ? 'hour' : 'hours'}`,
|
||||
value: 'HOURS',
|
||||
},
|
||||
{
|
||||
label: 'Days',
|
||||
subtitle: (value: number) =>
|
||||
`Wait for ${value} ${value === 1 ? 'day' : 'days'}`,
|
||||
value: 'DAYS',
|
||||
},
|
||||
{
|
||||
label: 'Weeks',
|
||||
subtitle: (value: number) =>
|
||||
`Wait for ${value} ${value === 1 ? 'week' : 'weeks'}`,
|
||||
value: 'WEEKS',
|
||||
},
|
||||
];
|
@ -0,0 +1,26 @@
|
||||
import classnames from 'classnames';
|
||||
import { Button as WpButton } from '@wordpress/components';
|
||||
|
||||
type ExtendedProps = {
|
||||
variant: WpButton.ButtonVariant | 'sidebar-primary';
|
||||
centered?: boolean;
|
||||
};
|
||||
|
||||
type Props =
|
||||
| (Omit<WpButton.ButtonProps, keyof ExtendedProps> & ExtendedProps)
|
||||
| (Omit<WpButton.AnchorProps, keyof ExtendedProps> & ExtendedProps);
|
||||
|
||||
export function Button({ centered, variant, ...props }: Props): JSX.Element {
|
||||
return (
|
||||
<WpButton
|
||||
className={classnames([
|
||||
variant === 'sidebar-primary'
|
||||
? 'mailpoet-automation-button-sidebar-primary'
|
||||
: '',
|
||||
centered ? 'mailpoet-automation-button-centered' : '',
|
||||
])}
|
||||
variant={variant === 'sidebar-primary' ? 'primary' : variant}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { registerStepType } from '../../editor/store';
|
||||
import { step as SendEmailStep } from './steps/send_email';
|
||||
|
||||
export const initialize = (): void => {
|
||||
registerStepType(SendEmailStep);
|
||||
};
|
@ -0,0 +1,68 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { plus } from '@wordpress/icons';
|
||||
import { Button } from '../../../components/button';
|
||||
import { store } from '../../../../../editor/store';
|
||||
import { MailPoet } from '../../../../../../mailpoet';
|
||||
|
||||
export function DesignEmailButton(): JSX.Element {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const { selectedStep, workflowId, workflowSaved } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
workflowId: select(store).getWorkflowData().id,
|
||||
workflowSaved: select(store).getSelectedStep(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const emailId = selectedStep?.args?.email_id as string | undefined;
|
||||
const workflowStepId = selectedStep.id;
|
||||
|
||||
const createEmail = useCallback(async () => {
|
||||
setIsSaving(true);
|
||||
const response = await MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'create',
|
||||
data: {
|
||||
type: 'automation',
|
||||
subject: '',
|
||||
options: {
|
||||
workflowId,
|
||||
workflowStepId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(store).updateStepArgs(
|
||||
workflowStepId,
|
||||
'email_id',
|
||||
parseInt(response.data.id as string, 10),
|
||||
);
|
||||
|
||||
dispatch(store).save();
|
||||
}, [workflowId, workflowStepId]);
|
||||
|
||||
// This component is rendered only when no email ID is set. Once we have the ID
|
||||
// and the workflow is saved, we can safely redirect to the email design flow.
|
||||
useEffect(() => {
|
||||
if (emailId && workflowSaved) {
|
||||
window.location.href = `admin.php?page=mailpoet-newsletter-editor&id=${emailId}`;
|
||||
}
|
||||
}, [emailId, workflowSaved]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="sidebar-primary"
|
||||
centered
|
||||
icon={plus}
|
||||
onClick={createEmail}
|
||||
isBusy={isSaving}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Design email
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { PanelBody, TextareaControl, TextControl } from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { DesignEmailButton } from './design_email_button';
|
||||
import { Thumbnail } from './thumbnail';
|
||||
import { PlainBodyTitle } from '../../../../../editor/components/panel';
|
||||
import { store } from '../../../../../editor/store';
|
||||
import { StepName } from '../../../../../editor/components/panel/step-name';
|
||||
|
||||
function SingleLineTextareaControl(
|
||||
props: ComponentProps<typeof TextareaControl>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<TextareaControl
|
||||
{...props}
|
||||
onChange={(value) =>
|
||||
// replace a newline or a group of multiple newlines by a space (text pasting)
|
||||
props.onChange(value.replaceAll(/(\r?\n)+/g, ' '))
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
// disable inserting newlines via "Enter" key
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (props.onKeyDown) {
|
||||
props.onKeyDown(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailPanel(): JSX.Element {
|
||||
const { selectedStep, selectedStepType } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
selectedStepType: select(store).getSelectedStepType(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelBody opened>
|
||||
<StepName
|
||||
currentName={(selectedStep.args.name as string) ?? ''}
|
||||
defaultName={selectedStepType.title}
|
||||
update={(value) => {
|
||||
dispatch(store).updateStepArgs(selectedStep.id, 'name', value);
|
||||
}}
|
||||
/>
|
||||
<TextControl
|
||||
label="“From” name"
|
||||
placeholder="John Doe"
|
||||
value={(selectedStep.args.sender_name as string) ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(selectedStep.id, 'sender_name', value)
|
||||
}
|
||||
/>
|
||||
<TextControl
|
||||
type="email"
|
||||
label="“From” email address"
|
||||
placeholder="you@domain.com"
|
||||
value={(selectedStep.args.sender_address as string) ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'sender_address',
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SingleLineTextareaControl
|
||||
label="Subject"
|
||||
placeholder="Type in subject…"
|
||||
value={(selectedStep.args.subject as string) ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(selectedStep.id, 'subject', value)
|
||||
}
|
||||
/>
|
||||
<SingleLineTextareaControl
|
||||
label="Preheader"
|
||||
placeholder="Type in preheader…"
|
||||
value={(selectedStep.args.preheader as string) ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(selectedStep.id, 'preheader', value)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mailpoet-automation-email-content-separator" />
|
||||
<PlainBodyTitle title="Email content" />
|
||||
{selectedStep.args.email_id ? (
|
||||
<Thumbnail emailId={selectedStep.args.email_id as number} />
|
||||
) : (
|
||||
<DesignEmailButton />
|
||||
)}
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { PremiumModal } from 'common/premium_modal';
|
||||
import { Hooks } from 'wp-js-hooks';
|
||||
import { store } from '../../../../../editor/store';
|
||||
|
||||
export function GoogleAnalyticsPanel(): JSX.Element {
|
||||
const { selectedStep } = useSelect(
|
||||
(select) => ({ selectedStep: select(store).getSelectedStep() }),
|
||||
[],
|
||||
);
|
||||
|
||||
const enabled = typeof selectedStep.args?.ga_campaign !== 'undefined';
|
||||
const panelBody = Hooks.applyFilters(
|
||||
'mailpoet.automation.send_email.google_analytics_panel',
|
||||
<PremiumModal
|
||||
onRequestClose={() =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'ga_campaign',
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
{__(
|
||||
'Google Analytics tracking is not available in the free version of the MailPoet plugin.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</PremiumModal>,
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelBody title="Google analytics" initialOpen={false}>
|
||||
<ToggleControl
|
||||
label="Enable custom GA tracking"
|
||||
checked={enabled}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'ga_campaign',
|
||||
value ? '' : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{enabled && panelBody}
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { EmailPanel } from './email_panel';
|
||||
import { GoogleAnalyticsPanel } from './google_analytics_panel';
|
||||
import { ReplyToPanel } from './reply_to_panel';
|
||||
|
||||
export function Edit(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<EmailPanel />
|
||||
<ReplyToPanel />
|
||||
<GoogleAnalyticsPanel />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { store } from '../../../../../editor/store';
|
||||
|
||||
export function ReplyToPanel(): JSX.Element {
|
||||
const { selectedStep } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(store).getSelectedStep(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const replyToName = selectedStep.args.reply_to_name as string | undefined;
|
||||
const replyToAddress = selectedStep.args.reply_to_address as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const enabled =
|
||||
typeof replyToName !== 'undefined' || typeof replyToAddress !== 'undefined';
|
||||
|
||||
return (
|
||||
<PanelBody title="Reply to" initialOpen={false}>
|
||||
<ToggleControl
|
||||
label="Use different email address for getting replies to the email"
|
||||
checked={enabled}
|
||||
onChange={(value) => {
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'reply_to_name',
|
||||
value ? '' : undefined,
|
||||
);
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'reply_to_address',
|
||||
value ? '' : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
<TextControl
|
||||
label="“Reply to” name"
|
||||
placeholder="John Doe"
|
||||
value={replyToName ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'reply_to_name',
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
type="email"
|
||||
label="“Reply to” email address"
|
||||
placeholder="you@domain.com"
|
||||
value={replyToAddress ?? ''}
|
||||
onChange={(value) =>
|
||||
dispatch(store).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'reply_to_address',
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import { ComponentProps, ComponentType, useEffect, useState } from 'react';
|
||||
import { Spinner as WpSpinner } from '@wordpress/components';
|
||||
import { Button } from '../../../components/button';
|
||||
import { MailPoetAjax } from '../../../../../../ajax';
|
||||
|
||||
// @types/wordpress__components don't define "className", which is supported
|
||||
const Spinner = WpSpinner as ComponentType<
|
||||
ComponentProps<typeof WpSpinner> & { className?: string }
|
||||
>;
|
||||
|
||||
type Props = {
|
||||
emailId: number;
|
||||
};
|
||||
|
||||
export function Thumbnail({ emailId }: Props): JSX.Element {
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const data = await MailPoetAjax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'get',
|
||||
data: { id: emailId },
|
||||
});
|
||||
|
||||
// TODO: we need to implement thumbnails backend first
|
||||
if (data?.data?.thumbnail_url) {
|
||||
setThumbnailUrl(data.data.thumbnail_url as string);
|
||||
}
|
||||
};
|
||||
|
||||
void getData();
|
||||
}, [emailId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mailpoet-automation-thumbnail-box">
|
||||
{thumbnailUrl ? (
|
||||
<div className="mailpoet-automation-thumbnail-wrapper">
|
||||
<img
|
||||
className="mailpoet-automation-thumbnail-image"
|
||||
src={thumbnailUrl}
|
||||
alt="Email thumbnail"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Spinner className="mailpoet-automation-thumbnail-spinner" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mailpoet-automation-thumbnail-buttons">
|
||||
<Button
|
||||
variant="sidebar-primary"
|
||||
centered
|
||||
href={`?page=mailpoet-newsletter-editor&id=${emailId}`}
|
||||
>
|
||||
Edit content
|
||||
</Button>
|
||||
<Button variant="secondary" centered>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
export function Icon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.73578 1.5L8 6.01219L13.2642 1.5H2.73578ZM14.5 2.41638L8.48809 7.56944L8 7.98781L7.51191 7.56944L1.5 2.41638V10C1.5 10.2761 1.72386 10.5 2 10.5H14C14.2761 10.5 14.5 10.2761 14.5 10V2.41638ZM0 2C0 0.89543 0.89543 0 2 0H14C15.1046 0 16 0.895431 16 2V10C16 11.1046 15.1046 12 14 12H2C0.895431 12 0 11.1046 0 10V2Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Icon } from './icon';
|
||||
import { Edit } from './edit';
|
||||
import { StepType } from '../../../../editor/store/types';
|
||||
|
||||
export const step: StepType = {
|
||||
key: 'mailpoet:send-email',
|
||||
group: 'actions',
|
||||
title: 'Send email',
|
||||
description: 'An email will be sent to subscriber',
|
||||
subtitle: (data) => (data.args.name as string) ?? 'Send email',
|
||||
foreground: '#996800',
|
||||
background: '#FCF9E8',
|
||||
icon: Icon,
|
||||
edit: Edit,
|
||||
} as const;
|
@ -1,4 +1,4 @@
|
||||
import { EllipsisMenu, MenuItem } from '@woocommerce/components';
|
||||
import { EllipsisMenu, MenuItem } from '@woocommerce/components/build';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Workflow } from '../../workflow';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Search, TableCard } from '@woocommerce/components';
|
||||
import { Search, TableCard } from '@woocommerce/components/build';
|
||||
import { Button, Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getRow } from './get-row';
|
||||
@ -27,7 +27,7 @@ export function AutomationListing({ workflows, loading }: Props): JSX.Element {
|
||||
isLoading={workflows.length === 0 || loading}
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
rowKey={(data, i) => data[i].id}
|
||||
rowKey={(_, i) => workflows[i].id}
|
||||
query={{ page: 2 }}
|
||||
rowsPerPage={7}
|
||||
totalRows={workflows.length}
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { getQueryArg, removeQueryArgs } from '@wordpress/url';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Notice } from '../../notices/notice';
|
||||
|
||||
export const LISTING_NOTICE_PARAMETERS = {
|
||||
workflowHadBeenDeleted: 'mailpoet-had-been-deleted',
|
||||
workflowDeleted: 'mailpoet-workflow-deleted',
|
||||
};
|
||||
|
||||
export function WorkflowListingNotices(): JSX.Element {
|
||||
const workflowHadBeenDeleted = parseInt(
|
||||
getQueryArg(
|
||||
window.location.href,
|
||||
LISTING_NOTICE_PARAMETERS.workflowHadBeenDeleted,
|
||||
) as string,
|
||||
10,
|
||||
);
|
||||
const workflowDeleted = parseInt(
|
||||
getQueryArg(
|
||||
window.location.href,
|
||||
LISTING_NOTICE_PARAMETERS.workflowDeleted,
|
||||
) as string,
|
||||
10,
|
||||
);
|
||||
|
||||
if (Number.isNaN(workflowHadBeenDeleted) && Number.isNaN(workflowDeleted)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlWithoutNotices = removeQueryArgs(
|
||||
window.location.href,
|
||||
...Object.values(LISTING_NOTICE_PARAMETERS),
|
||||
);
|
||||
window.history.pushState('', '', urlWithoutNotices);
|
||||
if (workflowHadBeenDeleted) {
|
||||
return (
|
||||
<Notice type="error" closable timeout={false}>
|
||||
<p>
|
||||
{__(
|
||||
'You cannot edit this automation because it is in the Trash.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</p>
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
if (workflowDeleted) {
|
||||
return (
|
||||
<Notice type="success" closable timeout={false}>
|
||||
<p>{__('1 workflow moved to the Trash.', 'mailpoet')}</p>
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
@ -17,7 +17,8 @@ const createDelayStep = (nextStepId: string) => ({
|
||||
key: 'core:delay',
|
||||
next_step_id: nextStepId,
|
||||
args: {
|
||||
seconds: 60,
|
||||
delay: 1,
|
||||
delay_type: 'HOURS',
|
||||
},
|
||||
});
|
||||
|
||||
@ -70,18 +71,21 @@ export function CreateTestingWorkflowButton(): JSX.Element {
|
||||
}
|
||||
|
||||
type TemplateButtonProps = {
|
||||
template: string;
|
||||
slug: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function CreateWorkflowFromTemplateButton({
|
||||
template,
|
||||
slug,
|
||||
children,
|
||||
}: TemplateButtonProps): JSX.Element {
|
||||
const [createWorkflowFromTemplate, { loading, error }] = useMutation(
|
||||
'workflows/create-from-template',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
slug,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -91,12 +95,7 @@ export function CreateWorkflowFromTemplateButton({
|
||||
className="button button-primary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await createWorkflowFromTemplate({
|
||||
body: JSON.stringify({
|
||||
name: `Test from template ${new Date().toISOString()}`,
|
||||
template,
|
||||
}),
|
||||
});
|
||||
await createWorkflowFromTemplate();
|
||||
window.location.reload();
|
||||
}}
|
||||
disabled={loading}
|
||||
|
198
mailpoet/assets/js/src/common/authorize_sender_domain_modal.tsx
Normal file
198
mailpoet/assets/js/src/common/authorize_sender_domain_modal.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { noop } from 'lodash';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { Modal } from 'common/modal/modal';
|
||||
import {
|
||||
ManageSenderDomain,
|
||||
SenderDomainDnsItem,
|
||||
SenderDomainEntity,
|
||||
} from 'common/manage_sender_domain';
|
||||
import { isErrorResponse, Response, ErrorResponse } from 'ajax';
|
||||
|
||||
interface SenderDomainApiResponseType extends Response {
|
||||
data: SenderDomainDnsItem[];
|
||||
}
|
||||
|
||||
type VerifyResponseType = {
|
||||
dns: SenderDomainDnsItem[];
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
};
|
||||
interface SenderDomainApiVerifyResponseType extends Response {
|
||||
data: VerifyResponseType;
|
||||
}
|
||||
|
||||
type ApiActionType = 'fetch' | 'create' | 'verify';
|
||||
|
||||
/**
|
||||
* @param {string} domain - Sender Domain
|
||||
* @param {ApiActionType} type - action type
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const makeApiRequest = (domain: string, type: ApiActionType = 'fetch') => {
|
||||
let requestAction = 'getAuthorizedSenderDomains';
|
||||
|
||||
if (type === 'create') {
|
||||
requestAction = 'createAuthorizedSenderDomain';
|
||||
} else if (type === 'verify') {
|
||||
requestAction = 'verifyAuthorizedSenderDomain';
|
||||
}
|
||||
|
||||
return MailPoet.Ajax.post({
|
||||
api_version: MailPoet.apiVersion,
|
||||
endpoint: 'settings',
|
||||
action: requestAction,
|
||||
data: { domain },
|
||||
});
|
||||
};
|
||||
|
||||
const getApiErrorMessage = (error: { error?: ErrorResponse }): string =>
|
||||
isErrorResponse(error) && error.errors[0] && error.errors[0].message
|
||||
? error.errors[0].message
|
||||
: '';
|
||||
|
||||
const generateRowData = (senderDomain: string, dns: SenderDomainDnsItem[]) => {
|
||||
const row: SenderDomainEntity[] = [
|
||||
{
|
||||
domain: senderDomain,
|
||||
dns,
|
||||
},
|
||||
];
|
||||
return row;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
senderDomain: string;
|
||||
onRequestClose: () => void;
|
||||
setVerifiedSenderDomain?: (senderDomain: string) => void;
|
||||
useModal: boolean;
|
||||
};
|
||||
|
||||
function AuthorizeSenderDomainModal({
|
||||
senderDomain,
|
||||
onRequestClose,
|
||||
setVerifiedSenderDomain,
|
||||
useModal,
|
||||
}: Props): JSX.Element {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [loadingButton, setLoadingButton] = useState(false);
|
||||
const [rowData, setRowData] = useState<SenderDomainEntity[]>([]);
|
||||
const modalIsOpened = useRef<boolean>(false);
|
||||
|
||||
const performStateUpdate = (callback: (param) => void, args) => {
|
||||
if (!modalIsOpened.current) return; // do nothing if modal is not opened
|
||||
callback(args);
|
||||
};
|
||||
|
||||
const verifyDnsButtonClicked = async () => {
|
||||
setLoadingButton(true);
|
||||
|
||||
try {
|
||||
const res: SenderDomainApiVerifyResponseType = await makeApiRequest(
|
||||
senderDomain,
|
||||
'verify',
|
||||
);
|
||||
if (!modalIsOpened.current) return;
|
||||
|
||||
setRowData(generateRowData(senderDomain, res.data.dns));
|
||||
if (res.data.ok) {
|
||||
// record verified, close the modal
|
||||
setErrorMessage('');
|
||||
setVerifiedSenderDomain(senderDomain);
|
||||
onRequestClose();
|
||||
}
|
||||
} catch (e) {
|
||||
const error: { error?: ErrorResponse; meta?: VerifyResponseType } = e;
|
||||
if (!modalIsOpened.current) return;
|
||||
|
||||
setRowData(generateRowData(senderDomain, error?.meta?.dns || []));
|
||||
const apiErrorMessage = getApiErrorMessage(e);
|
||||
setErrorMessage(apiErrorMessage || error?.meta?.error || '');
|
||||
}
|
||||
|
||||
performStateUpdate(setLoadingButton, false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!senderDomain) {
|
||||
return null;
|
||||
}
|
||||
modalIsOpened.current = true;
|
||||
|
||||
const allSenderDomains = window.mailpoet_all_sender_domains || [];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (allSenderDomains.includes(senderDomain)) {
|
||||
// sender domain already exist
|
||||
const res: SenderDomainApiResponseType = await makeApiRequest(
|
||||
senderDomain,
|
||||
);
|
||||
performStateUpdate(
|
||||
setRowData,
|
||||
generateRowData(senderDomain, res.data),
|
||||
);
|
||||
} else {
|
||||
// create new sender domain
|
||||
const res: SenderDomainApiResponseType = await makeApiRequest(
|
||||
senderDomain,
|
||||
'create',
|
||||
);
|
||||
performStateUpdate(
|
||||
setRowData,
|
||||
generateRowData(senderDomain, res.data),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const apiErrorMessage = getApiErrorMessage(e);
|
||||
|
||||
performStateUpdate(setErrorMessage, apiErrorMessage);
|
||||
}
|
||||
})().catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
return () => {
|
||||
modalIsOpened.current = false;
|
||||
};
|
||||
}, [senderDomain]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{errorMessage && (
|
||||
<strong className="mailpoet_error_item mailpoet_error">
|
||||
{' '}
|
||||
{errorMessage}{' '}
|
||||
</strong>
|
||||
)}
|
||||
<ManageSenderDomain
|
||||
rows={rowData}
|
||||
verifyDnsButtonClicked={verifyDnsButtonClicked}
|
||||
loadingButton={loadingButton}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return useModal ? (
|
||||
<Modal
|
||||
onRequestClose={onRequestClose}
|
||||
contentClassName="authorize-sender-domain-modal"
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
);
|
||||
}
|
||||
|
||||
AuthorizeSenderDomainModal.propTypes = {
|
||||
senderDomain: PropTypes.string.isRequired,
|
||||
useModal: PropTypes.bool,
|
||||
};
|
||||
|
||||
AuthorizeSenderDomainModal.defaultProps = {
|
||||
setVerifiedSenderDomain: noop,
|
||||
useModal: true,
|
||||
};
|
||||
|
||||
export { AuthorizeSenderDomainModal };
|
@ -0,0 +1,109 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import classnames from 'classnames';
|
||||
import { extractEmailDomain, extractPageNameFromUrl } from 'common/functions';
|
||||
import { Tabs } from './tabs/tabs';
|
||||
import { Tab } from './tabs/tab';
|
||||
import { Modal } from './modal/modal';
|
||||
import { AuthorizeSenderEmailModal } from './authorize_sender_email_modal';
|
||||
import { AuthorizeSenderDomainModal } from './authorize_sender_domain_modal';
|
||||
|
||||
const trackEvent = (type: 'email' | 'domain') => {
|
||||
const page = `${extractPageNameFromUrl() || 'some other'} page`;
|
||||
|
||||
if (type === 'email') {
|
||||
MailPoet.trackEvent('MSS in plugin authorize email', {
|
||||
'attempt to authorize': type,
|
||||
'original page': page,
|
||||
});
|
||||
} else if (type === 'domain') {
|
||||
MailPoet.trackEvent('MSS in plugin verify sender domain', {
|
||||
'attempt to authorize': type,
|
||||
'original page': page,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onRequestClose: () => void;
|
||||
senderEmail: string;
|
||||
onSuccessAction: (param: { type: 'email' | 'domain'; data: string }) => void;
|
||||
showSenderEmailTab: boolean;
|
||||
showSenderDomainTab: boolean;
|
||||
initialTab: 'sender_email' | 'sender_domain';
|
||||
};
|
||||
|
||||
function AuthorizeSenderEmailAndDomainModal({
|
||||
onRequestClose,
|
||||
senderEmail,
|
||||
onSuccessAction,
|
||||
showSenderEmailTab = false,
|
||||
showSenderDomainTab = false,
|
||||
initialTab = 'sender_email',
|
||||
}: Props): JSX.Element {
|
||||
if (!senderEmail) return null;
|
||||
|
||||
const emailAddressDomain = extractEmailDomain(senderEmail);
|
||||
|
||||
if (showSenderEmailTab) {
|
||||
trackEvent('email');
|
||||
}
|
||||
|
||||
if (showSenderDomainTab) {
|
||||
trackEvent('domain');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onRequestClose={onRequestClose}
|
||||
contentClassName="authorize-sender-email-and-domain-modal"
|
||||
>
|
||||
<Tabs activeKey={initialTab}>
|
||||
<Tab
|
||||
key="sender_email"
|
||||
className={classnames({
|
||||
mailpoet_hidden: !showSenderEmailTab,
|
||||
})}
|
||||
title={MailPoet.I18n.t(
|
||||
'authorizeSenderEmailAndDomainModalSenderEmailTabTitle',
|
||||
)}
|
||||
>
|
||||
{showSenderEmailTab && (
|
||||
<AuthorizeSenderEmailModal
|
||||
useModal={false}
|
||||
senderEmail={senderEmail}
|
||||
onRequestClose={onRequestClose}
|
||||
setAuthorizedAddress={(authorizedEmailAddress) => {
|
||||
onSuccessAction({
|
||||
type: 'email',
|
||||
data: authorizedEmailAddress,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
key="sender_domain"
|
||||
className={classnames({
|
||||
mailpoet_hidden: !showSenderDomainTab,
|
||||
})}
|
||||
title={MailPoet.I18n.t(
|
||||
'authorizeSenderEmailAndDomainModalSenderDomainTabTitle',
|
||||
)}
|
||||
>
|
||||
{showSenderDomainTab && (
|
||||
<AuthorizeSenderDomainModal
|
||||
useModal={false}
|
||||
senderDomain={emailAddressDomain}
|
||||
onRequestClose={onRequestClose}
|
||||
setVerifiedSenderDomain={(verifiedSenderDomain) => {
|
||||
onSuccessAction({ type: 'domain', data: verifiedSenderDomain });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthorizeSenderEmailAndDomainModal };
|
@ -7,6 +7,7 @@ import { MailPoet } from 'mailpoet';
|
||||
import { Modal } from 'common/modal/modal';
|
||||
import { Button, Loader } from 'common';
|
||||
import { isErrorResponse, ErrorResponse } from 'ajax';
|
||||
import { Grid } from 'common/grid';
|
||||
|
||||
const SET_INTERVAL_PERFORM_REQUEST_EVERY_SECONDS = 15;
|
||||
|
||||
@ -69,12 +70,14 @@ type Props = {
|
||||
senderEmail: string;
|
||||
onRequestClose: () => void;
|
||||
setAuthorizedAddress?: (emailAddress: string) => void;
|
||||
useModal: boolean;
|
||||
};
|
||||
|
||||
function AuthorizeSenderEmailModal({
|
||||
senderEmail,
|
||||
onRequestClose,
|
||||
setAuthorizedAddress,
|
||||
useModal,
|
||||
}: Props) {
|
||||
const [createEmailApiResponse, setCreateEmailApiResponse] =
|
||||
useState<boolean>(null);
|
||||
@ -181,15 +184,8 @@ function AuthorizeSenderEmailModal({
|
||||
*/
|
||||
}, [senderEmailAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={MailPoet.I18n.t('authorizeSenderEmailModalTitle').replace(
|
||||
'[senderEmail]',
|
||||
senderEmailAddress,
|
||||
)}
|
||||
onRequestClose={onRequestClose}
|
||||
contentClassName="authorize-sender-email-modal"
|
||||
>
|
||||
const content = (
|
||||
<>
|
||||
{createEmailApiResponse && (
|
||||
<p>
|
||||
{ReactStringReplace(
|
||||
@ -208,7 +204,11 @@ function AuthorizeSenderEmailModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{showLoader && <Loader size={64} />}
|
||||
{showLoader && (
|
||||
<Grid.Column align="center">
|
||||
<Loader size={64} />
|
||||
</Grid.Column>
|
||||
)}
|
||||
|
||||
{confirmEmailApiResponse && (
|
||||
<>
|
||||
@ -219,16 +219,33 @@ function AuthorizeSenderEmailModal({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return useModal ? (
|
||||
<Modal
|
||||
title={MailPoet.I18n.t('authorizeSenderEmailModalTitle').replace(
|
||||
'[senderEmail]',
|
||||
senderEmailAddress,
|
||||
)}
|
||||
onRequestClose={onRequestClose}
|
||||
contentClassName="authorize-sender-email-modal"
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
);
|
||||
}
|
||||
|
||||
AuthorizeSenderEmailModal.propTypes = {
|
||||
senderEmail: PropTypes.string.isRequired,
|
||||
useModal: PropTypes.bool,
|
||||
};
|
||||
|
||||
AuthorizeSenderEmailModal.defaultProps = {
|
||||
setAuthorizedAddress: noop,
|
||||
useModal: true,
|
||||
};
|
||||
|
||||
export { AuthorizeSenderEmailModal };
|
||||
|
@ -17,10 +17,14 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
target?: '_blank' | '_self' | '_parent' | '_top' | string;
|
||||
automationId?: string;
|
||||
className?: string;
|
||||
dataTip?: boolean;
|
||||
dataFor?: string;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
dataFor,
|
||||
dataTip,
|
||||
dimension,
|
||||
variant,
|
||||
withSpinner,
|
||||
@ -56,6 +60,8 @@ export function Button({
|
||||
'button-small': dimension === 'small',
|
||||
})}
|
||||
data-automation-id={automationId}
|
||||
data-tip={dataTip}
|
||||
data-for={dataFor}
|
||||
>
|
||||
{iconStart}
|
||||
{children && <span>{children}</span>}
|
||||
|
@ -0,0 +1,54 @@
|
||||
import { MailPoet } from 'mailpoet';
|
||||
import { extractEmailDomain } from 'common/functions';
|
||||
|
||||
/**
|
||||
* @param {string} email - Email address
|
||||
* @param {ApiActionType} type - action type
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const makeApiRequest = (domain: string) =>
|
||||
MailPoet.Ajax.post({
|
||||
api_version: MailPoet.apiVersion,
|
||||
endpoint: 'settings',
|
||||
action: 'checkDomainDmarcPolicy',
|
||||
data: { domain },
|
||||
});
|
||||
|
||||
/**
|
||||
* Check domain DMARC policy
|
||||
*
|
||||
* returns `false` if not required, `true` if DMARC policy is Restricted
|
||||
* @param {string} email Email address
|
||||
* @param {boolean} isMssActive Is MailPoet sending service active?
|
||||
* @returns {Promise<boolean>} false if not required, `true` if DMARC policy is Restricted
|
||||
*/
|
||||
const checkSenderEmailDomainDmarcPolicy = async (
|
||||
email: string,
|
||||
isMssActive = window.mailpoet_mss_active,
|
||||
) => {
|
||||
if (!email) return false;
|
||||
|
||||
if (!isMssActive) {
|
||||
return false;
|
||||
}
|
||||
const emailAddressDomain = extractEmailDomain(email);
|
||||
|
||||
const isDomainVerified = (
|
||||
window.mailpoet_verified_sender_domains || []
|
||||
).includes(emailAddressDomain);
|
||||
if (isDomainVerified) {
|
||||
// do nothing if the email domain is verified
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await makeApiRequest(emailAddressDomain);
|
||||
const isDmarcPolicyRestricted = Boolean(res?.data?.isDmarcPolicyRestricted);
|
||||
return isDmarcPolicyRestricted;
|
||||
} catch (error) {
|
||||
// do nothing for now when the request fails
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export { checkSenderEmailDomainDmarcPolicy };
|
@ -0,0 +1,5 @@
|
||||
export const extractEmailDomain = (email: string): string =>
|
||||
String(email || '')
|
||||
.split('@')
|
||||
.pop()
|
||||
.toLowerCase();
|
@ -0,0 +1,7 @@
|
||||
export const extractPageNameFromUrl = () => {
|
||||
const searchParam = new URLSearchParams(window.location.search);
|
||||
const searchParamPage = searchParam.get('page') || '';
|
||||
const mailpoetPageName = searchParamPage.replace('mailpoet-', '');
|
||||
const pageNameFromUrl = mailpoetPageName || searchParamPage || '';
|
||||
return pageNameFromUrl;
|
||||
};
|
@ -3,3 +3,5 @@ export * from './t';
|
||||
export * from './is_email';
|
||||
export * from './set_lowercase_value';
|
||||
export * from './parsley_helper_functions';
|
||||
export * from './extract_email_domain';
|
||||
export * from './extract_page_name_from_url';
|
||||
|
@ -64,7 +64,7 @@ export const addOrUpdateError = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias to Parsely removeError method
|
||||
* Alias to Parsely reset method
|
||||
*
|
||||
* Remove an already present error message.
|
||||
*
|
||||
@ -72,13 +72,11 @@ export const addOrUpdateError = (
|
||||
* @param {string} parsleyFieldName Parsely Error name
|
||||
* @returns
|
||||
*/
|
||||
export const removeError = (
|
||||
export const resetFieldError = (
|
||||
domElementSelector: string,
|
||||
parsleyFieldName: string,
|
||||
) => {
|
||||
if (!doesErrorFieldExist(parsleyFieldName)) return; // do nothing if error message does not exist
|
||||
|
||||
jQuery(domElementSelector)
|
||||
.parsley()
|
||||
.removeError(parsleyFieldName, { updateClass: true });
|
||||
jQuery(domElementSelector).parsley().reset();
|
||||
};
|
||||
|
@ -0,0 +1,55 @@
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip } from 'common/tooltip/tooltip';
|
||||
import { Button } from 'common/button/button';
|
||||
import { useRef } from 'react';
|
||||
import { copy } from '@wordpress/icons';
|
||||
|
||||
const copyTextToClipboard = (value: string) => {
|
||||
if (!navigator.clipboard) {
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(value)
|
||||
.then()
|
||||
.catch(() => {
|
||||
// noop
|
||||
});
|
||||
};
|
||||
|
||||
function DomainKeyComponent({ className = '', tooltip = '', ...props }) {
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const performActionOnClick = () => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
copyTextToClipboard(inputRef.current?.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classnames(className, 'mailpoet-form-input', {})}>
|
||||
<input ref={inputRef} onClick={performActionOnClick} {...props} />
|
||||
|
||||
{tooltip && (
|
||||
<>
|
||||
<Button
|
||||
iconStart={copy}
|
||||
variant="secondary"
|
||||
onClick={performActionOnClick}
|
||||
dataTip
|
||||
dataFor={props.name}
|
||||
/>
|
||||
<Tooltip id={props.name} place="top">
|
||||
<span> {tooltip} </span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DomainKeyComponent };
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user