Compare commits

...

296 Commits

Author SHA1 Message Date
0180a1245b Merge pull request #1623 from mailpoet/release-3-14
Release 3.14
2018-11-14 11:35:14 +02:00
593071efff Release 3.14 2018-11-13 17:53:50 +01:00
3c46c87da9 Merge pull request #1622 from mailpoet/display-alc-warning
Display ALC warning [MAILPOET-1631]
2018-11-13 11:27:22 -05:00
9d1639c100 Merge pull request #1624 from mailpoet/fonts-again
Fix custom fonts for buttons [MAILPOET-1633]
2018-11-13 11:20:15 -05:00
8ff4f7720b Fix custom fonts for buttons [MAILPOET-1633] 2018-11-13 16:26:59 +01:00
13d35c4385 Display ALC warning
[MAILPOET-1631]
2018-11-13 13:08:31 +01:00
c778b84932 Merge pull request #1616 from mailpoet/fix-margin
Add space between save and next buttons [MAILPOET-1630]
2018-11-13 05:47:54 -05:00
1ee06d7196 Merge pull request #1617 from mailpoet/menu-icon
Fix menu icon on editor page [MAILPOET-1629]
2018-11-13 05:27:29 -05:00
5134c60291 Make gap between next and save smaller and fix button size on mobile screen
[MAILPOET-1630]
2018-11-13 09:23:59 +01:00
57486a522a Fix menu icon on editor page [MAILPOET-1629] 2018-11-12 19:58:05 +01:00
8ae34e83ec Add space between save and next buttons
[MAILPOET-1630]
2018-11-12 19:31:50 +01:00
19d13c5165 Merge pull request #1613 from mailpoet/readme-change
Update readme.txt: Multisites are not supported
2018-11-12 09:06:37 -05:00
f64b42d5cd Merge pull request #1594 from mailpoet/update-first-pn-logic
Send posts published before post notification has been created [MAILPOET-1560]
2018-11-12 08:44:02 -05:00
78539d0e6f Update readme.txt: Multisites are not supported
[MAILPOET-1628]
2018-11-12 14:30:33 +01:00
2e6dd9b20f Merge pull request #1605 from mailpoet/receive-post-notification-at
Add Acceptance test for receive post notification [MQ-69]
2018-11-12 07:51:18 -05:00
d1b91cd661 Merge pull request #1610 from mailpoet/remove-data-post-id
Remove unwanted data-post-id attributes from templates [MAILPOET-1620]
2018-11-12 07:37:55 -05:00
fb3fdbf1ed Merge pull request #1612 from mailpoet/gdpr
GDPR: add guide in Settings [MAILPOET-1540]
2018-11-12 07:17:16 -05:00
a0e8e52183 Merge pull request #1589 from mailpoet/congratulate-first-newsletter
Show congratulation page after first newsletter [MAILPOET-1468]
2018-11-12 05:31:20 -05:00
9364a422b3 Add Acceptance test for receive post notification
[MQ-69]
2018-11-12 11:00:36 +01:00
5e16bc4184 Add test and remove redundant call
[MAILPOET-1560]
2018-11-12 08:33:00 +01:00
75295e55c2 Schedule post notification after it's been created
[MAILPOET-1560]
2018-11-12 08:33:00 +01:00
13329d568d Send posts published before post notification has been created
[MAILPOET-1560]
2018-11-12 08:33:00 +01:00
b9993da62c Show congratulation page after first newsletter
[MAILPOET-1468]
2018-11-12 08:10:46 +01:00
5605c0c9dd Added Your Privacy section in Help 2018-11-11 10:13:53 +01:00
c60a7a233c Added GDPR Compliant section in Settings > Basics 2018-11-11 10:13:53 +01:00
6c8705f3c2 Merge pull request #1611 from mailpoet/di-public-services
Make all services registered in DI public [MAILPOET-1621]
2018-11-09 10:18:29 -05:00
99a889fab6 Merge pull request #1599 from mailpoet/editor-error
put validation messages on new line [MAILPOET-1569]
2018-11-08 12:59:25 -05:00
a1457678b0 Merge pull request #1577 from mailpoet/fonts
adding custom fonts [MAILPOET-1493]
2018-11-08 11:18:36 -05:00
b6e0b7ceb0 Make all services registered in DI public
In ideal application one needs only on public service some bootstrap and that is why all services are private as default. Our code is not ideal yet so we have to declare services as public.
[MAILPOET-1621]
2018-11-08 16:13:40 +01:00
8d3e751845 Merge pull request #1602 from mailpoet/fix-paused-post-notifications
Don't block new post notifications by earlier ones paused during sending [MAILPOET-1609]
2018-11-08 10:10:54 -05:00
410aa76d37 Merge pull request #1603 from mailpoet/retina-friendly-menu-icon
Make menu icon retina friendly [MAILPOET-1559]
2018-11-08 09:27:41 -05:00
wxa
c9b350adab Remove unwanted data-post-id attributes from templates [MAILPOET-1620] 2018-11-08 16:54:36 +03:00
9f46300d7d Merge pull request #1604 from mailpoet/add-space-to-listings
Add space to listings [MAILPOET-1612]
2018-11-08 07:27:37 -05:00
55e7c97af0 Merge pull request #1601 from mailpoet/fix-warning
Fix react warning when updating unmounted component [MAILPOET-1597]
2018-11-08 06:36:57 -05:00
c4536f3591 Merge pull request #1573 from mailpoet/unsub-settings
Preview Default Unsub page from Settings [MQ-101]
2018-11-08 06:02:08 -05:00
wxa
e4d4b575bd Merge pull request #1569 from mailpoet/settings-page-1
Settings page acceptance test checks [MQ-78][MQ-79][MQ-80][MQ-81]
2018-11-07 19:25:10 +03:00
wxa
2771c8c33e Merge pull request #1572 from mailpoet/pages-shortcodes-tests
Preview default subscription management page and change default subsc…
2018-11-07 19:20:56 +03:00
wxa
79dbaa119d Merge pull request #1568 from mailpoet/js-accpetance-test-checks
Add js error checks to some accpetance tests [MQ-100]
2018-11-07 19:14:45 +03:00
05d054dfad Make menu icon retina friendly
[MAILPOET-1559]
2018-11-07 16:42:02 +01:00
4bbde3435d Fix bottom margin [MAILPOET-1569] 2018-11-07 15:32:06 +01:00
74d711fa73 put validation messages on new line 2018-11-07 12:12:31 +01:00
464c27849b Add space to listings
[MAILPOET-1612]
2018-11-07 10:47:56 +01:00
wxa
2f202f6b3d Don't block new post notifications by one paused during sending [MAILPOET-1609] 2018-11-06 19:14:54 +03:00
186b4c9a36 Fix react warning when updating unmounted component
[MAILPOET-1597]
2018-11-06 15:35:23 +01:00
4b94392362 Merge pull request #1600 from mailpoet/release-3-13
Release 3.13.0
2018-11-06 07:28:24 -05:00
cca2a1923d fix code typos and style 2018-11-06 12:38:39 +01:00
76fe4dba59 Release 3.13.0 2018-11-06 11:38:06 +01:00
fef557bb94 Merge pull request #1598 from mailpoet/mozart-build-fix
Update composer.json for mozart dependencies to work with PHP5
2018-11-06 05:36:15 -05:00
5c5069643e Merge pull request #1595 from mailpoet/php56-warning
Update PHP 5.6 warning [MAILPOET-1610]
2018-11-06 05:19:33 -05:00
6e391559fe remove unused CSS 2018-11-06 11:01:55 +01:00
62e47dcf12 make small improvements and additional specs details. 2018-11-06 11:01:55 +01:00
85e1467936 css fine tuning2 2018-11-06 10:59:20 +01:00
8d816337f7 css fine tuning 2018-11-06 10:59:20 +01:00
8209facb5c adding custom fonts 2018-11-06 10:59:20 +01:00
19d5c44588 Update composer.json for mozart dependencies to work with PHP5 2018-11-06 10:14:34 +01:00
afc04e44af Merge pull request #1592 from mailpoet/react-upgrade
Upgrade React 15.4 to 15.5 [MAILPOET-1611]
2018-11-05 14:51:32 -05:00
80d6cd0187 Merge pull request #1591 from mailpoet/update-post-notification-templates
Update post notification templates [MAILPOET-1558]
2018-11-05 14:07:50 -05:00
b1f9a8a84f Merge pull request #1593 from mailpoet/fix-flaky-segments-test
Fix flaky segments test [MAILPOET-1578]
2018-11-05 13:54:36 -05:00
5635014285 Merge pull request #1582 from mailpoet/symfony-di
Add symfony dependency injection container [MAILPOET-1605]
2018-11-05 13:50:05 -05:00
91e66476d3 added data-automation-ids, fixed typo. [MQ-83][MQ-84] 2018-11-05 12:20:52 -05:00
33b4ed324e Codesniffer. [MQ-83][MQ-84] 2018-11-05 12:11:10 -05:00
a90672652e Preview default subscription management page and change default subscription management page. [MQ-83][MQ-84] 2018-11-05 12:11:10 -05:00
cc49c36466 data-automation-id additions, extra assertion. [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 11:07:47 -05:00
0a82c82387 fixing a typo I thought was fixed in the last commit. [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 10:34:13 -05:00
d987341094 fixes [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 10:16:06 -05:00
25f70df2ad fixes [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 10:16:06 -05:00
42859d94a3 Settings page acceptance test checks, CodeSniffer fixes [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 10:16:06 -05:00
4abaebf96d Settings page acceptance test checks [MQ-78][MQ-79][MQ-80][MQ-81] 2018-11-05 10:16:06 -05:00
cada06360b Fix templates data to use proper images and social icons URLs
[MAILPOET-1558]
2018-11-05 15:42:10 +01:00
fe6519d503 Update template screenshots
[MAILPOET-1558]
2018-11-05 15:33:49 +01:00
5a4cebf0d7 Update template screenshots
[MAILPOET-1558]
2018-11-05 15:33:48 +01:00
cb8d532d64 Fix ALC rendering of posts to have a padded title
[MAILPOET-1558]
2018-11-05 15:33:48 +01:00
7f31f0888e Update template data
[MAILPOET-1558]
2018-11-05 15:33:48 +01:00
b54d31c797 added data-automation-id for robustness [MQ-100] 2018-11-05 08:38:48 -05:00
f3a597ef90 Preview Default Unsub page from Settings [MQ-101] 2018-11-05 07:52:18 -05:00
d1fbe3dae1 Add additional js checks for more robust tests [MQ-100] 2018-11-05 06:04:25 -05:00
f30e25964c Added space to please CodeSniffer gods [MQ-100] 2018-11-05 06:04:25 -05:00
908f2f9f60 Add js error checks to some accpetance tests [MQ-100] 2018-11-05 06:04:25 -05:00
wxa
4a781849db Display outdated PHP version warning for 5.6 [MAILPOET-1610] 2018-11-05 11:18:15 +03:00
c4094efd91 Update PHP 5.6 warning
[MAILPOET-1610]
2018-11-05 11:15:59 +03:00
c13974ad82 Merge pull request #1590 from mailpoet/default-confirmation-email-update
Change default signup confirmation email [MAILPOET-1563]
2018-11-02 13:40:48 -04:00
787cccb33d Merge pull request #1588 from mailpoet/php55-end-of-support
End support for PHP 5.5 [MAILPOET-1317]
2018-11-02 12:03:36 -04:00
7e13bbcc57 Merge pull request #1596 from mailpoet/new-screenshots
Update screenshots [MAILPOET-1577]
2018-11-02 09:35:35 -04:00
wxa
741cfb1775 Add the sixth screenshot [MAILPOET-1577] 2018-11-02 13:31:57 +03:00
797491ddec Update screenshots
[MAILPOET-1577]
2018-11-01 15:13:33 +01:00
cfcba021b1 Merge WP_ROOT and WP_TEST_PATH environment vars
[MAILPOET-1605]
2018-11-01 14:46:38 +01:00
35f59cdcc0 Fix flaky segments test
This is hard to debug, but the id was NULL in the previous implementation
and that sometimes returned an object instead of expected
Now the id is always a number and that will hopefully fix the flakyness.

[MAILPOET-1578]
2018-11-01 09:58:44 +01:00
9f60cf1554 Fix listings breaking due to doubly-prefixed forward slash character 2018-11-01 03:25:58 +02:00
21f03bbcb7 Re-enable auto-refresh, search and groups in listings 2018-11-01 02:41:10 +02:00
ee37f39980 Upgrade React-DOM to 15.5 to avoid using deprecated React.PropTypes 2018-11-01 02:17:59 +02:00
883023e581 Fix remaining validation related prop type 2018-11-01 02:15:01 +02:00
ca3e309104 Swap React.PropTypes to using standalone prop-types package 2018-11-01 02:12:16 +02:00
d46f9203ee Fix newsletter scheduling related required proptype issues 2018-11-01 02:04:18 +02:00
7c19aecaa0 Fix subscribers related eslint violations 2018-11-01 01:56:33 +02:00
050417ec83 Fix segment related eslint violations 2018-11-01 01:56:21 +02:00
5b077b3911 Fix newsletter related broken eslint rules 2018-11-01 01:49:35 +02:00
70743b98b6 Fix listing related eslint PropTypes violations 2018-11-01 00:39:24 +02:00
103714a035 Fix forms related eslint PropTypes violations 2018-10-31 22:34:03 +02:00
0ae162b334 Fix form eslint PropTypes rules 2018-10-31 22:29:22 +02:00
4f97bb45e1 Fix PropType eslint warnings for form fields 2018-10-31 22:04:11 +02:00
d4fa041ba8 Convert React.createClass to ES6 classes and createReactClass calls 2018-10-31 21:18:44 +02:00
eb1acc8145 Update to React 15.5 in preparation for code adjustments 2018-10-31 19:48:21 +02:00
6479a6cc10 Update readme.md file
[MAILPOET-1605]
2018-10-31 17:49:32 +01:00
9eb586b8e6 Add ./do container:dump command and run it within build
[MAILPOET-1605]
2018-10-31 17:49:15 +01:00
f39c0c58c2 Add dumpContainer to container factory
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
0c155ffe7c Add processing of mozart libraries into build process
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
ad77fa547a Remove services.yaml and define services directly in PHP
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
7747c30028 Install mozart dependecies from special config
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
ea9be3e078 Remove tests folders for symfony packages
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
fff8176a49 Refactor lib/Router to use with container for endpoints
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
71ad9f50cb Add container factory and initialize container in Initializer
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
8eff3dc3c6 Add symfony/dependency-injection dependency
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
49ff4880f8 Switch mozart to custom repository to enable sub-deps processing
[MAILPOET-1605]
2018-10-31 17:26:47 +01:00
wxa
4d93aff2cb Change default signup confirmation email [MAILPOET-1563] 2018-10-31 17:06:15 +03:00
wxa
7b859660b7 End support for PHP 5.5 [MAILPOET-1317] 2018-10-31 15:21:34 +03:00
3e22223d0c Merge pull request #1587 from mailpoet/release-3.12.1
releasing 3.12.1
2018-10-31 05:45:00 -04:00
384c9fb17b releasing 3.12.1 2018-10-30 17:45:13 +01:00
79acc0e4df Merge pull request #1584 from mailpoet/new-column-layout
New column layout [MAILPOET-1568]
2018-10-30 09:29:41 -04:00
52a29ff449 Merge pull request #1586 from mailpoet/nps-poll-for-old-users
NPS poll for old users [MAILPOET-1599]
2018-10-30 06:29:21 -04:00
c4db1df528 Refactor test readability
[MAILPOET-1568]
2018-10-30 07:49:29 +01:00
1fe7bedf20 Fix rendered column widths
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
f5be4e47e8 Rename misleading variable name
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
2ae39c9255 Make renderer render different widths columns
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
6028027d47 Use proper backbone set and get
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
283452f47e Update default value
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
7be91476f0 Set width to the new column layouts
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
b528587b1f Add classes to container with the new columns
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
056b971f7b Add new column layouts to editor
[MAILPOET-1568]
2018-10-30 07:49:28 +01:00
fcfd6f1f09 Merge pull request #1567 from mailpoet/split_unit_tests
Split unit tests into integration and pure unit tests
2018-10-29 11:21:32 -04:00
wxa
90186f2af5 Update README: split unit and integration tests 2018-10-29 17:57:47 +03:00
wxa
c9ef3cfb95 Execute unit tests earlier in the workflow on CircleCI 2018-10-29 17:57:47 +03:00
wxa
b6f40a9b52 Remove AspectMock, private method call trick and WP globals fixes from unit tests 2018-10-29 17:57:47 +03:00
wxa
1936524cec Move a test from integration to unit as an example 2018-10-29 17:57:47 +03:00
wxa
8b2c5116f4 Fix AspectMock config 2018-10-29 17:57:47 +03:00
wxa
9734759ba5 Run both integration and unit tests on CircleCI 2018-10-29 17:57:47 +03:00
wxa
c76b4c3c99 Unify Codeception configs 2018-10-29 17:57:47 +03:00
wxa
9092aa3029 Make separate setups for integration and unit tests 2018-10-29 17:57:47 +03:00
wxa
87e515b89d Move current unit tests to integration tests 2018-10-29 17:57:47 +03:00
05e5ca3f43 Merge pull request #1585 from mailpoet/fix-jetpack-cdn-css
Permit CSS assets to be loaded from WP.com CDN on MP3 pages [MAILPOET-1584]
2018-10-29 10:55:48 -04:00
5fed328826 fix errors 2018-10-28 16:06:03 +01:00
7538a08678 NPS poll for old users 2018-10-28 15:57:59 +01:00
wxa
82d6a4b096 Permit CSS assets to be loaded from WP.com CDN on MP3 pages [MAILPOET-1584] 2018-10-26 14:42:46 +03:00
b21ef30202 Fix failing Scheduler test
[MAILPOET-1602]
2018-10-25 08:43:17 +02:00
653f31e997 Merge pull request #1580 from mailpoet/use-older-chrome
Use older chrome in acceptance tests
2018-10-24 12:04:19 +02:00
149c794905 Freeze mailhog to avoid problems in the future 2018-10-24 11:30:39 +02:00
cbf48327d9 Use older chrome for acceptance tests 2018-10-24 11:11:48 +02:00
c83ed66160 Merge pull request #1579 from mailpoet/release-3.12.0
Release 3.12.0
2018-10-23 10:21:55 -04:00
wxa
042fe48669 Release 3.12.0 2018-10-23 14:04:59 +03:00
9aba09d42e Merge pull request #1578 from mailpoet/readme-update
Update readme.txt [MAILPOET-1545]
2018-10-23 05:37:59 -04:00
wxa
25a8519583 Update readme.txt [MAILPOET-1545] 2018-10-23 11:37:52 +03:00
9fc442ae8e Merge pull request #1563 from mailpoet/fix-coverage-php72
Fix code coverage on PHPUnit 5.7 and PHP 7.2 [MAILPOET-1596]
2018-10-22 15:04:49 -04:00
1e7542848a Merge pull request #1556 from mailpoet/arrays-to-objects
Replace entity arrays with objects [MAILPOET-1495]
2018-10-22 14:59:12 -04:00
78821a69ab Merge pull request #1565 from mailpoet/logging-fix
Remove newsletter logging where is no newsletter [MAILPOET-1595]
2018-10-22 14:57:11 -04:00
4ae459838a Merge pull request #1558 from mailpoet/fix-flaky-scheduler-test
Try to fix flaky scheduler tests [MAILPOET-1579]
2018-10-22 14:54:31 -04:00
aa83883cfa Merge pull request #1575 from mailpoet/email-notifications-fix
New subscriber emails notifications fixes [MAILPOET-1952][MAILPOET-1600]
2018-10-22 13:23:11 -04:00
6d9f0767af Merge pull request #1576 from mailpoet/fix-bulk-confirmation
Fix bulk re-sending confirmation emails for subscribers [MAILPOET-1598]
2018-10-22 13:00:24 -04:00
c03feeae3f Merge pull request #1564 from mailpoet/fix-satismeter-date-format
Fix date format for satismeter [MAILPOET-1575]
2018-10-22 12:04:54 -04:00
8effcf2c59 Merge pull request #1566 from mailpoet/fix-discount-announcement
Fix discount announcement [MAILPOET-1590]
2018-10-22 10:52:39 -04:00
f81c5617ed Merge pull request #1574 from mailpoet/remove-www-from-sender
Remove www from confirmation email sender [MAILPOET-1593]
2018-10-22 10:11:07 -04:00
e6c4c02484 Merge pull request #1560 from mailpoet/mpapi_welcome_email
Schedule welcome email only if subscriber is confirmed in MPAPI [MAILPOET-1589]
2018-10-22 06:31:55 -04:00
ff332e78e9 Always do all validations on submit in settings basic tab
[MAILPOET-1592]
2018-10-22 11:44:46 +02:00
f211ffce3b Update a discount notification display until date in a new subscriber notification
[MAILPOET-1600]
2018-10-22 11:19:01 +02:00
ba127990e3 Fix displaying of discount notification in a new subscriber email
The problem was that we use twig 1.x and it doesn't support date function same way as twig 2.0.
[MAILPOET-1592]
2018-10-22 11:12:01 +02:00
03ca022596 Fix bulk re-sending confirmation emails for subscribers
[MAILPOET-1598]
2018-10-22 10:08:35 +02:00
ee00464cc5 Remove www from confirmation email sender
[MAILPOET-1593]
2018-10-22 09:17:10 +02:00
aaa1c659b8 Update button styles
[MAILPOET-1590]
2018-10-18 15:48:53 +02:00
b9ffc0e280 Update text on premium page
[MAILPOET-1590]
2018-10-18 15:09:09 +02:00
a943589de9 Update text on premium page
[MAILPOET-1590]
2018-10-18 15:08:51 +02:00
c0e252f42a Update the discount amount
[MAILPOET-1590]
2018-10-18 09:36:41 +02:00
980574eb6d Update button style
[MAILPOET-1590]
2018-10-18 09:34:56 +02:00
72ea5fb84e Display the announcement on the dashboard only
[MAILPOET-1590]
2018-10-18 09:31:22 +02:00
cc303c0cd3 Remove opening in new tab
[MAILPOET-1590]
2018-10-18 09:16:25 +02:00
d11c9c5296 Remove hanging period
[MAILPOET-1590]
2018-10-18 09:14:54 +02:00
fd893cec0e Remove newsletter logging where is no newsletter
[MAILPOET-1595]
2018-10-18 09:07:32 +02:00
debe9ea1ba Fix date format for satismeter
[MAILPOET-1575]
2018-10-18 08:44:24 +02:00
wxa
3029e6102f Fix code coverage on PHPUnit 5.7 and PHP 7.2 [MAILPOET-1596] 2018-10-17 18:49:16 +03:00
9c8c89149b Releasing 3.11.5 2018-10-17 17:22:56 +02:00
edb3f25838 Merge pull request #1561 from mailpoet/js-warning-fix
Js warning fix [MAILPOET-1591]
2018-10-17 09:53:25 -04:00
2f730c280c Fix error always shown in settings
[MAILPOET-1592]
2018-10-17 14:15:14 +02:00
1aa0a86d66 Fix rendering announcement everywhere
[MAILPOET-1591]
2018-10-17 13:48:20 +02:00
wxa
6b6f488f1e Schedule welcome email only if subscriber is confirmed in MPAPI [MAILPOET-1589] 2018-10-16 19:03:44 +03:00
c1e8815134 Releasing 3.11.4 2018-10-16 13:38:21 +02:00
64e5742700 Merge pull request #1553 from mailpoet/fix-wp-com
Permit JS assets to be loaded from WP.com CDN on MP3 pages [MAILPOET-1584]
2018-10-16 05:57:37 -04:00
ce6d45e557 Merge pull request #1557 from mailpoet/announcements
In app announcement of discounts [MAILPOET-1552]
2018-10-15 14:49:58 -04:00
91a1ca69ed fixing typos and failing tests 2018-10-15 20:07:05 +02:00
549e2e7e86 Merge pull request #1555 from mailpoet/gdpr-tip
Form Editor GDPR tip [MAILPOET-1476]
2018-10-15 13:24:52 -04:00
ce6711d69e adding discount to premium page 2018-10-15 17:36:05 +02:00
0639b1ad1f adding notice to dashboard 2018-10-15 17:36:05 +02:00
ad37c6b9c5 Merge pull request #1552 from mailpoet/email-notifications
Email notifications [MAILPOET-1522]
2018-10-15 11:16:23 -04:00
605f8f1ce1 Try to fix flaky scheduler tests
[MAILPOET-1579]
2018-10-15 15:50:06 +02:00
1ffd5741be Merge pull request #1548 from mailpoet/php-deps-update
PHP Dependencies update [MAILPOET-1542]
2018-10-15 08:01:03 -04:00
a1d606e533 Add cleanup
[MAILPOET-1522]
2018-10-15 13:52:04 +02:00
a6ab757d22 Show warning if settings is not consistent
[MAILPOET-1522]
2018-10-15 13:26:52 +02:00
654dd1e8d0 Rename Send classes
[MAILPOET-1522]
2018-10-15 13:06:19 +02:00
40c19cd5d8 Fix parameters
[MAILPOET-1522]
2018-10-15 12:56:15 +02:00
3cccce3d86 Merge pull request #1549 from mailpoet/update-readme
Updating README and CONTRIBUTING [MAILPOET-1541]
2018-10-15 05:49:59 -04:00
57203a6917 Split text into two lines
[MAILPOET-1522]
2018-10-15 11:30:55 +02:00
d01805a911 Add default notification email
[MAILPOET-1522]
2018-10-15 11:18:53 +02:00
4774ab09f3 Merge pull request #1554 from mailpoet/tests_aspectmock_fix
Fix AspectMock to work when the Premium plugin is active [MAILPOET-1585]
2018-10-14 17:52:58 -04:00
5dad1fa545 Add GDPR guide tip into form editor
[MAILPOET-1476]
2018-10-12 13:39:55 +02:00
c3481dd4b7 Refactor subscription links to work with subscriber entity
[MAILPOET-1495]
2018-10-12 11:33:15 +02:00
b5338979b1 Refactor segment usage as object in SubscribersListings
[MAILPOET-1495]
2018-10-12 11:26:33 +02:00
436406f800 Refactor simple array usages in models
[MAILPOET-1495]
2018-10-12 11:26:33 +02:00
112f7b21d5 Update newsletterTemplate endpoint to use entity objects
[MAILPOET-1495]
2018-10-12 11:26:33 +02:00
b45c47a9e7 Update SubscriberFinder and Scheduler to use object entities
[MAILPOET-1495]
2018-10-12 11:26:23 +02:00
wxa
cb3952c9fb Fix unit tests failing in MySQL strict mode [MAILPOET-1585] 2018-10-11 23:09:37 +03:00
wxa
b597cb39f1 Fix AspectMock to work when the Premium plugin is active [MAILPOET-1585] 2018-10-11 23:08:40 +03:00
3102f114bd Add feature announcement
[MAILPOET-1522]
2018-10-11 19:37:45 +02:00
bdf244536d Permit JS assets to be loaded from WP.com CDN on MP3 pages [MAILPOET-1584]
The CDN is provided by JetPack plugin in their "Asset CDN" beta feature
2018-10-11 17:21:06 +03:00
ca3b6412ee Fix exception class in mailpoet custom sniff
[MAILPOET-1542]
2018-10-11 15:23:06 +02:00
469dc15e4d Downgrade aspect mock and goap framework to previous version
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
253be30153 Update kint-php/kint
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
d7fb5450f4 Update sub-dependencies
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
280bc06c03 Update update sensiolabs/security-checker symfony/polyfill-mbstring
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
c1a505c425 Use new php7.1 container in circle
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
614d9f7e56 Update lucatume/wp-browser
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
7c9b2be760 Update PHPCompatibility standard and PHP_CodeSniffer
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
0f2bc75248 Update nesbot/carbon and league/filesystem
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
f0df936dbc Update consolidation sub dependencies
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
4b675a41d6 Update aspect mock and goaop framework
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
0e7cc668e2 Update css-tidy
[MAILPOET-1542]
2018-10-11 14:02:13 +02:00
70debcc828 Refactor confirmation email sending
Aspect mock stopped working for me so I had to create a separate service
for sending confirmation emails.

[MAILPOET-1522]
2018-10-11 13:39:39 +02:00
4249c7a2cb Send an email notification on new subscriber
[MAILPOET-1522]
2018-10-11 10:23:06 +02:00
fbe2f72706 Add settings for new subscriber email notifications
[MAILPOET-1505]
2018-10-11 10:23:06 +02:00
6b9c252649 Remove unused methods in SubscribersFinder
[MAILPOET-1495]
2018-10-11 09:19:23 +02:00
be75e0d3ee Merge pull request #1545 from mailpoet/welcome-email-data-factory
Welcome email data factory [MAILPOET-1550]
2018-10-10 14:05:30 -04:00
8e12bbf97f Merge pull request #1543 from mailpoet/subscriber-data-factory
Create subscriber data factory [MAILPOET-1549]
2018-10-10 14:00:47 -04:00
1936a3bcdd Merge pull request #1551 from mailpoet/trim-settings
Trim whitespaces from intputs in settings page [MAILPOET-1553]
2018-10-10 09:44:35 -04:00
7eb37119ed removing files structure section 2018-10-10 15:43:53 +02:00
aadca62a32 Merge pull request #1542 from mailpoet/migration-page-fix
Fix migration page for all languages [MAILPOET-1543]
2018-10-10 08:15:00 -04:00
11401e2dab adding tools 2018-10-10 14:00:42 +02:00
4cae2a7d2d adding comments to .env.sample 2018-10-10 11:49:06 +02:00
9866f4c707 adding i18n description 2018-10-10 11:25:38 +02:00
b632d74e5f triming settings values 2018-10-09 19:54:06 +02:00
c3a97b7139 Merge pull request #1547 from mailpoet/release-3.11.3
Prepare 3.11.3 release
2018-10-09 18:30:11 +03:00
a772aceb39 Updating README and CONTRIBUTING 2018-10-09 17:29:08 +02:00
d59ace50f5 Update stable tag to the latest version 2018-10-09 17:52:55 +03:00
ff6f686333 Prepare 3.11.3 release 2018-10-09 17:51:22 +03:00
2ef8512fa7 Include mailpoet-cron.php in the zip during build 2018-10-09 16:46:34 +02:00
cabea5dadb Merge pull request #1544 from mailpoet/release-3.11.2
Prepare for 3.11.2 release
2018-10-09 14:56:01 +03:00
b80505a17c Update readme.txt with updated changelog 2018-10-09 14:29:40 +03:00
e0b676fac4 Uncomment comment
[MAILPOET-1550]
2018-10-09 12:09:31 +02:00
6566f515f5 Copy migrations to dump to prevent test failures
[MAILPOET-1550]
2018-10-09 12:05:31 +02:00
2836f72435 Allow test to run in paralel
[MAILPOET-1550]
2018-10-09 12:05:07 +02:00
0b644e7c8f Add data factory for welcome emails
[MAILPOET-1550]
2018-10-09 12:04:32 +02:00
df4d3bacab Restore original version of package-json.lock using Node 9 2018-10-09 11:27:55 +03:00
6dc0acb63a Add release information and update package-lock.json 2018-10-09 11:03:11 +03:00
a346492d46 Refactor post notification factory
[MAILPOET-1550]
2018-10-09 09:24:29 +02:00
b6513262b0 Create subscriber data factory
[MAILPOET-1549]
2018-10-08 15:59:07 +02:00
868e07ab86 Fix migration page for all languages
[MAILPOET-1543]
2018-10-08 13:25:00 +02:00
9ec6f52098 Merge pull request #1526 from mailpoet/linux-cron
Linux cron [MAILPOET-1538]
2018-10-08 06:41:57 -04:00
222d6b4eac Merge pull request #1514 from mailpoet/delete-subscriber-tasks
delete related scheduled_task_subscribers records [MAILPOET-1532]
2018-10-05 13:32:41 -04:00
0683dc9817 Merge pull request #1537 from mailpoet/admin-fatal-error
Fix error at editor page for admin who is not a subscriber [MAILPOET-1556]
2018-10-05 11:59:44 -04:00
45e6ddd9cb Merge pull request #1538 from mailpoet/better-drag-and-drop-modal
Better drag and drop modal title [MAILPOET-1555]
2018-10-05 11:23:41 -04:00
c556ec64fc Merge pull request #1535 from mailpoet/add-missing-translation
Add missing translation [MAILPOET-1554]
2018-10-05 10:24:30 -04:00
5bc9573ac6 Merge pull request #1528 from mailpoet/post-notifications-log
Add post notification logging [MAILPOET-1536]
2018-10-05 10:04:04 -04:00
a3d7afd1ca Merge pull request #1534 from mailpoet/fix-day-picker
Make date picker wider [MAILPOET-1531]
2018-10-05 09:32:56 -04:00
8c44ef561c adding transaction 2018-10-04 14:52:49 +02:00
c51a3f208c delete related scheduled_task_subscribers records 2018-10-04 14:52:49 +02:00
5524805ec3 pulled bad tests 2018-10-04 14:06:06 +02:00
816ec7e1d7 code sniffer fixes for 4 files 2018-10-04 14:06:06 +02:00
d74283ecad better drag and drop modal title 2018-10-04 10:36:19 +02:00
e810840445 Fix error at editor page for admin who is not a subscriber
[MAILPOET-1556]
2018-10-04 09:42:27 +02:00
b2e2087cfc Split cron daemon into 2 classes
[MAILPOET-1538]
2018-10-04 08:27:31 +02:00
69d920376f requested changes [MQ-70][MQ-71][MQ-72][MQ-73][MQ-74][MQ-77] 2018-10-04 08:01:07 +02:00
22efacd2d7 requested changes, added coverage for import/export [MQ-76][MQ-75][MQ-74][MQ-73][MQ-72][MQ-71][MQ-70][MQ-77] 2018-10-04 08:01:07 +02:00
25c72c6ce7 Edit, Add, Delete Subscribers [MQ-71][MQ-72][MQ-73][MQ-74][MQ-77] 2018-10-04 08:01:07 +02:00
e94d85fd47 Edit, Add, Delete Subscribers [MQ-71][MQ-72][MQ-73][MQ-74][MQ-77] 2018-10-04 08:01:07 +02:00
c6b379c840 Edit, Add, Delete Subscribers [MQ-71][MQ-72][MQ-73][MQ-74][MQ-77] 2018-10-04 08:01:07 +02:00
71bf3bbe19 Merge pull request #1531 from mailpoet/data-factory-lists
Data factory lists [MAILPOET-1546]
2018-10-03 15:01:26 -04:00
6e60056061 Merge pull request #1529 from mailpoet/cookies-accptance-tests
Add cookies option [MAILPOET-1547]
2018-10-03 14:59:58 -04:00
96edbc159f Merge pull request #1523 from mailpoet/create-welcome-email
Create Welcome Email [MQ-61]
2018-10-03 13:03:07 -04:00
c3c0765b78 Merge pull request #1524 from mailpoet/edit-delete-duplicate-welcome-email
Manage welcome emails by saving as draft, editing, deleting, duplicat…
2018-10-03 12:52:45 -04:00
b614997150 add missing translation 2018-10-03 16:31:51 +02:00
f81323ad52 Rename Daemon to DaemonHttpRunner
[MAILPOET-1538]
2018-10-03 10:28:26 +02:00
cfc0d79bed requested changes [MQ-61] 2018-10-02 22:33:22 -04:00
c6adfc6a5c requested changes, cleanup [MQ-[MQ-62][MQ-63][MQ-64][MQ-66][MQ-67][MQ-68] 2018-10-02 22:01:12 -04:00
53149c291c Merge pull request #1521 from mailpoet/receive-email-types
Confirm standard newsletter is sent to a subscriber [MQ-60]
2018-10-02 20:11:10 -04:00
e8f0a0067d Exit with error on failure
[MAILPOET-1538]
2018-10-02 14:53:36 +02:00
cc4ff09aea Small acceptance tests improvements 2018-10-02 13:54:06 +02:00
77dfd28479 Make date picker wider
[MAILPOET-1531]
2018-10-02 12:10:39 +02:00
cd283c11fe Use data factory
[MAILPOET-1546]
2018-10-02 09:33:06 +02:00
57baff7b1d Add segments to form factory
[MAILPOET-1546]
2018-10-02 09:22:31 +02:00
fd72756622 Add Segment data factory
[MAILPOET-1546]
2018-10-02 08:56:43 +02:00
9b3abea2e5 Add more checks to linux cron
[MAILPOET-1538]
2018-10-02 08:25:56 +02:00
42c557a981 Add cookies option
[MAILPOET-1547]
2018-10-01 15:45:26 +02:00
e4db455a47 Add post notification logging
[MAILPOET-1536]
2018-10-01 15:02:12 +02:00
5d1f3153cd Add Linux Cron Script
[MAILPOET-1538]
2018-09-27 16:51:44 +02:00
fcd8509cef Add linux cron option to settings
[MAILPOET-1538]
2018-09-27 15:08:56 +02:00
ad731fc5ed Manage welcome emails by saving as draft, editing, deleting, duplicating, searching, and saving as template [MQ-62][MQ-63][MQ-64][MQ-66][MQ-67][MQ-68] 2018-09-27 01:32:32 -04:00
4257ff6313 Create Welcome Email [MQ-61] 2018-09-26 19:59:25 -04:00
f9a3875de1 Create Welcome Email [MQ-61] 2018-09-26 19:39:54 -04:00
b17d217669 Confirm standard newsletter is sent to a subscriber [MQ-60] 2018-09-26 14:37:01 -04:00
556e4a0ce0 Confirm standard newsletter is sent to a subscriber [MQ-60] 2018-09-26 13:43:39 -04:00
a72d84b940 Confirm standard newsletter is sent to a subscriber [MQ-60] 2018-09-26 13:21:05 -04:00
049074e793 Confirm standard newsletter is sent to a subscriber [MQ-60] 2018-09-26 13:05:30 -04:00
456 changed files with 10664 additions and 5952 deletions

View File

@ -3,7 +3,7 @@ jobs:
build_and_code_qa: build_and_code_qa:
working_directory: /home/circleci/mailpoet working_directory: /home/circleci/mailpoet
docker: docker:
- image: mailpoet/wordpress:7.1_20180417.1 - image: mailpoet/wordpress:7.1_20181009.1
environment: environment:
TZ: /usr/share/zoneinfo/Etc/UTC TZ: /usr/share/zoneinfo/Etc/UTC
steps: steps:
@ -42,7 +42,35 @@ jobs:
root: /home/circleci/mailpoet root: /home/circleci/mailpoet
paths: paths:
- . - .
php5_and_js: php5_unit:
working_directory: /home/circleci/mailpoet
docker:
- image: mailpoet/wordpress:5.6.30_20180417.1
- image: circleci/mysql:5.7
environment:
TZ: /usr/share/zoneinfo/Etc/UTC
steps:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up virtual host"
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run:
name: "Set up test environment"
command: source ./.circleci/setup.bash && setup php5
- run:
name: "PHP Unit tests"
command: |
WP_ROOT="/home/circleci/mailpoet/wordpress" ./do t:u --xml
- store_test_results:
path: tests/_output
- store_artifacts:
path: tests/_output
destination: codeception
- store_artifacts:
path: /tmp/fake-mailer/
destination: fake-mailer
php5_integration_and_js:
working_directory: /home/circleci/mailpoet working_directory: /home/circleci/mailpoet
docker: docker:
- image: mailpoet/wordpress:5.6.30_20180417.1 - image: mailpoet/wordpress:5.6.30_20180417.1
@ -67,9 +95,9 @@ jobs:
mkdir test-results/mocha mkdir test-results/mocha
./do t:j test-results/mocha/junit.xml ./do t:j test-results/mocha/junit.xml
- run: - run:
name: "PHP Unit tests" name: "PHP Integration tests"
command: | command: |
WP_TEST_PATH="/home/circleci/mailpoet/wordpress" ./do t:u --xml WP_ROOT="/home/circleci/mailpoet/wordpress" ./do t:i --xml
- store_test_results: - store_test_results:
path: test-results/mocha path: test-results/mocha
- store_artifacts: - store_artifacts:
@ -131,10 +159,10 @@ jobs:
path: tests/_output path: tests/_output
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
php7: php7_unit:
working_directory: /home/circleci/mailpoet working_directory: /home/circleci/mailpoet
docker: docker:
- image: mailpoet/wordpress:7.1_20180417.1 - image: mailpoet/wordpress:7.1_20181009.1
- image: circleci/mysql:5.7 - image: circleci/mysql:5.7
environment: environment:
TZ: /usr/share/zoneinfo/Etc/UTC TZ: /usr/share/zoneinfo/Etc/UTC
@ -162,10 +190,41 @@ jobs:
- store_artifacts: - store_artifacts:
path: /tmp/fake-mailer/ path: /tmp/fake-mailer/
destination: fake-mailer destination: fake-mailer
php7_multisite: php7_integration:
working_directory: /home/circleci/mailpoet working_directory: /home/circleci/mailpoet
docker: docker:
- image: mailpoet/wordpress:7.1_20180417.1 - image: mailpoet/wordpress:7.1_20181009.1
- image: circleci/mysql:5.7
environment:
TZ: /usr/share/zoneinfo/Etc/UTC
steps:
- attach_workspace:
at: /home/circleci/mailpoet
- run:
name: "Set up virtual host"
command: echo 127.0.0.1 mailpoet.loc | sudo tee -a /etc/hosts
- run:
name: "Prepare example.com for testing"
command: echo 127.0.0.1 example.com | sudo tee -a /etc/hosts
- run:
name: "Set up test environment"
command: source ./.circleci/setup.bash && setup php7
- run:
name: "PHP Integration tests"
command: |
./do t:i --xml
- store_test_results:
path: tests/_output
- store_artifacts:
path: tests/_output
destination: codeception
- store_artifacts:
path: /tmp/fake-mailer/
destination: fake-mailer
php7_integration_multisite:
working_directory: /home/circleci/mailpoet
docker:
- image: mailpoet/wordpress:7.1_20181009.1
- image: circleci/mysql:5.7 - image: circleci/mysql:5.7
environment: environment:
TZ: /usr/share/zoneinfo/Etc/UTC TZ: /usr/share/zoneinfo/Etc/UTC
@ -182,9 +241,9 @@ jobs:
name: "Set up test environment" name: "Set up test environment"
command: source ./.circleci/setup.bash && setup php7_multisite command: source ./.circleci/setup.bash && setup php7_multisite
- run: - run:
name: "PHP Unit tests" name: "PHP Integration tests"
command: | command: |
./do t:multisite-unit --xml ./do t:multisite-integration --xml
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
- store_artifacts: - store_artifacts:
@ -198,20 +257,24 @@ workflows:
build_and_test: build_and_test:
jobs: jobs:
- build_and_code_qa - build_and_code_qa
- php7: - php5_unit:
requires: requires:
- build_and_code_qa - build_and_code_qa
- php5_and_js: - php7_unit:
requires: requires:
- build_and_code_qa - build_and_code_qa
- php5_integration_and_js:
requires:
- php5_unit
- php7_integration:
requires:
- php7_unit
- php7_integration_multisite:
requires:
- php7_unit
- acceptance_tests: - acceptance_tests:
requires: requires:
- build_and_code_qa - php5_unit
- php7_multisite:
requires:
- build_and_code_qa
- php7
- acceptance_tests_multisite: - acceptance_tests_multisite:
requires: requires:
- build_and_code_qa - php5_unit
- acceptance_tests

View File

@ -40,14 +40,14 @@ function setup {
# Add a second blog # Add a second blog
wp site create --slug=php7_multisite $wp_cli_wordpress_path $wp_cli_allow_root wp site create --slug=php7_multisite $wp_cli_wordpress_path $wp_cli_allow_root
echo "WP_TEST_MULTISITE_SLUG=php7_multisite" >> .env echo "WP_TEST_MULTISITE_SLUG=php7_multisite" >> .env
echo "WP_TEST_PATH_MULTISITE=/home/circleci/mailpoet/wordpress" >> .env echo "WP_ROOT_MULTISITE=/home/circleci/mailpoet/wordpress" >> .env
echo "HTTP_HOST=mailpoet.loc" >> .env echo "HTTP_HOST=mailpoet.loc" >> .env
# Add a third dummy blog # Add a third dummy blog
wp site create --slug=dummy_multisite $wp_cli_wordpress_path $wp_cli_allow_root wp site create --slug=dummy_multisite $wp_cli_wordpress_path $wp_cli_allow_root
else else
wp core install --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc --title="WordPress Single" $wp_cli_wordpress_path $wp_cli_allow_root wp core install --admin_name=admin --admin_password=admin --admin_email=admin@mailpoet.loc --url=http://mailpoet.loc --title="WordPress Single" $wp_cli_wordpress_path $wp_cli_allow_root
echo "WP_TEST_PATH=/home/circleci/mailpoet/wordpress" >> .env echo "WP_ROOT=/home/circleci/mailpoet/wordpress" >> .env
fi fi
# Softlink plugin to plugin path # Softlink plugin to plugin path

View File

@ -1,10 +1,17 @@
WP_TEST_PATH="/var/www/wordpress" # Required
WP_TEST_PATH_MULTISITE="/var/www/wordpress" WP_ROOT="/var/www/wordpress"
WP_TEST_ENABLE_NETWORK_TESTS="false"
WP_TEST_MAILER_ENABLE_SENDING="false"
# Optional: for multisite acceptance tests
WP_ROOT_MULTISITE="/var/www/wordpress"
WP_TEST_MULTISITE_SLUG="" WP_TEST_MULTISITE_SLUG=""
WP_TEST_ENABLE_NETWORK_TESTS="true" HTTP_HOST="" // URL of your site (used for multisite env and equals to DOMAIN_CURRENT_SITE from wp-config.php)
# Optional: for sending tests
# These are required if WP_TEST_MAILER_ENABLE_SENDING is "true"
WP_TEST_IMPORT_MAILCHIMP_API="" WP_TEST_IMPORT_MAILCHIMP_API=""
WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma) WP_TEST_IMPORT_MAILCHIMP_LISTS="" // (separated with comma)
WP_TEST_MAILER_ENABLE_SENDING="true"
WP_TEST_MAILER_AMAZON_ACCESS="" WP_TEST_MAILER_AMAZON_ACCESS=""
WP_TEST_MAILER_AMAZON_SECRET="" WP_TEST_MAILER_AMAZON_SECRET=""
WP_TEST_MAILER_AMAZON_REGION="" WP_TEST_MAILER_AMAZON_REGION=""
@ -13,7 +20,8 @@ WP_TEST_MAILER_SENDGRID_API=""
WP_TEST_MAILER_SMTP_HOST="" WP_TEST_MAILER_SMTP_HOST=""
WP_TEST_MAILER_SMTP_LOGIN="" WP_TEST_MAILER_SMTP_LOGIN=""
WP_TEST_MAILER_SMTP_PASSWORD="" WP_TEST_MAILER_SMTP_PASSWORD=""
# Optional: for plugin deployment
WP_SVN_USERNAME="" WP_SVN_USERNAME=""
WP_SVN_PASSWORD="" WP_SVN_PASSWORD=""
WP_TRANSIFEX_API_TOKEN="" WP_TRANSIFEX_API_TOKEN=""
HTTP_HOST="" // URL of your site (used for multisite env and equals to DOMAIN_CURRENT_SITE from wp-config.php)

View File

@ -4,6 +4,7 @@
"amd": true, "amd": true,
"browser": true "browser": true
}, },
"parser": "babel-eslint",
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 6,
"ecmaFeatures": { "ecmaFeatures": {

6
.gitignore vendored
View File

@ -23,4 +23,8 @@ lang
.mp_svn .mp_svn
/nbproject/ /nbproject/
tests/_data/acceptanceGenerated.sql tests/_data/acceptanceGenerated.sql
lib/Dependencies lib/Dependencies
lib/DI/CachedContainer.php
mozart/Dependencies
mozart/Classes
mozart/vendor

View File

@ -1,6 +1,6 @@
# Contributing # Contributing
## Code. ## PHP Code
- Two spaces indentation. - Two spaces indentation.
- CamelCase for classes. - CamelCase for classes.
- camelCase for methods. - camelCase for methods.
@ -10,27 +10,23 @@
- Require other classes with 'use' at the beginning of the class file. - Require other classes with 'use' at the beginning of the class file.
- Do not specify 'public' if method is public, it's implicit. - Do not specify 'public' if method is public, it's implicit.
- Always use guard clauses. - Always use guard clauses.
- Ensure compatibility with PHP 5.3 and newer versions. - Ensure compatibility with PHP 5.5 and newer versions.
- Cover your code in tests. - Cover your code in tests.
Recommendations: ## JS Code
- Max line length at 80 chars. - Javascript code should follow the [Airbnb style guide](https://github.com/airbnb/javascript).
- Keep classes under 100 LOC.
- Keep methods under 10 LOC.
- Pass no more than 4 parameters/hash keys into a method.
- Keep Pull Requests small, under 100 LOC changed.
## Git flow. ## Git flow
- Do not commit to master. - Do not commit to master.
- Open a short-living feature branch. - Open a short-living feature branch.
- Open a pull request. - Open a pull request.
- Add Jira issue reference in the title of the Pull Request. - Add Jira issue reference in the title of the Pull Request.
- Work on the pull request. - Work on the pull request.
- Wait for review and confirmation from another developer before merging to master. - Use the `./do qa` command to check your code style before pushing.
- Commit title no more than 80 chars, empty line after. - Use good commit messages as explained here https://chris.beams.io/posts/git-commit
- Commit description as long as you want, 80 chars wrap. - Wait for review from another developer.
## Issues creation. ## Issues creation
- Issues are managed on Jira. - Issues are managed on Jira.
- Discuss issues on public Slack chats, discuss code in pull requests. - Discuss issues on public Slack chats, discuss code in pull requests.
- Open a small Jira issue only when it has been discussed. - Open a small Jira issue only when it has been discussed.

View File

@ -1,11 +1,11 @@
FROM mailpoet/wordpress:5.6-cli_20180417.1 FROM mailpoet/wordpress:5.6-cli_20181009.1
ENV COMPOSER_ALLOW_SUPERUSER=1 ENV COMPOSER_ALLOW_SUPERUSER=1
RUN composer global require --optimize-autoloader "hirak/prestissimo" RUN composer global require --optimize-autoloader "hirak/prestissimo"
WORKDIR /wp-core/wp-content/plugins/mailpoet WORKDIR /wp-core/wp-content/plugins/mailpoet
ENV WP_TEST_PATH=/wp-core ENV WP_ROOT=/wp-core
ADD docker-entrypoint.sh / ADD docker-entrypoint.sh /

302
README.md
View File

@ -1,187 +1,156 @@
# MailPoet. # MailPoet
MailPoet done the right way. MailPoet done the right way.
# Install. # Contents
- Install system dependencies: - [Setup](#setup)
``` - [Frameworks and libraries](#frameworks-and-libraries)
php - [Workflow Commands](#workflow-commands)
nodejs - [Coding and Testing](#coding-and-testing)
wordpress
```
- Clone the repo in `wp-content/plugins`. # Setup
- Install composer. ## Requirements
```sh - PHP 5.6+
$ curl -sS https://getcomposer.org/installer | php - NodeJS
$ ./composer.phar install - WordPress
``` - Docker & Docker Compose
- Install dependencies. ## Installation
```sh ```bash
$ ./do install # go to WP plugins directory
``` $ cd path_to_wp_directory/wp-content/plugins
# clone this repository
- Update dependencies when needed. $ git clone https://github.com/mailpoet/mailpoet.git
```sh $ cd mailpoet
$ ./do update # create the .env file
```
- Copy .env.sample to .env.
```sh
$ cp .env.sample .env $ cp .env.sample .env
``` # change the values on .env file
# download composer
- Compile assets. $ curl -sS https://getcomposer.org/installer | php
```sh $ chmod +x ./composer.phar
# install PHP dependencies
$ ./composer.phar install
# install all dependencies (PHP and JS)
$ ./do install
# compile JS and CSS files
$ ./do compile:all $ ./do compile:all
``` ```
# Tests. # Frameworks and libraries
- Unit tests (using [verify](https://github.com/Codeception/Verify)): - [Paris ORM](https://github.com/j4mie/paris).
```sh - [Symfony/dependency-injection](https://github.com/symfony/dependency-injection) ([docs for 3.4](https://symfony.com/doc/3.4/components/dependency_injection.html)).
$ ./do test:unit - [Mozart](https://github.com/coenjacobs/mozart) for moving dependencies into MP namespace
- [Twig](https://twig.symfony.com/) and [Handlebars](https://handlebarsjs.com/) are used for templates rendering.
- [Monolog](https://seldaek.github.io/monolog/) is used for logging.
- [Robo](https://robo.li/) is used to write and run workflow commands.
- [Codeception](https://codeception.com/) is used to write unit and acceptance tests.
- [Docker](https://www.docker.com/), [Docker Compose](https://docs.docker.com/compose/) and [Selenium](https://www.seleniumhq.org/) to run acceptance tests.
- [React](https://reactjs.org/) is used to create most of UIs.
- [Marionette](https://marionettejs.com/) is used to build the newsletters editor.
- [Stylus](http://stylus-lang.com/) is used to write styles.
- [Mocha](https://mochajs.org/), [Chai](https://www.chaijs.com/) and [Sinon](https://sinonjs.org/) are used to write Javascript tests.
- [ESLint](https://eslint.org/) is used to lint JS files.
- [Webpack](https://webpack.js.org/) is used to bundle assets.
# Workflow Commands
```bash
$ ./do install # install PHP and JS dependencies
$ ./do update # update PHP and JS dependencies
$ ./do compile:css # compiles Stylus files into CSS.
$ ./do compile:js # bundles JS files for the browser.
$ ./do compile:all # compiles CSS and JS files.
$ ./do watch:css # watch CSS files for changes and compile them.
$ ./do watch:js # watch JS files for changes and compile them.
$ ./do watch # watch CSS and JS files for changes and compile them.
$ ./do test:unit [--file=...] [--debug]
# runs the PHP unit tests.
# if --file specified then only tests on that file are executed.
# if --debug then tests are executed in debugging mode.
$ ./do test:integration [--file=...] [--multisite] [--debug]
# runs the PHP integration tests.
# if --file specified then only tests on that file are executed.
# if --multisite then tests are executed in a multisite wordpress setup.
# if --debug then tests are executed in debugging mode.
$ ./do test:multisite:integration # alias for ./do test:integration --multisite
$ ./do test:debug:unit # alias for ./do test:unit --debug
$ ./do test:debug:integration # alias for ./do test:integration --debug
$ ./do test:failed:unit # run the last failing unit test.
$ ./do test:failed:integration # run the last failing integration test.
$ ./do test:coverage # run tests and output coverage information.
$ ./do test:javascript # run the JS tests.
$ ./do test:acceptance [--file=...] [--skip-deps]
# run acceptances tests into a docker environment.
# if --file given then only tests on that file are executed.
# if --skip-deps then it skips installation of composer dependencies.
$ ./do test:acceptance:multisite [--file=...] [--skip-deps]
# same as test:acceptance but runs into a multisite wordpress setup.
$ ./do delete:docker # stop and remove all running docker containers.
$ ./do qa:lint # PHP code linter.
$ ./do qa:lint:javascript # JS code linter.
$ ./do qa # PHP and JS linters.
$ ./do container:dump # Generates DI container cache.
``` ```
- JS tests (using Mocha): # Coding and Testing
```sh
$ ./do test:javascript ## DI
We use Symfony/dependency-injection container. Container configuration can be found in `libs/DI/ContainerFactory.php`
The container is configured and used with minimum sub-dependencies to keep final package size small.
You can check [the docs](https://symfony.com/doc/3.4/components/dependency_injection.html) to learn more about Symfony Container.
## Mozart
We use Mozart plugin for composer to prevent plugin libraries conflicts in PHP. Two plugins may be using different versions of a library. Mozart prefix dependencies namespaces and moves them into `libs\Dependencies` directory.
Dependencies handled by Mozart are configured in extra configuration file `mozart/composer.json`. Installation and processing is triggered in post scripts of the main `composer.json` file.
## i18n
We use functions `__()`, `_n()` and `_x()` with domain `mailpoet` to translate strings.
**in PHP code**
```php
__('text to translate', 'mailpoet');
_n('single text', 'plural text', $number, 'mailpoet');
_x('text to translate', 'context for translators', 'mailpoet');
``` ```
- Debug tests: **in Twig views**
```sh
$ ./do test:debug
```
- Code linters and quality checkers:
```sh
$ ./do qa
```
- Javascript linter:
```sh
$ ./do lint:javascript
```
# CSS
- [Stylus](https://learnboost.github.io/stylus/)
- [Nib extension](http://tj.github.io/nib/)
```sh
assets/css/src -> place your *.styl files here
```
### Watch for changes and recompile
```sh
$ ./do watch
```
## Module loading and organization
Our JS modules are stored in `assets/js/` folder. Modules should follow AMD module definition style:
```js
define('moduleName', ['dependency1', 'dependency2'], function(dependency1, dependency2){
// Module code here
return {
// Module exports here
};
})
```
Module loader will look for `dependency1` in `node_modules/` dependencies, as well as in `assets/js`. So you can use dependencies, defined in `package.json`, without the need of providing an absolute path to it.
Once found, dependencies will be injected into your module via function arguments.
When it comes to loading modules on a real page, WebPack uses "entry points" to create different bundles. In order for the module to be included in a specific bundle, it must be reachable from that bundle's entry point. [A good example on WebPack's website](http://webpack.github.io/docs/code-splitting.html#split-app-and-vendor-code).
Once javascript is compiled with `./do compile:javascript`, your module will be placed into a bundle. Including that bundle in a webpage will give provide you access to your module.
## Handlebars (`views/*.hbs`)
```html ```html
<!-- use the `templates` block --> <%= __('text to translate') %>
<% block templates %> <%= _n('single text', 'plural text', $number) %>
<!-- include a .hbs template --> <%= _x('text to translate', 'context for translators') %>
<%= partial('my_template_1', 'form/templates/toolbar/fields.hbs') %> ```
<!-- include a .hbs template and register it as a partial --> The domain `mailpoet` will be added automatically by the Twig functions.
<%= partial('my_template_2', 'form/templates/blocks.hbs', '_my_partial') %>
<!-- custom partial using partial defined above --> **in Javascript code**
<script id="my_template_3" type="text/x-handlebars-template">
{{> _my_partial }} First add the string to the translations block in the Twig view:
</script>
```html
<% block translations %>
<%= localize({
'key': __('string to translate'),
...
}) %>
<% endblock %> <% endblock %>
``` ```
# i18n Then use `MailPoet.I18n.t('key')` to get the translated string on your Javascript code.
- Use the regular WordPress functions in PHP and Twig:
```php ## Acceptance testing
__()
_n()
_x()
```
```html
<p>
<%= __('Click %shere%s!') | format('<a href="#">', '</a>') | raw %>
</p>
```
```html
<p>
<%= _n('deleted %d message', 'deleted %d messages', count) | format(count) %>
<!-- count === 1 -> "deleted 1 message" -->
<!-- count > 1 -> "deleted $count messages" -->
</p>
```
- Handlebars.
You can use Twig i18n functions in Handlebars, just load your template from a Twig view.
# Build
To build a plugin , run `./build.sh`.
Some build process steps are described below (their dependencies etc.).
## packtranslations step
This step imports translations from Transifex and generates MO files. It requires:
* `tx` client: https://docs.transifex.com/client/installing-the-client
* `msgfmt` command (from Gettext package)
Finally , a `WP_TRANSIFEX_API_TOKEN` environment variable should be initialized with a valid key.
# Publish
The `publish` command currently does the following:
* Pushes translations POT file to Transifex;
* Publishes the release in SVN.
Before you run it, you need to:
1. Ensure there is an up-to-date local copy of MailPoet SVN repository in `.mp_svn` directory by running `./do svn:checkout`.
2. Have all your features merged in Git `master`, your `mailpoet.php` and `readme.txt` tagged with a new version.
3. Run `./build.sh` to produce a `mailpoet.zip` distributable archive.
Everything's ready? Then run `./do publish`.
If the job goes fine, you'll get a message like this:
```
Go to '.mp_svn' and run 'svn ci -m "Release 3.0.0-beta.9"' to publish the
release
Run 'svn copy ...' to tag the release
```
It's quite literal: you can review the changes to be pushed and if you're satisfied, run the suggested command to finish the release publishing process.
If you're confident, execute `./do publish --force` and your release will be published to the remote SVN repository without manual intervention (automatically). For easier authentication you might want to set `WP_SVN_USERNAME` and `WP_SVN_PASSWORD` environment variables.
# Acceptance testing
We are using Gravity Flow plugin's setup as an example for our acceptance test suite: https://www.stevenhenty.com/learn-acceptance-testing-deeply/ We are using Gravity Flow plugin's setup as an example for our acceptance test suite: https://www.stevenhenty.com/learn-acceptance-testing-deeply/
@ -189,17 +158,6 @@ From the article above:
_Windows users only: enable hard drive sharing in the Docker settings._ _Windows users only: enable hard drive sharing in the Docker settings._
The browser runs in a docker container. You can use a VNC client to watch the test run, follow instructions in official The browser runs in a docker container. You can use a VNC client to watch the test run, follow instructions in official
repo: https://github.com/SeleniumHQ/docker-selenium repo: https://github.com/SeleniumHQ/docker-selenium
If youre on a Mac, you can open vnc://localhost:5900 in Safari to watch the tests running in Chrome. If youre on Windows, youll need a VNC client. Password: secret. If youre on a Mac, you can open vnc://localhost:5900 in Safari to watch the tests running in Chrome. If youre on Windows, youll need a VNC client. Password: secret.
To run tests:
```sh
$ ./do test:acceptance
```
You can skip installation of composer dependencies using --skip-deps parameter.
```sh
$ ./do test:acceptance --skip-deps
```

View File

@ -156,7 +156,27 @@ class RoboFile extends \Robo\Tasks {
function testUnit(array $opts=['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) { function testUnit(array $opts=['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
$this->loadEnv(); $this->loadEnv();
$command = 'vendor/bin/codecept run unit -c codeception.unit.yml'; $command = 'vendor/bin/codecept run unit';
if($opts['file']) {
$command .= ' -f ' . $opts['file'];
}
if($opts['xml']) {
$command .= ' --xml';
}
if($opts['debug']) {
$command .= ' --debug';
}
return $this->_exec($command);
}
function testIntegration(array $opts=['file' => null, 'xml' => false, 'multisite' => false, 'debug' => false]) {
$this->loadEnv();
$command = 'vendor/bin/codecept run integration';
if($opts['multisite']) { if($opts['multisite']) {
$command = 'MULTISITE=true ' . $command; $command = 'MULTISITE=true ' . $command;
@ -177,14 +197,14 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec($command); return $this->_exec($command);
} }
function testMultisiteUnit($opts=['file' => null, 'xml' => false, 'multisite' => true]) { function testMultisiteIntegration($opts=['file' => null, 'xml' => false, 'multisite' => true]) {
return $this->testUnit($opts); return $this->testIntegration($opts);
} }
function testCoverage($opts=['file' => null, 'xml' => false]) { function testCoverage($opts=['file' => null, 'xml' => false]) {
$this->loadEnv(); $this->loadEnv();
$command = join(' ', array( $command = join(' ', array(
'vendor/bin/codecept run unit -c codeception.unit.yml ', 'vendor/bin/codecept run -s acceptance',
(($opts['file']) ? $opts['file'] : ''), (($opts['file']) ? $opts['file'] : ''),
'--coverage', '--coverage',
($opts['xml']) ? '--coverage-xml' : '--coverage-html' ($opts['xml']) ? '--coverage-xml' : '--coverage-html'
@ -219,16 +239,12 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec('vendor/bin/security-checker security:check --format=simple'); return $this->_exec('vendor/bin/security-checker security:check --format=simple');
} }
function testDebug($opts=['file' => null, 'xml' => false]) { function testDebugUnit($opts=['file' => null, 'xml' => false, 'debug' => true]) {
$this->loadEnv(); return $this->testUnit($opts);
$this->_exec('vendor/bin/codecept build -c codeception.unit.yml'); }
$command = 'vendor/bin/codecept run unit -c codeception.unit.yml --debug -f '.(($opts['file']) ? $opts['file'] : ''); function testDebugIntegration($opts=['file' => null, 'xml' => false, 'debug' => true]) {
return $this->testIntegration($opts);
if($opts['xml']) {
$command .= ' --xml';
}
return $this->_exec($command);
} }
function testAcceptance($opts=['file' => null, 'skip-deps' => false]) { function testAcceptance($opts=['file' => null, 'skip-deps' => false]) {
@ -254,10 +270,31 @@ class RoboFile extends \Robo\Tasks {
return $this->_exec('docker-compose down -v --remove-orphans --rmi all'); return $this->_exec('docker-compose down -v --remove-orphans --rmi all');
} }
function testFailed() { function testFailedUnit() {
$this->loadEnv(); $this->loadEnv();
$this->_exec('vendor/bin/codecept build -c codeception.unit.yml'); $this->_exec('vendor/bin/codecept build');
return $this->_exec('vendor/bin/codecept run -c codeception.unit.yml -g failed'); return $this->_exec('vendor/bin/codecept run unit -g failed');
}
function testFailedIntegration() {
$this->loadEnv();
$this->_exec('vendor/bin/codecept build');
return $this->_exec('vendor/bin/codecept run integration -g failed');
}
function containerDump() {
$this->say('Deleting DI Container');
$this->_exec('rm -f ./lib/DI/CachedContainer.php');
$this->say('Generating DI container cache');
$this->loadEnv();
define('ABSPATH', getenv('WP_ROOT') . '/');
if (!file_exists(ABSPATH . 'wp-config.php')) {
$this->yell('WP_ROOT env variable does not contain valid path to wordpress root.', 40, 'red');
exit(1);
}
require_once __DIR__ . '/vendor/autoload.php';
$container_factory = new \MailPoet\DI\ContainerFactory();
$container_factory->dumpContainer();
} }
function qa() { function qa() {
@ -288,7 +325,7 @@ class RoboFile extends \Robo\Tasks {
->taskExec( ->taskExec(
'./vendor/bin/phpcs '. './vendor/bin/phpcs '.
'--standard=./tasks/code_sniffer/MailPoet '. '--standard=./tasks/code_sniffer/MailPoet '.
'--runtime-set testVersion 5.5-7.2 '. '--runtime-set testVersion 5.6-7.2 '.
'--ignore=./lib/Util/Sudzy/*,./lib/Util/CSS.php,./lib/Util/XLSXWriter.php,./lib/Dependencies/*,'. '--ignore=./lib/Util/Sudzy/*,./lib/Util/CSS.php,./lib/Util/XLSXWriter.php,./lib/Dependencies/*,'.
'./lib/Util/pQuery/*,./lib/Config/PopulatorData/Templates/* '. './lib/Util/pQuery/*,./lib/Config/PopulatorData/Templates/* '.
'lib/ '. 'lib/ '.
@ -297,9 +334,9 @@ class RoboFile extends \Robo\Tasks {
->taskExec( ->taskExec(
'./vendor/bin/phpcs '. './vendor/bin/phpcs '.
'--standard=./tasks/code_sniffer/MailPoet '. '--standard=./tasks/code_sniffer/MailPoet '.
'--runtime-set testVersion 5.5-7.2 '. '--runtime-set testVersion 5.6-7.2 '.
'--ignore=./tests/unit/_bootstrap.php '. '--ignore=./tests/unit/_bootstrap.php,./tests/unit/_fixtures.php,./tests/integration/_bootstrap.php,./tests/integration/_fixtures.php '.
'tests/unit/ '. 'tests/unit tests/integration tests/acceptance tests/DataFactories '.
$severityFlag $severityFlag
) )
->run(); ->run();

View File

@ -16,3 +16,18 @@ Style for Members plugin
#wpbody #wpbody
padding-bottom: 20px; padding-bottom: 20px;
/* menu icon */
#adminmenu #toplevel_page_mailpoet-newsletters .wp-menu-image
background-size: 18px 18px;
background-repeat: no-repeat;
background-position: center;
#adminmenu #toplevel_page_mailpoet-newsletters.wp-not-current-submenu .wp-menu-image
background-image: url('data:image/png;base64,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');
#adminmenu #toplevel_page_mailpoet-newsletters.wp-has-current-submenu .wp-menu-image
background-image: url('data:image/png;base64,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');
#adminmenu #toplevel_page_mailpoet-newsletters a:hover .wp-menu-image
background-image: url('data:image/png;base64,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');

View File

@ -36,3 +36,5 @@
@require 'welcome_wizard' @require 'welcome_wizard'
@require 'intro' @require 'intro'
@require 'in_app_announcements' @require 'in_app_announcements'
@require 'newsletter_congratulate.styl'
@require 'discounts'

View File

@ -0,0 +1,17 @@
.mailpoet-discount-container
margin: 15px
padding: 20px
background: white
border: 1px solid #FF5301
text-align: center
.mailpoet-discount-container h1
margin: 0
line-height: 1.2em
font-size: 2.8em
font-weight: 400
.mailpoet-discount-container p
line-height: 1.2em
font-size: 1.2em

View File

@ -40,9 +40,14 @@
top: -3px top: -3px
left: 8px left: 8px
.mailpoet_in_app_announcement_background_videos .mailpoet_in_app_announcement_background_videos, .mailpoet_drag_and_drop_tutorial
text-align: center text-align: center
h2 h2
font-size: 28px font-size: 28px
video video
margin-top: 20px margin-top: 20px
.new_subscriber_notification_announcement
h2
font-size: 28px
text-align: center

View File

@ -168,14 +168,18 @@ body.mailpoet_modal_opened
padding-bottom: 52px padding-bottom: 52px
#mailpoet_loading #mailpoet_loading
width: 150px
height: 32px
position: relative position: relative
left: 50% left: 50%
top: 50% top: 50%
margin-left: -75px margin-left: -75px
margin-top: -16px margin-top: -16px
.mailpoet_loading
height: 32px
width: 150px
display flex
flex-direction: row
.mailpoet_modal_loading .mailpoet_modal_loading
animation-direction(linear) animation-direction(linear)
animation-duration(1.9500000000000002s) animation-duration(1.9500000000000002s)
@ -183,18 +187,17 @@ body.mailpoet_modal_opened
animation-name(bounce_mailpoet_modal_loading) animation-name(bounce_mailpoet_modal_loading)
border-radius(21px) border-radius(21px)
background-color: #E01D4E background-color: #E01D4E
float: left
height: 32px height: 32px
margin-left: 17px margin-left: 17px
width: 32px width: 32px
#mailpoet_modal_loading_1 #mailpoet_modal_loading_1, .mailpoet_modal_loading_1
animation-delay(0.39s) animation-delay(0.39s)
#mailpoet_modal_loading_2 #mailpoet_modal_loading_2, .mailpoet_modal_loading_2
animation-delay(0.9099999999999999s) animation-delay(0.9099999999999999s)
#mailpoet_modal_loading_3 #mailpoet_modal_loading_3, .mailpoet_modal_loading_3
animation-delay(1.1700000000000002s) animation-delay(1.1700000000000002s)
@keyframes bounce_mailpoet_modal_loading @keyframes bounce_mailpoet_modal_loading

View File

@ -0,0 +1,23 @@
.newsletter_congratulate_page
margin-top: 30px;
.mailpoet_newsletter_loading
text-align: center;
.mailpoet_loading
margin: 100px auto 0 auto;
.mailpoet_newsletter_loading_header
margin: 30px;
.mailpoet_congratulate_success
width: 100%;
h1
text-align center;
margin-bottom: 30px;
img, .button
margin-left: auto;
margin-right: auto;
display: block;

View File

@ -236,3 +236,28 @@ select.mailpoet_font-size
margin-left: 10px margin-left: 10px
input.mailpoet_option_offset_left_small input.mailpoet_option_offset_left_small
margin-left: 10px !important margin-left: 10px !important
.mailpoet_form_field span.select2-container
width: 103px !important
span.select2-container--open > span.select2-dropdown
width: 150px !important
span.select2-container--open > span.select2-dropdown li.select2-results__option
font-size: 13px
margin: 0px !important
& .select2-results__group
font-weight: normal
color: #bfbfbf
& .select2-results__option
padding-left: 15px
font-size: 13px
&[aria-selected=true]
background-color: #eee
color: #444
.mailpoet-fonts-notice
color: #999

View File

@ -1,5 +1,5 @@
#mailpoet_editor_bottom #mailpoet_editor_bottom
margin: 10px 0 70px margin: 10px 0 120px
.mailpoet_save_wrapper .mailpoet_save_wrapper
float: right float: right
@ -7,6 +7,9 @@
margin-right: 20px margin-right: 20px
margin-bottom: 10px margin-bottom: 10px
.mailpoet_save_next
margin-left: 5px
.mailpoet_save_options .mailpoet_save_options
border-radius(3px) border-radius(3px)
@ -43,6 +46,8 @@
.mailpoet_save_show_options_icon .mailpoet_save_show_options_icon
vertical-align: middle vertical-align: middle
height: 14px;
margin-top: -6px;
.mailpoet_save_as_template_container, .mailpoet_save_as_template_container,
.mailpoet_export_template_container .mailpoet_export_template_container
@ -61,16 +66,25 @@
.mailpoet_save_as_template_title, .mailpoet_save_as_template_title,
.mailpoet_export_template_title .mailpoet_export_template_title
font-size: 1.1em font-size: 1.1em
.mailpoet_save_next, .mailpoet_save_button_group
float: right
.mailpoet_editor_messages
position: absolute
right: 0
.mailpoet_editor_last_saved .mailpoet_editor_last_saved
color: $primary-inactive-color color: $primary-inactive-color
font-size: 0.9em font-size: 0.9em
position: absolute
right: 0
margin-top: 10px margin-top: 10px
text-align: right
.mailpoet_save_error .mailpoet_save_error
margin-top: 10px
width: $sidebar-width - 20px
color: $error-text-color color: $error-text-color
text-align: right
.mailpoet_save_dropdown_down .mailpoet_save_dropdown_down
.mailpoet_save_options, .mailpoet_save_options,

View File

@ -20,6 +20,10 @@
& > .mailpoet_block & > .mailpoet_block
width: 100% width: 100%
.mailpoet_container_block
margin-bottom: 0
.mailpoet_automated_latest_content_display_options .mailpoet_automated_latest_content_display_options
animation-slide-open-downwards() animation-slide-open-downwards()

View File

@ -2,6 +2,7 @@ $column-margin = 20px
$one-column-width = $newsletter-width - (2 * $column-margin) $one-column-width = $newsletter-width - (2 * $column-margin)
$two-column-width = ($newsletter-width / 2) - (2 * $column-margin) $two-column-width = ($newsletter-width / 2) - (2 * $column-margin)
$three-column-width = ($newsletter-width / 3) - (2 * $column-margin) $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
$two-column-wider-column-width = (($newsletter-width / 3) - $column-margin) * 2
.mailpoet_container .mailpoet_container
width: 100% width: 100%
@ -27,12 +28,6 @@ $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
.mailpoet_container_horizontal > * .mailpoet_container_horizontal > *
vertical-align: top vertical-align: top
/**
* Enforce column widths:
* 1 column: 20px + 560px + 20px
* 2 columns: 20px + 260px + 20px + 260px + 20px
* 3 columns: 20px + 160px + 20px + 20px + 160px + 20px + 20px + 160px + 20px
*/
#mailpoet_editor_content #mailpoet_editor_content
.mailpoet_container .mailpoet_container
@ -73,6 +68,14 @@ $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
//padding-right: 20px //padding-right: 20px
width: $column-margin + $three-column-width + $column-margin width: $column-margin + $three-column-width + $column-margin
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_1_2 > .mailpoet_container_block:first-child,
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_2_1 > .mailpoet_container_block:nth-child(2)
width: $column-margin + $three-column-width + $column-margin
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_2_1 > .mailpoet_container_block:first-child,
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal.mailpoet_irregular_width_contents_container.column_layout_1_2 > .mailpoet_container_block:nth-child(2)
width: $column-margin + $two-column-wider-column-width + $column-margin
.mailpoet_container_empty .mailpoet_container_empty
text-align: center text-align: center
background-color: #f2f2f2 background-color: #f2f2f2

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
const KeyValueTable = props => ( const KeyValueTable = props => (
@ -13,13 +14,13 @@ const KeyValueTable = props => (
); );
KeyValueTable.propTypes = { KeyValueTable.propTypes = {
max_width: React.PropTypes.string, max_width: PropTypes.string,
rows: React.PropTypes.arrayOf(React.PropTypes.shape({ rows: PropTypes.arrayOf(PropTypes.shape({
key: React.PropTypes.string.isRequired, key: PropTypes.string.isRequired,
value: React.PropTypes.oneOfType([ value: PropTypes.oneOfType([
React.PropTypes.string, PropTypes.string,
React.PropTypes.number, PropTypes.number,
React.PropTypes.element, PropTypes.element,
]).isRequired, ]).isRequired,
})).isRequired, })).isRequired,
}; };

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
@ -10,10 +11,10 @@ const PrintBoolean = props => (
); );
PrintBoolean.propTypes = { PrintBoolean.propTypes = {
truthy: React.PropTypes.string, truthy: PropTypes.string,
falsy: React.PropTypes.string, falsy: PropTypes.string,
unknown: React.PropTypes.string, unknown: PropTypes.string,
children: React.PropTypes.bool, children: PropTypes.bool,
}; };
PrintBoolean.defaultProps = { PrintBoolean.defaultProps = {

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
const SteppedProgressBar = (props) => { const SteppedProgressBar = (props) => {
@ -20,8 +21,8 @@ const SteppedProgressBar = (props) => {
}; };
SteppedProgressBar.propTypes = { SteppedProgressBar.propTypes = {
steps_count: React.PropTypes.number.isRequired, steps_count: PropTypes.number.isRequired,
step: React.PropTypes.number.isRequired, step: PropTypes.number.isRequired,
}; };
module.exports = SteppedProgressBar; module.exports = SteppedProgressBar;

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
const FormFieldCheckbox = React.createClass({ class FormFieldCheckbox extends React.Component {
onValueChange: function onValueChange(e) { onValueChange = (e) => {
e.target.value = this.checkbox.checked ? '1' : '0'; e.target.value = this.checkbox.checked ? '1' : '0';
return this.props.onValueChange(e); return this.props.onValueChange(e);
}, };
render: function render() {
render() {
if (this.props.field.values === undefined) { if (this.props.field.values === undefined) {
return false; return false;
} }
@ -37,7 +39,16 @@ const FormFieldCheckbox = React.createClass({
{ options } { options }
</div> </div>
); );
}, }
}); }
FormFieldCheckbox.propTypes = {
onValueChange: PropTypes.func.isRequired,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.object.isRequired,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default FormFieldCheckbox; export default FormFieldCheckbox;

View File

@ -36,7 +36,10 @@ FormFieldDateYear.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired, onValueChange: PropTypes.func.isRequired,
year: PropTypes.string.isRequired, year: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
}; };
function FormFieldDateMonth(props) { function FormFieldDateMonth(props) {
@ -71,7 +74,10 @@ FormFieldDateMonth.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired, onValueChange: PropTypes.func.isRequired,
month: PropTypes.string.isRequired, month: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
monthNames: PropTypes.arrayOf(PropTypes.string).isRequired, monthNames: PropTypes.arrayOf(PropTypes.string).isRequired,
}; };
@ -108,7 +114,10 @@ FormFieldDateDay.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired, onValueChange: PropTypes.func.isRequired,
day: PropTypes.string.isRequired, day: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
}; };
class FormFieldDate extends React.Component { class FormFieldDate extends React.Component {

View File

@ -7,9 +7,10 @@ import FormFieldCheckbox from 'form/fields/checkbox.jsx';
import FormFieldSelection from 'form/fields/selection.jsx'; import FormFieldSelection from 'form/fields/selection.jsx';
import FormFieldDate from 'form/fields/date.jsx'; import FormFieldDate from 'form/fields/date.jsx';
import jQuery from 'jquery'; import jQuery from 'jquery';
import PropTypes from 'prop-types';
const FormField = React.createClass({ class FormField extends React.Component {
renderField: function renderField(data, inline = false) { renderField = (data, inline = false) => {
let description = false; let description = false;
if (data.field.description) { if (data.field.description) {
description = ( description = (
@ -76,8 +77,9 @@ const FormField = React.createClass({
{ description } { description }
</div> </div>
); );
}, };
render: function render() {
render() {
let field = false; let field = false;
if (this.props.field.fields !== undefined) { if (this.props.field.fields !== undefined) {
@ -113,7 +115,29 @@ const FormField = React.createClass({
</td> </td>
</tr> </tr>
); );
}
}
FormField.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.object,
tip: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string,
]),
label: PropTypes.string,
fields: PropTypes.array,
description: PropTypes.string,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
FormField.defaultProps = {
onValueChange: function onValueChange() {
// no-op
}, },
}); };
export default FormField; export default FormField;

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
const FormFieldRadio = React.createClass({ class FormFieldRadio extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
render: function render() { render() {
if (this.props.field.values === undefined) { if (this.props.field.values === undefined) {
return false; return false;
} }
@ -30,7 +31,23 @@ const FormFieldRadio = React.createClass({
{ options } { options }
</div> </div>
); );
}
}
FormFieldRadio.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.object,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
FormFieldRadio.defaultProps = {
onValueChange: function onValueChange() {
// no-op
}, },
}); };
export default FormFieldRadio; export default FormFieldRadio;

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import _ from 'underscore'; import _ from 'underscore';
import PropTypes from 'prop-types';
const FormFieldSelect = React.createClass({ class FormFieldSelect extends React.Component {
render() { render() {
if (this.props.field.values === undefined) { if (this.props.field.values === undefined) {
return false; return false;
@ -70,7 +71,29 @@ const FormFieldSelect = React.createClass({
{options} {options}
</select> </select>
); );
}
}
FormFieldSelect.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.object,
placeholder: PropTypes.string,
filter: PropTypes.func,
sortBy: PropTypes.func,
validation: PropTypes.object,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
automationId: PropTypes.string,
};
FormFieldSelect.defaultProps = {
automationId: '',
onValueChange: function onValueChange() {
// no-op
}, },
}); };
module.exports = FormFieldSelect; module.exports = FormFieldSelect;

View File

@ -3,23 +3,16 @@ import jQuery from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import 'react-dom'; import 'react-dom';
import 'select2'; import 'select2';
import PropTypes from 'prop-types';
const Selection = React.createClass({ class Selection extends React.Component {
allowMultipleValues: function allowMultipleValues() { componentDidMount() {
return (this.props.field.multiple === true);
},
isSelect2Initialized: function isSelect2Initialized() {
return (jQuery(`#${this.select.id}`).hasClass('select2-hidden-accessible') === true);
},
isSelect2Component: function isSelect2Component() {
return this.allowMultipleValues() || this.props.field.forceSelect2;
},
componentDidMount: function componentDidMount() {
if (this.isSelect2Component()) { if (this.isSelect2Component()) {
this.setupSelect2(); this.setupSelect2();
} }
}, }
componentDidUpdate: function componentDidUpdate(prevProps) {
componentDidUpdate(prevProps) {
if ((this.props.item !== undefined && prevProps.item !== undefined) if ((this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id) && (this.props.item.id !== prevProps.item.id)
) { ) {
@ -34,38 +27,73 @@ const Selection = React.createClass({
) { ) {
this.resetSelect2(); this.resetSelect2();
} }
}, }
componentWillUnmount: function componentWillUnmount() {
componentWillUnmount() {
if (this.isSelect2Component()) { if (this.isSelect2Component()) {
this.destroySelect2(); this.destroySelect2();
} }
}, }
getFieldId: function getFieldId(data) {
getFieldId = (data) => {
const props = data || this.props; const props = data || this.props;
return props.field.id || props.field.name; return props.field.id || props.field.name;
}, };
resetSelect2: function resetSelect2() {
this.destroySelect2();
this.setupSelect2();
},
destroySelect2: function destroySelect2() {
if (this.isSelect2Initialized()) {
jQuery(`#${this.select.id}`).select2('destroy');
this.cleanupAfterSelect2();
}
},
cleanupAfterSelect2: function cleanupAfterSelect2() {
// remove DOM elements created by Select2 that are not tracked by React
jQuery(`#${this.select.id}`)
.find('option:not(.default)')
.remove();
// unbind events (https://select2.org/programmatic-control/methods#event-unbinding) getSelectedValues = () => {
jQuery(`#${this.select.id}`) if (this.props.field.selected !== undefined) {
.off('select2:unselecting') return this.props.field.selected(this.props.item);
.off('select2:opening'); } else if (this.props.item !== undefined && this.props.field.name !== undefined) {
}, if (this.allowMultipleValues()) {
setupSelect2: function setupSelect2() { if (_.isArray(this.props.item[this.props.field.name])) {
return this.props.item[this.props.field.name].map(item => item.id);
}
} else {
return this.props.item[this.props.field.name];
}
}
return null;
};
getItems = () => {
let items;
if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
items = window[`mailpoet_${this.props.field.endpoint}`];
} else if (this.props.field.values !== undefined) {
items = this.props.field.values;
}
if (_.isArray(items)) {
if (this.props.field.filter !== undefined) {
items = items.filter(this.props.field.filter);
}
}
return items;
};
getLabel = (item) => {
if (this.props.field.getLabel !== undefined) {
return this.props.field.getLabel(item, this.props.item);
}
return item.name;
};
getSearchLabel = (item) => {
if (this.props.field.getSearchLabel !== undefined) {
return this.props.field.getSearchLabel(item, this.props.item);
}
return null;
};
getValue = (item) => {
if (this.props.field.getValue !== undefined) {
return this.props.field.getValue(item, this.props.item);
}
return item.id;
};
setupSelect2 = () => {
if (this.isSelect2Initialized()) { if (this.isSelect2Initialized()) {
return; return;
} }
@ -138,38 +166,39 @@ const Selection = React.createClass({
}); });
select2.on('change', this.handleChange); select2.on('change', this.handleChange);
}, };
getSelectedValues: function getSelectedValues() {
if (this.props.field.selected !== undefined) {
return this.props.field.selected(this.props.item);
} else if (this.props.item !== undefined && this.props.field.name !== undefined) {
if (this.allowMultipleValues()) {
if (_.isArray(this.props.item[this.props.field.name])) {
return this.props.item[this.props.field.name].map(item => item.id);
}
} else {
return this.props.item[this.props.field.name];
}
}
return null;
},
getItems: function getItems() {
let items;
if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
items = window[`mailpoet_${this.props.field.endpoint}`];
} else if (this.props.field.values !== undefined) {
items = this.props.field.values;
}
if (_.isArray(items)) { resetSelect2 = () => {
if (this.props.field.filter !== undefined) { this.destroySelect2();
items = items.filter(this.props.field.filter); this.setupSelect2();
} };
}
return items; destroySelect2 = () => {
}, if (this.isSelect2Initialized()) {
handleChange: function handleChange(e) { jQuery(`#${this.select.id}`).select2('destroy');
this.cleanupAfterSelect2();
}
};
cleanupAfterSelect2 = () => {
// remove DOM elements created by Select2 that are not tracked by React
jQuery(`#${this.select.id}`)
.find('option:not(.default)')
.remove();
// unbind events (https://select2.org/programmatic-control/methods#event-unbinding)
jQuery(`#${this.select.id}`)
.off('select2:unselecting')
.off('select2:opening');
};
allowMultipleValues = () => (this.props.field.multiple === true);
isSelect2Initialized = () => (jQuery(`#${this.select.id}`).hasClass('select2-hidden-accessible') === true);
isSelect2Component = () => this.allowMultipleValues() || this.props.field.forceSelect2;
handleChange = (e) => {
if (this.props.onValueChange === undefined) return; if (this.props.onValueChange === undefined) return;
const valueTextPair = jQuery(`#${this.select.id}`).children(':selected').map(function element() { const valueTextPair = jQuery(`#${this.select.id}`).children(':selected').map(function element() {
@ -185,43 +214,28 @@ const Selection = React.createClass({
id: e.target.id, id: e.target.id,
}, },
}); });
}, };
getLabel: function getLabel(item) {
if (this.props.field.getLabel !== undefined) {
return this.props.field.getLabel(item, this.props.item);
}
return item.name;
},
getSearchLabel: function getSearchLabel(item) {
if (this.props.field.getSearchLabel !== undefined) {
return this.props.field.getSearchLabel(item, this.props.item);
}
return null;
},
getValue: function getValue(item) {
if (this.props.field.getValue !== undefined) {
return this.props.field.getValue(item, this.props.item);
}
return item.id;
},
// When it's impossible to represent the desired value in DOM, // When it's impossible to represent the desired value in DOM,
// this function may be used to transform the placeholder value into // this function may be used to transform the placeholder value into
// desired value. // desired value.
transformChangedValue: function transformChangedValue(value, textValuePair) { transformChangedValue = (value, textValuePair) => {
if (typeof this.props.field.transformChangedValue === 'function') { if (typeof this.props.field.transformChangedValue === 'function') {
return this.props.field.transformChangedValue.call(this, value, textValuePair); return this.props.field.transformChangedValue.call(this, value, textValuePair);
} }
return value; return value;
}, };
insertEmptyOption: function insertEmptyOption() {
insertEmptyOption = () => {
// https://select2.org/placeholders // https://select2.org/placeholders
// For single selects only, in order for the placeholder value to appear, // For single selects only, in order for the placeholder value to appear,
// we must have a blank <option> as the first option in the <select> control. // we must have a blank <option> as the first option in the <select> control.
if (this.allowMultipleValues()) return undefined; if (this.allowMultipleValues()) return undefined;
if (this.props.field.placeholder) return (<option className="default" />); if (this.props.field.placeholder) return (<option className="default" />);
return undefined; return undefined;
}, };
render: function render() {
render() {
const items = this.getItems(this.props.field); const items = this.getItems(this.props.field);
const selectedValues = this.getSelectedValues(); const selectedValues = this.getSelectedValues();
const options = items.map((item) => { const options = items.map((item) => {
@ -255,7 +269,42 @@ const Selection = React.createClass({
{ options } { options }
</select> </select>
); );
}
}
Selection.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.object,
getLabel: PropTypes.func,
resetSelect2OnUpdate: PropTypes.bool,
selected: PropTypes.func,
endpoint: PropTypes.string,
filter: PropTypes.func,
getSearchLabel: PropTypes.func,
getValue: PropTypes.func,
placeholder: PropTypes.string,
remoteQuery: PropTypes.object,
extendSelect2Options: PropTypes.object,
multiple: PropTypes.bool,
forceSelect2: PropTypes.bool,
transformChangedValue: PropTypes.func,
disabled: PropTypes.bool,
validation: PropTypes.object,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
disabled: PropTypes.bool,
width: PropTypes.string,
};
Selection.defaultProps = {
onValueChange: function onValueChange() {
// no-op
}, },
}); disabled: false,
width: '',
};
export default Selection; export default Selection;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
const FormFieldText = React.createClass({ class FormFieldText extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
render() { render() {
const name = this.props.field.name || null; const name = this.props.field.name || null;
const item = this.props.item || {}; const item = this.props.item || {};
@ -51,7 +52,31 @@ const FormFieldText = React.createClass({
{...this.props.field.validation} {...this.props.field.validation}
/> />
); );
}
}
FormFieldText.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
defaultValue: PropTypes.string,
id: PropTypes.string,
class: PropTypes.string,
size: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
disabled: PropTypes.func,
placeholder: PropTypes.string,
validation: PropTypes.object,
}).isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
FormFieldText.defaultProps = {
onValueChange: function onValueChange() {
// no-op
}, },
}); };
module.exports = FormFieldText; module.exports = FormFieldText;

View File

@ -3,30 +3,43 @@ import MailPoet from 'mailpoet';
import classNames from 'classnames'; import classNames from 'classnames';
import FormField from 'form/fields/field.jsx'; import FormField from 'form/fields/field.jsx';
import jQuery from 'jquery'; import jQuery from 'jquery';
import PropTypes from 'prop-types';
const Form = React.createClass({ class Form extends React.Component {
contextTypes: { static contextTypes = {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, };
getDefaultProps: function getDefaultProps() {
return { static defaultProps = {
params: {}, params: {},
}; errors: undefined,
}, fields: undefined,
getInitialState: function getInitialState() { item: undefined,
return { onItemLoad: undefined,
loading: false, isValid: undefined,
errors: [], onSuccess: undefined,
item: {}, onChange: undefined,
}; loading: false,
}, beforeFormContent: undefined,
getValues: function getValues() { afterFormContent: undefined,
return this.props.item ? this.props.item : this.state.item; children: undefined,
}, id: '',
getErrors: function getErrors() { onSubmit: undefined,
return this.props.errors ? this.props.errors : this.state.errors; automationId: '',
}, messages: {
componentDidMount: function componentDidMount() { onUpdate: () => { /* no-op */ },
onCreate: () => { /* no-op */ },
},
endpoint: undefined,
};
state = {
loading: false,
errors: [],
item: {},
};
componentDidMount() {
if (this.props.params.id !== undefined) { if (this.props.params.id !== undefined) {
this.loadItem(this.props.params.id); this.loadItem(this.props.params.id);
} else { } else {
@ -36,8 +49,9 @@ const Form = React.createClass({
}); });
}); });
} }
}, }
componentWillReceiveProps: function componentWillReceiveProps(props) {
componentWillReceiveProps(props) {
if (props.params.id === undefined) { if (props.params.id === undefined) {
setImmediate(() => { setImmediate(() => {
this.setState({ this.setState({
@ -49,10 +63,16 @@ const Form = React.createClass({
this.form.reset(); this.form.reset();
} }
} }
}, }
loadItem: function loadItem(id) {
getValues = () => this.props.item || this.state.item;
getErrors = () => this.props.errors || this.state.errors;
loadItem = (id) => {
this.setState({ loading: true }); this.setState({ loading: true });
if (!this.props.endpoint) return;
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint, endpoint: this.props.endpoint,
@ -76,8 +96,9 @@ const Form = React.createClass({
this.context.router.push('/new'); this.context.router.push('/new');
}); });
}); });
}, };
handleSubmit: function handleSubmit(e) {
handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
// handle validation // handle validation
@ -105,6 +126,8 @@ const Form = React.createClass({
item.id = this.props.params.id; item.id = this.props.params.id;
} }
if (!this.props.endpoint) return;
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint, endpoint: this.props.endpoint,
@ -129,8 +152,9 @@ const Form = React.createClass({
this.setState({ errors: response.errors }); this.setState({ errors: response.errors });
} }
}); });
}, };
handleValueChange: function handleValueChange(e) {
handleValueChange = (e) => {
if (this.props.onChange) { if (this.props.onChange) {
return this.props.onChange(e); return this.props.onChange(e);
} }
@ -143,8 +167,9 @@ const Form = React.createClass({
item, item,
}); });
return true; return true;
}, };
render: function render() {
render() {
let errors; let errors;
if (this.getErrors() !== undefined) { if (this.getErrors() !== undefined) {
errors = this.getErrors().map(error => ( errors = this.getErrors().map(error => (
@ -231,7 +256,32 @@ const Form = React.createClass({
{ afterFormContent } { afterFormContent }
</div> </div>
); );
}, }
}); }
Form.propTypes = {
params: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
errors: PropTypes.arrayOf(PropTypes.object),
endpoint: PropTypes.string,
fields: PropTypes.arrayOf(PropTypes.object),
messages: PropTypes.shape({
onUpdate: PropTypes.func,
onCreate: PropTypes.func,
}).isRequired,
loading: PropTypes.bool,
children: PropTypes.array, // eslint-disable-line react/forbid-prop-types
id: PropTypes.string,
automationId: PropTypes.string,
beforeFormContent: PropTypes.func,
afterFormContent: PropTypes.func,
onItemLoad: PropTypes.func,
isValid: PropTypes.func,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
onSuccess: PropTypes.func,
};
export default Form; export default Form;

View File

@ -672,7 +672,7 @@ WysijaForm = {
if (type === undefined) type = 'block'; if (type === undefined) type = 'block';
// identify element // identify element
id = element.identify(); id = element.identify();
instance = WysijaForm.instances[id] || new WysijaForm[type.capitalize().camelize()](id); instance = WysijaForm.instances[id] || new (WysijaForm[type.capitalize().camelize()])(id);
WysijaForm.instances[id] = instance; WysijaForm.instances[id] = instance;
return instance; return instance;

View File

@ -1,16 +1,21 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router'; import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
import PropTypes from 'prop-types';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import FormList from './list.jsx'; import FormList from './list.jsx';
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({ class App extends React.Component {
render() { render() {
return this.props.children; return this.props.children;
}, }
}); }
App.propTypes = {
children: PropTypes.element.isRequired,
};
const container = document.getElementById('forms_container'); const container = document.getElementById('forms_container');

View File

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import jQuery from 'jquery'; import jQuery from 'jquery';
import PropTypes from 'prop-types';
import Listing from '../listing/listing.jsx'; import Listing from '../listing/listing.jsx';
const columns = [ const columns = [
@ -122,8 +123,8 @@ const itemActions = [
}, },
]; ];
const FormList = React.createClass({ class FormList extends React.Component {
createForm() { createForm = () => {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: 'forms', endpoint: 'forms',
@ -138,8 +139,9 @@ const FormList = React.createClass({
); );
} }
}); });
}, };
renderItem(form, actions) {
renderItem = (form, actions) => {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
@ -177,7 +179,8 @@ const FormList = React.createClass({
</td> </td>
</div> </div>
); );
}, };
render() { render() {
return ( return (
<div> <div>
@ -204,7 +207,12 @@ const FormList = React.createClass({
/> />
</div> </div>
); );
}, }
}); }
FormList.propTypes = {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
module.exports = FormList; module.exports = FormList;

View File

@ -151,6 +151,16 @@ define('handlebars_helpers', ['handlebars'], function (Handlebars) {
case 'Times New Roman': return new Handlebars.SafeString("'Times New Roman', Times, Baskerville, Georgia, serif"); case 'Times New Roman': return new Handlebars.SafeString("'Times New Roman', Times, Baskerville, Georgia, serif");
case 'Trebuchet MS': return new Handlebars.SafeString("'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif"); case 'Trebuchet MS': return new Handlebars.SafeString("'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif");
case 'Verdana': return new Handlebars.SafeString('Verdana, Geneva, sans-serif'); case 'Verdana': return new Handlebars.SafeString('Verdana, Geneva, sans-serif');
case 'Arvo': return new Handlebars.SafeString('arvo, courier, georgia, serif');
case 'Lato': return new Handlebars.SafeString("lato, 'helvetica neue', helvetica, arial, sans-serif");
case 'Lora': return new Handlebars.SafeString("lora, georgia, 'times new roman', serif");
case 'Merriweather': return new Handlebars.SafeString("merriweather, georgia, 'times new roman', serif");
case 'Merriweather Sans': return new Handlebars.SafeString("'merriweather sans', 'helvetica neue', helvetica, arial, sans-serif");
case 'Noticia Text': return new Handlebars.SafeString("'noticia text', georgia, 'times new roman', serif");
case 'Open Sans': return new Handlebars.SafeString("'open sans', 'helvetica neue', helvetica, arial, sans-serif");
case 'Playfair Display': return new Handlebars.SafeString("playfair display, georgia, 'times new roman', serif");
case 'Roboto': return new Handlebars.SafeString("roboto, 'helvetica neue', helvetica, arial, sans-serif");
case 'Source Sans Pro': return new Handlebars.SafeString("'source sans pro', 'helvetica neue', helvetica, arial, sans-serif");
default: return font; default: return font;
} }
}); });

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import ReactHtmlParser from 'react-html-parser'; import ReactHtmlParser from 'react-html-parser';
@ -47,10 +48,10 @@ function Tooltip(props) {
} }
Tooltip.propTypes = { Tooltip.propTypes = {
tooltipId: React.PropTypes.string, tooltipId: PropTypes.string,
tooltip: React.PropTypes.node.isRequired, tooltip: PropTypes.node.isRequired,
place: React.PropTypes.string, place: PropTypes.string,
className: React.PropTypes.string, className: PropTypes.string,
}; };
Tooltip.defaultProps = { Tooltip.defaultProps = {

View File

@ -1,4 +1,5 @@
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import KeyValueTable from 'common/key_value_table.jsx'; import KeyValueTable from 'common/key_value_table.jsx';
import PrintBoolean from 'common/print_boolean.jsx'; import PrintBoolean from 'common/print_boolean.jsx';
@ -46,12 +47,12 @@ const CronStatus = (props) => {
}; };
CronStatus.propTypes = { CronStatus.propTypes = {
status_data: React.PropTypes.shape({ status_data: PropTypes.shape({
accessible: React.PropTypes.bool, accessible: PropTypes.bool,
status: React.PropTypes.string, status: PropTypes.string,
updated_at: React.PropTypes.number, updated_at: PropTypes.number,
run_accessed_at: React.PropTypes.number, run_accessed_at: PropTypes.number,
run_completed_at: React.PropTypes.number, run_completed_at: PropTypes.number,
}).isRequired, }).isRequired,
}; };

View File

@ -2,18 +2,24 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router'; import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import PropTypes from 'prop-types';
import SystemStatus from 'help/system_status.jsx';
import SystemInfo from 'help/system_info.jsx';
import KnowledgeBase from 'help/knowledge_base.jsx'; import KnowledgeBase from 'help/knowledge_base.jsx';
import SystemInfo from 'help/system_info.jsx';
import SystemStatus from 'help/system_status.jsx';
import YourPrivacy from 'help/your_privacy.jsx';
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({ class App extends React.Component {
render() { render() {
return this.props.children; return this.props.children;
}, }
}); }
App.propTypes = {
children: PropTypes.element.isRequired,
};
const container = document.getElementById('help_container'); const container = document.getElementById('help_container');
@ -26,6 +32,7 @@ if (container) {
<Route path="knowledgeBase(/)**" params={{ tab: 'knowledgeBase' }} component={KnowledgeBase} /> <Route path="knowledgeBase(/)**" params={{ tab: 'knowledgeBase' }} component={KnowledgeBase} />
<Route path="systemStatus(/)**" params={{ tab: 'systemStatus' }} component={SystemStatus} /> <Route path="systemStatus(/)**" params={{ tab: 'systemStatus' }} component={SystemStatus} />
<Route path="systemInfo(/)**" params={{ tab: 'systemInfo' }} component={SystemInfo} /> <Route path="systemInfo(/)**" params={{ tab: 'systemInfo' }} component={SystemInfo} />
<Route path="yourPrivacy(/)**" params={{ tab: 'yourPrivacy' }} component={YourPrivacy} />
</Route> </Route>
</Router> </Router>
), container); ), container);

View File

@ -1,4 +1,5 @@
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import KeyValueTable from 'common/key_value_table.jsx'; import KeyValueTable from 'common/key_value_table.jsx';
import TasksList from './tasks_list/tasks_list.jsx'; import TasksList from './tasks_list/tasks_list.jsx';
@ -58,23 +59,23 @@ const QueueStatus = (props) => {
}; };
QueueStatus.propTypes = { QueueStatus.propTypes = {
status_data: React.PropTypes.shape({ status_data: PropTypes.shape({
status: React.PropTypes.string, status: PropTypes.string,
started: React.PropTypes.number, started: PropTypes.number,
sent: React.PropTypes.number, sent: PropTypes.number,
retry_attempt: React.PropTypes.number, retry_attempt: PropTypes.number,
retry_at: React.PropTypes.number, retry_at: PropTypes.number,
error: React.PropTypes.shape({ error: PropTypes.shape({
operation: React.PropTypes.string, operation: PropTypes.string,
error_message: React.PropTypes.string, error_message: PropTypes.string,
}), }),
tasksStatusCounts: React.PropTypes.shape({ tasksStatusCounts: PropTypes.shape({
completed: React.PropTypes.number.isRequired, completed: PropTypes.number.isRequired,
running: React.PropTypes.number.isRequired, running: PropTypes.number.isRequired,
paused: React.PropTypes.number.isRequired, paused: PropTypes.number.isRequired,
scheduled: React.PropTypes.number.isRequired, scheduled: PropTypes.number.isRequired,
}).isRequired, }).isRequired,
latestTasks: React.PropTypes.arrayOf(TasksListDataRow.propTypes.task).isRequired, latestTasks: PropTypes.arrayOf(TasksListDataRow.propTypes.task).isRequired,
}).isRequired, }).isRequired,
}; };

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
@ -19,6 +20,11 @@ const tabs = [
label: MailPoet.I18n.t('tabSystemInfoTitle'), label: MailPoet.I18n.t('tabSystemInfoTitle'),
link: '/systemInfo', link: '/systemInfo',
}, },
{
name: 'yourPrivacy',
label: MailPoet.I18n.t('tabYourPrivacyTitle'),
link: '/yourPrivacy',
},
]; ];
function Tabs(props) { function Tabs(props) {
@ -44,7 +50,7 @@ function Tabs(props) {
); );
} }
Tabs.propTypes = { tab: React.PropTypes.string }; Tabs.propTypes = { tab: PropTypes.string };
Tabs.defaultProps = { tab: 'knowledgeBase' }; Tabs.defaultProps = { tab: 'knowledgeBase' };
module.exports = Tabs; module.exports = Tabs;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import TaskListDataRow from './tasks_list_data_row.jsx'; import TaskListDataRow from './tasks_list_data_row.jsx';
@ -30,8 +31,8 @@ const TasksList = (props) => {
}; };
TasksList.propTypes = { TasksList.propTypes = {
show_scheduled_at: React.PropTypes.bool, show_scheduled_at: PropTypes.bool,
tasks: React.PropTypes.arrayOf(TaskListDataRow.propTypes.task).isRequired, tasks: PropTypes.arrayOf(TaskListDataRow.propTypes.task).isRequired,
}; };
TasksList.defaultProps = { TasksList.defaultProps = {

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
@ -36,19 +37,19 @@ const TasksListDataRow = props => (
); );
TasksListDataRow.propTypes = { TasksListDataRow.propTypes = {
show_scheduled_at: React.PropTypes.bool, show_scheduled_at: PropTypes.bool,
task: React.PropTypes.shape({ task: PropTypes.shape({
id: React.PropTypes.number.isRequired, id: PropTypes.number.isRequired,
type: React.PropTypes.string.isRequired, type: PropTypes.string.isRequired,
priority: React.PropTypes.number.isRequired, priority: PropTypes.number.isRequired,
updated_at: React.PropTypes.number.isRequired, updated_at: PropTypes.number.isRequired,
scheduled_at: React.PropTypes.number, scheduled_at: PropTypes.number,
status: React.PropTypes.string, status: PropTypes.string,
newsletter: React.PropTypes.shape({ newsletter: PropTypes.shape({
newsletter_id: React.PropTypes.number.isRequired, newsletter_id: PropTypes.number.isRequired,
queue_id: React.PropTypes.number.isRequired, queue_id: PropTypes.number.isRequired,
preview_url: React.PropTypes.string.isRequired, preview_url: PropTypes.string.isRequired,
subject: React.PropTypes.string, subject: PropTypes.string,
}), }),
}).isRequired, }).isRequired,
}; };

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
@ -13,7 +14,7 @@ const TasksListLabelsRow = props => (
); );
TasksListLabelsRow.propTypes = { TasksListLabelsRow.propTypes = {
show_scheduled_at: React.PropTypes.bool, show_scheduled_at: PropTypes.bool,
}; };
TasksListLabelsRow.defaultProps = { TasksListLabelsRow.defaultProps = {

View File

@ -0,0 +1,20 @@
import React from 'react';
import MailPoet from 'mailpoet';
import Tabs from './tabs.jsx';
function YourPrivacy() {
return (
<div>
<Tabs tab="yourPrivacy" />
<p>{MailPoet.I18n.t('yourPrivacyContent1')}</p>
<p>{MailPoet.I18n.t('yourPrivacyContent2')}</p>
<p>{MailPoet.I18n.t('yourPrivacyContent3')}</p>
<a target="_blank" rel="noreferrer noopener" href="https://www.mailpoet.com/privacy-notice/" className="button button-primary">{MailPoet.I18n.t('yourPrivacyButton')}</a>
</div>
);
}
module.exports = YourPrivacy;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import InAppAnnouncementDot from './in_app_announcement_dot.jsx'; import InAppAnnouncementDot from './in_app_announcement_dot.jsx';
@ -78,11 +79,11 @@ const validateBooleanWithWindowDependency = (props, propName, componentName, win
}; };
InAppAnnouncement.propTypes = { InAppAnnouncement.propTypes = {
width: React.PropTypes.string, width: PropTypes.string,
height: React.PropTypes.string, height: PropTypes.string,
className: React.PropTypes.string, className: PropTypes.string,
children: React.PropTypes.element.isRequired, children: PropTypes.element.isRequired,
validUntil: React.PropTypes.instanceOf(Date), validUntil: PropTypes.instanceOf(Date),
showToNewUser: (props, propName, componentName) => ( showToNewUser: (props, propName, componentName) => (
validateBooleanWithWindowDependency(props, propName, componentName, 'mailpoet_is_new_user') validateBooleanWithWindowDependency(props, propName, componentName, 'mailpoet_is_new_user')
), ),

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from 'react-dom/server';
import classNames from 'classnames'; import classNames from 'classnames';
@ -20,11 +21,11 @@ const InAppAnnouncementDot = props => (
); );
InAppAnnouncementDot.propTypes = { InAppAnnouncementDot.propTypes = {
children: React.PropTypes.element.isRequired, children: PropTypes.element.isRequired,
width: React.PropTypes.string, width: PropTypes.string,
height: React.PropTypes.string, height: PropTypes.string,
className: React.PropTypes.string, className: PropTypes.string,
onUserTrigger: React.PropTypes.func, onUserTrigger: PropTypes.func,
}; };
InAppAnnouncementDot.defaultProps = { InAppAnnouncementDot.defaultProps = {

View File

@ -1,29 +1,26 @@
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
const ListingBulkActions = React.createClass({ class ListingBulkActions extends React.Component {
getInitialState: function getInitialState() { state = {
return { action: false,
action: false, extra: false,
extra: false, };
};
},
handleChangeAction: function handleChangeAction(e) {
this.setState({
action: e.target.value,
extra: false,
}, () => {
const action = this.getSelectedAction();
// action on select callback getSelectedAction = () => {
if (action !== null && action.onSelect !== undefined) { const selectedAction = this.action.value;
this.setState({ if (selectedAction.length > 0) {
extra: action.onSelect(e), const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
});
if (action.length > 0) {
return action[0];
} }
}); }
}, return null;
handleApplyAction: function handleApplyAction(e) { };
handleApplyAction = (e) => {
e.preventDefault(); e.preventDefault();
const action = this.getSelectedAction(); const action = this.getSelectedAction();
@ -58,19 +55,25 @@ const ListingBulkActions = React.createClass({
action: false, action: false,
extra: false, extra: false,
}); });
}, };
getSelectedAction: function getSelectedAction() {
const selectedAction = this.action.value;
if (selectedAction.length > 0) {
const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
if (action.length > 0) { handleChangeAction = (e) => {
return action[0]; this.setState({
action: e.target.value,
extra: false,
}, () => {
const action = this.getSelectedAction();
// action on select callback
if (action !== null && action.onSelect !== undefined) {
this.setState({
extra: action.onSelect(e),
});
} }
} });
return null; };
},
render: function render() { render() {
if (this.props.bulk_actions.length === 0) { if (this.props.bulk_actions.length === 0) {
return null; return null;
} }
@ -108,7 +111,17 @@ const ListingBulkActions = React.createClass({
{ this.state.extra } { this.state.extra }
</div> </div>
); );
}, }
}); }
ListingBulkActions.propTypes = {
bulk_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
selection: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]).isRequired,
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
onBulkAction: PropTypes.func.isRequired,
};
export default ListingBulkActions; export default ListingBulkActions;

View File

@ -1,32 +1,10 @@
import React from 'react'; import React from 'react';
import jQuery from 'jquery'; import jQuery from 'jquery';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
const ListingFilters = React.createClass({ class ListingFilters extends React.Component {
handleFilterAction: function handleFilterAction() { componentDidUpdate() {
const filters = {};
this.getAvailableFilters().forEach((filter, i) => {
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
});
if (this.props.onBeforeSelectFilter) {
this.props.onBeforeSelectFilter(filters);
}
return this.props.onSelectFilter(filters);
},
handleEmptyTrash: function handleEmptyTrash() {
return this.props.onEmptyTrash();
},
getAvailableFilters: function getAvailableFilters() {
const filters = this.props.filters;
return Object.keys(filters).filter(filter => !(
filters[filter].length === 0
|| (
filters[filter].length === 1
&& !filters[filter][0].value
)
));
},
componentDidUpdate: function componentDidUpdate() {
const selectedFilters = this.props.filter; const selectedFilters = this.props.filter;
this.getAvailableFilters().forEach( this.getAvailableFilters().forEach(
(filter, i) => { (filter, i) => {
@ -37,8 +15,33 @@ const ListingFilters = React.createClass({
} }
} }
); );
}, }
render: function render() {
getAvailableFilters = () => {
const filters = this.props.filters;
return Object.keys(filters).filter(filter => !(
filters[filter].length === 0
|| (
filters[filter].length === 1
&& !filters[filter][0].value
)
));
};
handleEmptyTrash = () => this.props.onEmptyTrash();
handleFilterAction = () => {
const filters = {};
this.getAvailableFilters().forEach((filter, i) => {
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
});
if (this.props.onBeforeSelectFilter) {
this.props.onBeforeSelectFilter(filters);
}
return this.props.onSelectFilter(filters);
};
render() {
const filters = this.props.filters; const filters = this.props.filters;
const availableFilters = this.getAvailableFilters() const availableFilters = this.getAvailableFilters()
.map((filter, i) => ( .map((filter, i) => (
@ -89,7 +92,23 @@ const ListingFilters = React.createClass({
{ emptyTrash } { emptyTrash }
</div> </div>
); );
}, }
}); }
ListingFilters.propTypes = {
filters: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]).isRequired,
onEmptyTrash: PropTypes.func.isRequired,
onBeforeSelectFilter: PropTypes.func,
onSelectFilter: PropTypes.func.isRequired,
filter: PropTypes.objectOf(PropTypes.string).isRequired,
group: PropTypes.string.isRequired,
};
ListingFilters.defaultProps = {
onBeforeSelectFilter: undefined,
};
export default ListingFilters; export default ListingFilters;

View File

@ -32,6 +32,7 @@ class ListingGroups extends React.Component {
data-automation-id={`filters_${group.label.replace(' ', '_').toLowerCase()}`} data-automation-id={`filters_${group.label.replace(' ', '_').toLowerCase()}`}
> >
{group.label} {group.label}
&nbsp;
<span className="count">({ parseInt(group.count, 10).toLocaleString() })</span> <span className="count">({ parseInt(group.count, 10).toLocaleString() })</span>
</a> </a>
</li> </li>

View File

@ -1,14 +1,12 @@
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import PropTypes from 'prop-types';
import ListingColumn from './listing_column.jsx';
const ListingHeader = React.createClass({ class ListingHeader extends React.Component {
handleSelectItems: function handleSelectItems() { handleSelectItems = () => this.props.onSelectItems(this.toggle.checked);
return this.props.onSelectItems(
this.toggle.checked render() {
);
},
render: function render() {
const columns = this.props.columns.map((column, index) => { const columns = this.props.columns.map((column, index) => {
const renderColumn = column; const renderColumn = column;
renderColumn.is_primary = (index === 0); renderColumn.is_primary = (index === 0);
@ -53,49 +51,26 @@ const ListingHeader = React.createClass({
{columns} {columns}
</tr> </tr>
); );
}, }
}); }
const ListingColumn = React.createClass({ ListingHeader.propTypes = {
handleSort: function handleSort() { onSelectItems: PropTypes.func.isRequired,
const sortBy = this.props.column.name; onSort: PropTypes.func.isRequired,
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc'; columns: PropTypes.arrayOf(PropTypes.object),
this.props.onSort(sortBy, sortOrder); sort_by: PropTypes.string,
}, sort_order: PropTypes.string,
render: function render() { is_selectable: PropTypes.bool.isRequired,
const classes = classNames( selection: PropTypes.oneOfType([
'manage-column', PropTypes.string,
{ 'column-primary': this.props.column.is_primary }, PropTypes.bool,
{ sortable: this.props.column.sortable }, ]).isRequired,
this.props.column.sorted, };
{ sorted: (this.props.sort_by === this.props.column.name) }
);
let label;
if (this.props.column.sortable === true) { ListingHeader.defaultProps = {
label = ( columns: [],
<a sort_by: undefined,
onClick={this.handleSort} sort_order: 'desc',
role="button" };
tabIndex={0}
>
<span>{ this.props.column.label }</span>
<span className="sorting-indicator" />
</a>
);
} else {
label = this.props.column.label;
}
return (
<th
role="columnheader"
className={classes}
id={this.props.column.name}
scope="col"
width={this.props.column.width || null}
>{label}</th>
);
},
});
module.exports = ListingHeader; module.exports = ListingHeader;

View File

@ -1,305 +1,76 @@
import MailPoet from 'mailpoet';
import jQuery from 'jquery'; import jQuery from 'jquery';
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'underscore'; import _ from 'underscore';
import { Link } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet';
import ListingBulkActions from 'listing/bulk_actions.jsx'; import ListingBulkActions from 'listing/bulk_actions.jsx';
import ListingHeader from 'listing/header.jsx'; import ListingHeader from 'listing/header.jsx';
import ListingPages from 'listing/pages.jsx'; import ListingPages from 'listing/pages.jsx';
import ListingSearch from 'listing/search.jsx'; import ListingSearch from 'listing/search.jsx';
import ListingGroups from 'listing/groups.jsx'; import ListingGroups from 'listing/groups.jsx';
import ListingFilters from 'listing/filters.jsx'; import ListingFilters from 'listing/filters.jsx';
import ListingItems from 'listing/listing_items.jsx';
const ListingItem = React.createClass({ const Listing = createReactClass({ // eslint-disable-line react/prefer-es6-class
getInitialState: function getInitialState() { displayName: 'Listing',
return {
expanded: false, /* eslint-disable react/require-default-props */
}; propTypes: {
limit: PropTypes.number,
sort_by: PropTypes.string,
sort_order: PropTypes.string,
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
auto_refresh: PropTypes.bool,
location: PropTypes.shape({
pathname: PropTypes.string,
}),
base_url: PropTypes.string,
type: PropTypes.string,
endpoint: PropTypes.string.isRequired,
afterGetItems: PropTypes.func,
messages: PropTypes.shape({
onRestore: PropTypes.func,
onTrash: PropTypes.func,
onDelete: PropTypes.func,
}),
onRenderItem: PropTypes.func.isRequired,
columns: PropTypes.arrayOf(PropTypes.object),
bulk_actions: PropTypes.arrayOf(PropTypes.object),
item_actions: PropTypes.arrayOf(PropTypes.object),
search: PropTypes.bool,
groups: PropTypes.bool,
renderExtraActions: PropTypes.func,
onBeforeSelectFilter: PropTypes.func,
getListingItemKey: PropTypes.func,
}, },
handleSelectItem: function handleSelectItem(e) { /* eslint-enable react/require-default-props */
this.props.onSelectItem(
parseInt(e.target.value, 10),
e.target.checked
);
return !e.target.checked;
},
handleRestoreItem: function handleRestoreItem(id) {
this.props.onRestoreItem(id);
},
handleTrashItem: function handleTrashItem(id) {
this.props.onTrashItem(id);
},
handleDeleteItem: function handleDeleteItem(id) {
this.props.onDeleteItem(id);
},
handleToggleItem: function handleToggleItem() {
this.setState({ expanded: !this.state.expanded });
},
render: function render() {
let checkbox = false;
if (this.props.is_selectable === true) {
checkbox = (
<th className="check-column" scope="row">
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
`Select ${this.props.item[this.props.columns[0].name]}`
}</label>
<input
type="checkbox"
value={this.props.item.id}
checked={
this.props.item.selected || this.props.selection === 'all'
}
onChange={this.handleSelectItem}
disabled={this.props.selection === 'all'}
id={`listing-row-checkbox-${this.props.item.id}`}
/>
</th>
);
}
const customActions = this.props.item_actions;
let itemActions = false;
if (customActions.length > 0) {
let isFirst = true;
itemActions = customActions
.filter(action => action.display === undefined || action.display(this.props.item))
.map((action, index) => {
let customAction = null;
if (action.name === 'trash') {
customAction = (
<span key={`action-${action.name}`} className="trash">
{(!isFirst) ? ' | ' : ''}
<a
href="javascript:;"
onClick={() => this.handleTrashItem(this.props.item.id)}
>
{MailPoet.I18n.t('moveToTrash')}
</a>
</span>
);
} else if (action.refresh) {
customAction = (
<span
onClick={this.props.onRefreshItems}
key={`action-${action.name}`}
className={action.name}
role="button"
tabIndex={index}
>
{(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) }
</span>
);
} else if (action.link) {
customAction = (
<span
key={`action-${action.name}`}
className={action.name}
>
{(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) }
</span>
);
} else {
customAction = (
<span
key={`action-${action.name}`}
className={action.name}
>
{(!isFirst) ? ' | ' : ''}
<a
href="javascript:;"
onClick={
(action.onClick !== undefined)
? () => action.onClick(this.props.item, this.props.onRefreshItems)
: false
}
>{ action.label }</a>
</span>
);
}
if (customAction !== null && isFirst === true) {
isFirst = false;
}
return customAction;
});
} else {
itemActions = (
<span className="edit">
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
</span>
);
}
let actions;
if (this.props.group === 'trash') {
actions = (
<div>
<div className="row-actions">
<span>
<a
href="javascript:;"
onClick={() => this.handleRestoreItem(this.props.item.id)}
>{MailPoet.I18n.t('restore')}</a>
</span>
{ ' | ' }
<span className="delete">
<a
className="submitdelete"
href="javascript:;"
onClick={() => this.handleDeleteItem(this.props.item.id)}
>{MailPoet.I18n.t('deletePermanently')}</a>
</span>
</div>
<button
onClick={() => this.handleToggleItem(this.props.item.id)}
className="toggle-row"
type="button"
>
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
</button>
</div>
);
} else {
actions = (
<div>
<div className="row-actions">
{ itemActions }
</div>
<button
onClick={() => this.handleToggleItem(this.props.item.id)}
className="toggle-row"
type="button"
>
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
</button>
</div>
);
}
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
return (
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
{ checkbox }
{ this.props.onRenderItem(this.props.item, actions) }
</tr>
);
},
});
const ListingItems = React.createClass({
render: function render() {
if (this.props.items.length === 0) {
let message;
if (this.props.loading === true) {
message = (this.props.messages.onLoadingItems
&& this.props.messages.onLoadingItems(this.props.group))
|| MailPoet.I18n.t('loadingItems');
} else {
message = (this.props.messages.onNoItemsFound
&& this.props.messages.onNoItemsFound(this.props.group))
|| MailPoet.I18n.t('noItemsFound');
}
return (
<tbody>
<tr className="no-items">
<td
colSpan={
this.props.columns.length
+ (this.props.is_selectable ? 1 : 0)
}
className="colspanchange"
>
{message}
</td>
</tr>
</tbody>
);
}
const selectAllClasses = classNames(
'mailpoet_select_all',
{ mailpoet_hidden: (
this.props.selection === false
|| (this.props.count <= this.props.limit)
),
}
);
return (
<tbody>
<tr className={selectAllClasses}>
<td colSpan={
this.props.columns.length
+ (this.props.is_selectable ? 1 : 0)
}
>
{
(this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLabel')
: MailPoet.I18n.t('selectedAllLabel').replace(
'%d',
this.props.count.toLocaleString()
)
}
&nbsp;
<a
onClick={this.props.onSelectAll}
href="javascript:;"
>{
(this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLink')
: MailPoet.I18n.t('clearSelection')
}</a>
</td>
</tr>
{this.props.items.map((item) => {
const renderItem = item;
renderItem.id = parseInt(item.id, 10);
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
let key = `item-${renderItem.id}-${item.id}`;
if (typeof this.props.getListingItemKey === 'function') {
key = this.props.getListingItemKey(item);
}
return (
<ListingItem
columns={this.props.columns}
onSelectItem={this.props.onSelectItem}
onRenderItem={this.props.onRenderItem}
onDeleteItem={this.props.onDeleteItem}
onRestoreItem={this.props.onRestoreItem}
onTrashItem={this.props.onTrashItem}
onRefreshItems={this.props.onRefreshItems}
selection={this.props.selection}
is_selectable={this.props.is_selectable}
item_actions={this.props.item_actions}
group={this.props.group}
key={key}
item={renderItem}
/>
);
})}
</tbody>
);
},
});
const Listing = React.createClass({
contextTypes: { contextTypes: {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, },
getDefaultProps: () => ({
limit: 10,
sort_by: null,
sort_order: undefined,
auto_refresh: true,
location: undefined,
base_url: '',
type: undefined,
afterGetItems: undefined,
messages: undefined,
columns: [],
bulk_actions: [],
item_actions: [],
search: true,
groups: true,
renderExtraActions: undefined,
onBeforeSelectFilter: undefined,
getListingItemKey: undefined,
}),
getInitialState: function getInitialState() { getInitialState: function getInitialState() {
return { return {
loading: false, loading: false,
@ -319,63 +90,28 @@ const Listing = React.createClass({
meta: {}, meta: {},
}; };
}, },
getParam: function getParam(param) {
const regex = /(.*)\[(.*)\]/;
const matches = regex.exec(param);
return [matches[1], matches[2]];
},
initWithParams: function initWithParams(params) {
const state = this.getInitialState();
// check for url params
if (params.splat) {
params.splat.split('/').forEach((param) => {
const [key, value] = this.getParam(param);
const filters = {};
switch (key) {
case 'filter':
value.split('&').forEach((pair) => {
const [k, v] = pair.split('=');
filters[k] = v;
});
state.filter = filters; componentDidMount: function componentDidMount() {
break; this.isComponentMounted = true;
default: const params = this.props.params || {};
state[key] = value; this.initWithParams(params);
}
if (this.props.auto_refresh) {
jQuery(document).on('heartbeat-tick.mailpoet', () => {
this.getItems();
}); });
} }
// limit per page
if (this.props.limit !== undefined) {
state.limit = Math.abs(Number(this.props.limit));
}
// sort by
if (state.sort_by === null && this.props.sort_by !== undefined) {
state.sort_by = this.props.sort_by;
}
// sort order
if (state.sort_order === null && this.props.sort_order !== undefined) {
state.sort_order = this.props.sort_order;
}
this.setState(state, () => {
this.getItems();
});
}, },
getParams: function getParams() {
// get all route parameters (without the "splat") componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
const params = _.omit(this.props.params, 'splat'); const params = nextProps.params || {};
// TODO: this.initWithParams(params);
// find a way to set the "type" in the routes definition
// so that it appears in `this.props.params`
if (this.props.type) {
params.type = this.props.type;
}
return params;
}, },
componentWillUnmount: function componentWillUnmount() {
this.isComponentMounted = false;
},
setParams: function setParams() { setParams: function setParams() {
if (this.props.location) { if (this.props.location) {
const params = Object.keys(this.state) const params = Object.keys(this.state)
@ -413,17 +149,19 @@ const Listing = React.createClass({
} }
} }
}, },
getUrlWithParams: function getUrlWithParams(params) { getUrlWithParams: function getUrlWithParams(params) {
let baseUrl = (this.props.base_url !== undefined) let baseUrl = (this.props.base_url !== undefined)
? this.props.base_url ? this.props.base_url
: null; : null;
if (baseUrl !== null) { if (baseUrl) {
baseUrl = this.setBaseUrlParams(baseUrl); baseUrl = this.setBaseUrlParams(baseUrl);
return `/${baseUrl}/${params}`; return `/${baseUrl}/${params}`;
} }
return `/${params}`; return `/${params}`;
}, },
setBaseUrlParams: function setBaseUrlParams(baseUrl) { setBaseUrlParams: function setBaseUrlParams(baseUrl) {
let ret = baseUrl; let ret = baseUrl;
if (ret.indexOf(':') !== -1) { if (ret.indexOf(':') !== -1) {
@ -437,24 +175,25 @@ const Listing = React.createClass({
return ret; return ret;
}, },
componentDidMount: function componentDidMount() {
this.isComponentMounted = true;
const params = this.props.params || {};
this.initWithParams(params);
if (this.props.auto_refresh) { getParams: function getParams() {
jQuery(document).on('heartbeat-tick.mailpoet', () => { // get all route parameters (without the "splat")
this.getItems(); const params = _.omit(this.props.params, 'splat');
}); // TODO:
// find a way to set the "type" in the routes definition
// so that it appears in `this.props.params`
if (this.props.type) {
params.type = this.props.type;
} }
return params;
}, },
componentWillUnmount: function componentWillUnmount() {
this.isComponentMounted = false; getParam: function getParam(param) {
}, const regex = /(.*)\[(.*)\]/;
componentWillReceiveProps: function componentWillReceiveProps(nextProps) { const matches = regex.exec(param);
const params = nextProps.params || {}; return [matches[1], matches[2]];
this.initWithParams(params);
}, },
getItems: function getItems() { getItems: function getItems() {
if (!this.isComponentMounted) return; if (!this.isComponentMounted) return;
@ -507,6 +246,49 @@ const Listing = React.createClass({
} }
}); });
}, },
initWithParams: function initWithParams(params) {
const state = this.getInitialState();
// check for url params
if (params.splat) {
params.splat.split('/').forEach((param) => {
const [key, value] = this.getParam(param);
const filters = {};
switch (key) {
case 'filter':
value.split('&').forEach((pair) => {
const [k, v] = pair.split('=');
filters[k] = v;
});
state.filter = filters;
break;
default:
state[key] = value;
}
});
}
// limit per page
if (this.props.limit !== undefined) {
state.limit = Math.abs(Number(this.props.limit));
}
// sort by
if (state.sort_by === null && this.props.sort_by !== undefined) {
state.sort_by = this.props.sort_by;
}
// sort order
if (state.sort_order === null && this.props.sort_order !== undefined) {
state.sort_order = this.props.sort_order;
}
this.setState(state, () => {
this.getItems();
});
},
handleRestoreItem: function handleRestoreItem(id) { handleRestoreItem: function handleRestoreItem(id) {
this.setState({ this.setState({
loading: true, loading: true,
@ -535,6 +317,7 @@ const Listing = React.createClass({
); );
}); });
}, },
handleTrashItem: function handleTrashItem(id) { handleTrashItem: function handleTrashItem(id) {
this.setState({ this.setState({
loading: true, loading: true,
@ -563,6 +346,7 @@ const Listing = React.createClass({
); );
}); });
}, },
handleDeleteItem: function handleDeleteItem(id) { handleDeleteItem: function handleDeleteItem(id) {
this.setState({ this.setState({
loading: true, loading: true,
@ -591,6 +375,7 @@ const Listing = React.createClass({
); );
}); });
}, },
handleEmptyTrash: function handleEmptyTrash() { handleEmptyTrash: function handleEmptyTrash() {
return this.handleBulkAction('all', { return this.handleBulkAction('all', {
action: 'delete', action: 'delete',
@ -611,6 +396,7 @@ const Listing = React.createClass({
); );
}); });
}, },
handleBulkAction: function handleBulkAction(selectedIds, params) { handleBulkAction: function handleBulkAction(selectedIds, params) {
if ( if (
this.state.selection === false this.state.selection === false
@ -651,6 +437,7 @@ const Listing = React.createClass({
} }
}); });
}, },
handleSearch: function handleSearch(search) { handleSearch: function handleSearch(search) {
this.setState({ this.setState({
search, search,
@ -661,6 +448,7 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleSort: function handleSort(sortBy, sortOrder = 'asc') { handleSort: function handleSort(sortBy, sortOrder = 'asc') {
this.setState({ this.setState({
sort_by: sortBy, sort_by: sortBy,
@ -669,6 +457,7 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleSelectItem: function handleSelectItem(id, isChecked) { handleSelectItem: function handleSelectItem(id, isChecked) {
let selectedIds = this.state.selected_ids; let selectedIds = this.state.selected_ids;
let selection = false; let selection = false;
@ -690,6 +479,7 @@ const Listing = React.createClass({
selected_ids: selectedIds, selected_ids: selectedIds,
}); });
}, },
handleSelectItems: function handleSelectItems(isChecked) { handleSelectItems: function handleSelectItems(isChecked) {
if (isChecked === false) { if (isChecked === false) {
this.clearSelection(); this.clearSelection();
@ -702,6 +492,7 @@ const Listing = React.createClass({
}); });
} }
}, },
handleSelectAll: function handleSelectAll() { handleSelectAll: function handleSelectAll() {
if (this.state.selection === 'all') { if (this.state.selection === 'all') {
this.clearSelection(); this.clearSelection();
@ -712,12 +503,14 @@ const Listing = React.createClass({
}); });
} }
}, },
clearSelection: function clearSelection() { clearSelection: function clearSelection() {
this.setState({ this.setState({
selection: false, selection: false,
selected_ids: [], selected_ids: [],
}); });
}, },
handleFilter: function handleFilter(filters) { handleFilter: function handleFilter(filters) {
this.setState({ this.setState({
filter: filters, filter: filters,
@ -726,6 +519,7 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleGroup: function handleGroup(group) { handleGroup: function handleGroup(group) {
// reset search // reset search
jQuery('#search_input').val(''); jQuery('#search_input').val('');
@ -739,6 +533,7 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleSetPage: function handleSetPage(page) { handleSetPage: function handleSetPage(page) {
this.setState({ this.setState({
page, page,
@ -748,13 +543,16 @@ const Listing = React.createClass({
this.setParams(); this.setParams();
}); });
}, },
handleRenderItem: function handleRenderItem(item, actions) { handleRenderItem: function handleRenderItem(item, actions) {
const render = this.props.onRenderItem(item, actions, this.state.meta); const render = this.props.onRenderItem(item, actions, this.state.meta);
return render.props.children; return render.props.children;
}, },
handleRefreshItems: function handleRefreshItems() { handleRefreshItems: function handleRefreshItems() {
this.getItems(); this.getItems();
}, },
render: function render() { render: function render() {
const items = this.state.items; const items = this.state.items;
const sortBy = this.state.sort_by; const sortBy = this.state.sort_by;

View File

@ -0,0 +1,68 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
class ListingColumn extends React.Component {
handleSort = () => {
const sortBy = this.props.column.name;
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
this.props.onSort(sortBy, sortOrder);
};
render() {
const classes = classNames(
'manage-column',
{ 'column-primary': this.props.column.is_primary },
{ sortable: this.props.column.sortable },
this.props.column.sorted,
{ sorted: (this.props.sort_by === this.props.column.name) }
);
let label;
if (this.props.column.sortable === true) {
label = (
<a
onClick={this.handleSort}
role="button"
tabIndex={0}
>
<span>{ this.props.column.label }</span>
<span className="sorting-indicator" />
</a>
);
} else {
label = this.props.column.label;
}
return (
<th
role="columnheader"
className={classes}
id={this.props.column.name}
scope="col"
width={this.props.column.width || null}
>{label}</th>
);
}
}
ListingColumn.propTypes = {
column: PropTypes.shape({
name: PropTypes.string,
sorted: PropTypes.string,
is_primary: PropTypes.bool,
sortable: PropTypes.bool,
label: PropTypes.string,
width: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
}).isRequired,
sort_by: PropTypes.string,
onSort: PropTypes.func.isRequired,
};
ListingColumn.defaultProps = {
sort_by: undefined,
};
module.exports = ListingColumn;

View File

@ -0,0 +1,215 @@
import React from 'react';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet';
import { Link } from 'react-router';
import classNames from 'classnames';
class ListingItem extends React.Component {
state = {
expanded: false,
};
handleSelectItem = (e) => {
this.props.onSelectItem(
parseInt(e.target.value, 10),
e.target.checked
);
return !e.target.checked;
};
handleRestoreItem = (id) => {
this.props.onRestoreItem(id);
};
handleTrashItem = (id) => {
this.props.onTrashItem(id);
};
handleDeleteItem = (id) => {
this.props.onDeleteItem(id);
};
handleToggleItem = () => {
this.setState({ expanded: !this.state.expanded });
};
render() {
let checkbox = false;
if (this.props.is_selectable === true) {
checkbox = (
<th className="check-column" scope="row">
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
`Select ${this.props.item[this.props.columns[0].name]}`
}</label>
<input
type="checkbox"
value={this.props.item.id}
checked={
this.props.item.selected || this.props.selection === 'all'
}
onChange={this.handleSelectItem}
disabled={this.props.selection === 'all'}
id={`listing-row-checkbox-${this.props.item.id}`}
/>
</th>
);
}
const customActions = this.props.item_actions;
let itemActions = false;
if (customActions.length > 0) {
let isFirst = true;
itemActions = customActions
.filter(action => action.display === undefined || action.display(this.props.item))
.map((action, index) => {
let customAction = null;
if (action.name === 'trash') {
customAction = (
<span key={`action-${action.name}`} className="trash">
{(!isFirst) ? ' | ' : ''}
<a
href="javascript:;"
onClick={() => this.handleTrashItem(this.props.item.id)}
>
{MailPoet.I18n.t('moveToTrash')}
</a>
</span>
);
} else if (action.refresh) {
customAction = (
<span
onClick={this.props.onRefreshItems}
key={`action-${action.name}`}
className={action.name}
role="button"
tabIndex={index}
>
{(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) }
</span>
);
} else if (action.link) {
customAction = (
<span
key={`action-${action.name}`}
className={action.name}
>
{(!isFirst) ? ' | ' : ''}
{ action.link(this.props.item) }
</span>
);
} else {
customAction = (
<span
key={`action-${action.name}`}
className={action.name}
>
{(!isFirst) ? ' | ' : ''}
<a
href="javascript:;"
onClick={
(action.onClick !== undefined)
? () => action.onClick(this.props.item, this.props.onRefreshItems)
: false
}
>{ action.label }</a>
</span>
);
}
if (customAction !== null && isFirst === true) {
isFirst = false;
}
return customAction;
});
} else {
itemActions = (
<span className="edit">
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
</span>
);
}
let actions;
if (this.props.group === 'trash') {
actions = (
<div>
<div className="row-actions">
<span>
<a
href="javascript:;"
onClick={() => this.handleRestoreItem(this.props.item.id)}
>{MailPoet.I18n.t('restore')}</a>
</span>
{ ' | ' }
<span className="delete">
<a
className="submitdelete"
href="javascript:;"
onClick={() => this.handleDeleteItem(this.props.item.id)}
>{MailPoet.I18n.t('deletePermanently')}</a>
</span>
</div>
<button
onClick={() => this.handleToggleItem(this.props.item.id)}
className="toggle-row"
type="button"
>
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
</button>
</div>
);
} else {
actions = (
<div>
<div className="row-actions">
{ itemActions }
</div>
<button
onClick={() => this.handleToggleItem(this.props.item.id)}
className="toggle-row"
type="button"
>
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
</button>
</div>
);
}
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
return (
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
{ checkbox }
{ this.props.onRenderItem(this.props.item, actions) }
</tr>
);
}
}
ListingItem.propTypes = {
onSelectItem: PropTypes.func.isRequired,
onRestoreItem: PropTypes.func.isRequired,
onTrashItem: PropTypes.func.isRequired,
onDeleteItem: PropTypes.func.isRequired,
is_selectable: PropTypes.bool.isRequired,
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selection: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
onRefreshItems: PropTypes.func.isRequired,
onRenderItem: PropTypes.func.isRequired,
group: PropTypes.string.isRequired,
};
module.exports = ListingItem;

View File

@ -0,0 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import MailPoet from 'mailpoet';
import ListingItem from 'listing/listing_item.jsx';
class ListingItems extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
render() {
if (this.props.items.length === 0) {
let message;
if (this.props.loading === true) {
message = (this.props.messages.onLoadingItems
&& this.props.messages.onLoadingItems(this.props.group))
|| MailPoet.I18n.t('loadingItems');
} else {
message = (this.props.messages.onNoItemsFound
&& this.props.messages.onNoItemsFound(this.props.group))
|| MailPoet.I18n.t('noItemsFound');
}
return (
<tbody>
<tr className="no-items">
<td
colSpan={
this.props.columns.length
+ (this.props.is_selectable ? 1 : 0)
}
className="colspanchange"
>
{message}
</td>
</tr>
</tbody>
);
}
const selectAllClasses = classNames(
'mailpoet_select_all',
{ mailpoet_hidden: (
this.props.selection === false
|| (this.props.count <= this.props.limit)
),
}
);
return (
<tbody>
<tr className={selectAllClasses}>
<td colSpan={
this.props.columns.length
+ (this.props.is_selectable ? 1 : 0)
}
>
{
(this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLabel')
: MailPoet.I18n.t('selectedAllLabel').replace(
'%d',
this.props.count.toLocaleString()
)
}
&nbsp;
<a
onClick={this.props.onSelectAll}
href="javascript:;"
>{
(this.props.selection !== 'all')
? MailPoet.I18n.t('selectAllLink')
: MailPoet.I18n.t('clearSelection')
}</a>
</td>
</tr>
{this.props.items.map((item) => {
const renderItem = item;
renderItem.id = parseInt(item.id, 10);
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
let key = `item-${renderItem.id}-${item.id}`;
if (typeof this.props.getListingItemKey === 'function') {
key = this.props.getListingItemKey(item);
}
return (
<ListingItem
columns={this.props.columns}
onSelectItem={this.props.onSelectItem}
onRenderItem={this.props.onRenderItem}
onDeleteItem={this.props.onDeleteItem}
onRestoreItem={this.props.onRestoreItem}
onTrashItem={this.props.onTrashItem}
onRefreshItems={this.props.onRefreshItems}
selection={this.props.selection}
is_selectable={this.props.is_selectable}
item_actions={this.props.item_actions}
group={this.props.group}
key={key}
item={renderItem}
/>
);
})}
</tbody>
);
}
}
ListingItems.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
loading: PropTypes.bool.isRequired,
messages: PropTypes.shape({
onLoadingItems: PropTypes.func,
onNoItemsFound: PropTypes.func,
}).isRequired,
group: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
is_selectable: PropTypes.bool.isRequired,
selection: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
count: PropTypes.number.isRequired,
limit: PropTypes.number.isRequired,
onSelectAll: PropTypes.func.isRequired,
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
getListingItemKey: PropTypes.func,
onSelectItem: PropTypes.func.isRequired,
onRenderItem: PropTypes.func.isRequired,
onDeleteItem: PropTypes.func.isRequired,
onRestoreItem: PropTypes.func.isRequired,
onTrashItem: PropTypes.func.isRequired,
onRefreshItems: PropTypes.func.isRequired,
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
};
ListingItems.defaultProps = {
getListingItemKey: undefined,
};
module.exports = ListingItems;

View File

@ -1,56 +1,62 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
const ListingPages = React.createClass({ class ListingPages extends React.Component {
getInitialState: function getInitialState() { state = {
return { page: null,
page: null, };
};
}, setPage = (page) => {
setPage: function setPage(page) {
this.setState({ this.setState({
page: null, page: null,
}, () => { }, () => {
this.props.onSetPage(this.constrainPage(page)); this.props.onSetPage(this.constrainPage(page));
}); });
}, };
setFirstPage: function setFirstPage() {
setFirstPage = () => {
this.setPage(1); this.setPage(1);
}, };
setLastPage: function setLastPage() {
setLastPage = () => {
this.setPage(this.getLastPage()); this.setPage(this.getLastPage());
}, };
setPreviousPage: function setPreviousPage() {
setPreviousPage = () => {
this.setPage(this.constrainPage( this.setPage(this.constrainPage(
parseInt(this.props.page, 10) - 1) parseInt(this.props.page, 10) - 1)
); );
}, };
setNextPage: function setNextPage() {
setNextPage = () => {
this.setPage(this.constrainPage( this.setPage(this.constrainPage(
parseInt(this.props.page, 10) + 1) parseInt(this.props.page, 10) + 1)
); );
}, };
constrainPage: function constrainPage(page) {
return Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage()); getLastPage = () => Math.ceil(this.props.count / this.props.limit);
},
handleSetManualPage: function handleSetManualPage(e) { handleSetManualPage = (e) => {
if (e.which === 13) { if (e.which === 13) {
this.setPage(this.state.page); this.setPage(this.state.page);
} }
}, };
handleChangeManualPage: function handleChangeManualPage(e) {
handleChangeManualPage = (e) => {
this.setState({ this.setState({
page: e.target.value, page: e.target.value,
}); });
}, };
handleBlurManualPage: function handleBlurManualPage(e) {
handleBlurManualPage = (e) => {
this.setPage(e.target.value); this.setPage(e.target.value);
}, };
getLastPage: function getLastPage() {
return Math.ceil(this.props.count / this.props.limit); constrainPage = page => Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
},
render: function render() { render() {
if (this.props.count === 0) { if (this.props.count === 0) {
return false; return false;
} }
@ -181,7 +187,17 @@ const ListingPages = React.createClass({
{ pagination } { pagination }
</div> </div>
); );
}, }
}); }
ListingPages.propTypes = {
onSetPage: PropTypes.func.isRequired,
page: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]).isRequired,
count: PropTypes.number.isRequired,
limit: PropTypes.number.isRequired,
};
module.exports = ListingPages; module.exports = ListingPages;

13
assets/js/src/loading.jsx Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
function Loading() {
return (
<div className="mailpoet_loading">
<div className="mailpoet_modal_loading mailpoet_modal_loading_1" />
<div className="mailpoet_modal_loading mailpoet_modal_loading_2" />
<div className="mailpoet_modal_loading mailpoet_modal_loading_3" />
</div>
);
}
module.exports = Loading;

View File

@ -87,7 +87,7 @@ define('modal', ['mailpoet', 'jquery'],
'<div class="mailpoet_popup_body clearfix"></div>' + '<div class="mailpoet_popup_body clearfix"></div>' +
'</div>' + '</div>' +
'</div>', '</div>',
loading: '<div id="mailpoet_loading" style="display:none;">' + loading: '<div id="mailpoet_loading" class="mailpoet_loading" style="display:none;">' +
'<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>' + '<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>' +
'<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>' + '<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>' +
'<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>' + '<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>' +

View File

@ -33,9 +33,9 @@ define('mp2migrator', ['mailpoet', 'jquery'], function (mp, jQuery) {
jQuery('#logger').html(''); jQuery('#logger').html('');
result.split('\n').forEach(function (resultRow) { result.split('\n').forEach(function (resultRow) {
var row = resultRow; var row = resultRow;
if (row.substr(0, 7) === '[ERROR]' || row.substr(0, 9) === '[WARNING]' || row === MailPoet.I18n.t('import_stopped_by_user')) { if (row.substr(0, 7) === '[ERROR]' || row.substr(0, 9) === '[WARNING]' || row.toLowerCase() === MailPoet.I18n.t('import_stopped_by_user').toLowerCase()) {
row = '<span class="error_msg">' + row + '</span>'; // Mark the errors in red row = '<span class="error_msg">' + row + '</span>'; // Mark the errors in red
} else if (row === MailPoet.I18n.t('import_complete')) { // Test if the import is complete } else if (row.toLowerCase() === MailPoet.I18n.t('import_complete').toLowerCase()) { // Test if the import is complete
jQuery('#import-actions').hide(); jQuery('#import-actions').hide();
jQuery('#upgrade-completed').show(); jQuery('#upgrade-completed').show();
} }

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import InAppAnnouncement from 'in_app_announcements/in_app_announcement.jsx'; import InAppAnnouncement from 'in_app_announcements/in_app_announcement.jsx';
@ -19,8 +20,8 @@ const BackgroundImageAnnouncement = props => (
); );
BackgroundImageAnnouncement.propTypes = { BackgroundImageAnnouncement.propTypes = {
username: React.PropTypes.string.isRequired, username: PropTypes.string.isRequired,
videoUrl: React.PropTypes.string.isRequired, videoUrl: PropTypes.string.isRequired,
}; };
module.exports = BackgroundImageAnnouncement; module.exports = BackgroundImageAnnouncement;

View File

@ -39,6 +39,7 @@ define([
defaults: function () { defaults: function () {
return this._getDefaults({ return this._getDefaults({
type: 'container', type: 'container',
columnLayout: false,
orientation: 'vertical', orientation: 'vertical',
image: { image: {
src: null, src: null,
@ -163,6 +164,8 @@ define([
this.renderOptions = _.defaults(options.renderOptions || {}, {}); this.renderOptions = _.defaults(options.renderOptions || {}, {});
}, },
onRender: function () { onRender: function () {
var classIrregular = '';
var columnLayout;
this.toolsView = new Module.ContainerBlockToolsView({ this.toolsView = new Module.ContainerBlockToolsView({
model: this.model, model: this.model,
tools: { tools: {
@ -183,7 +186,13 @@ define([
// Sets child container orientation HTML class here, // Sets child container orientation HTML class here,
// as child CollectionView won't have access to model // as child CollectionView won't have access to model
// and will overwrite existing region element instead // and will overwrite existing region element instead
this.$('> .mailpoet_container').attr('class', 'mailpoet_container mailpoet_container_' + this.model.get('orientation')); columnLayout = this.model.get('columnLayout');
if (typeof columnLayout === 'string') {
classIrregular = 'mailpoet_irregular_width_contents_container column_layout_' + columnLayout;
}
this.$('> .mailpoet_container').attr('class',
'mailpoet_container mailpoet_container_' + this.model.get('orientation') + ' ' + classIrregular
);
}, },
showTools: function () { showTools: function () {
if (this.renderOptions.depth === 1 && !this.$el.hasClass('mailpoet_container_layer_active')) { if (this.renderOptions.depth === 1 && !this.$el.hasClass('mailpoet_container_layer_active')) {
@ -353,6 +362,48 @@ define([
} }
}); });
Module.TwoColumn12ContainerWidgetView = base.WidgetView.extend({
className: base.WidgetView.prototype.className + ' mailpoet_droppable_layout_block',
getTemplate: function () { return window.templates.twoColumn12LayoutInsertion; },
behaviors: {
DraggableBehavior: {
cloneOriginal: true,
drop: function () {
var block = new Module.ContainerBlockModel({
orientation: 'horizontal',
blocks: [
new Module.ContainerBlockModel(),
new Module.ContainerBlockModel()
]
});
block.set('columnLayout', '1_2');
return block;
}
}
}
});
Module.TwoColumn21ContainerWidgetView = base.WidgetView.extend({
className: base.WidgetView.prototype.className + ' mailpoet_droppable_layout_block',
getTemplate: function () { return window.templates.twoColumn21LayoutInsertion; },
behaviors: {
DraggableBehavior: {
cloneOriginal: true,
drop: function () {
var block = new Module.ContainerBlockModel({
orientation: 'horizontal',
blocks: [
new Module.ContainerBlockModel(),
new Module.ContainerBlockModel()
]
});
block.set('columnLayout', '2_1');
return block;
}
}
}
});
App.on('before:start', function (BeforeStartApp) { App.on('before:start', function (BeforeStartApp) {
BeforeStartApp.registerBlockType('container', { BeforeStartApp.registerBlockType('container', {
blockModel: Module.ContainerBlockModel, blockModel: Module.ContainerBlockModel,
@ -376,6 +427,18 @@ define([
priority: 100, priority: 100,
widgetView: Module.ThreeColumnContainerWidgetView widgetView: Module.ThreeColumnContainerWidgetView
}); });
BeforeStartApp.registerLayoutWidget({
name: 'twoColumn12Layout',
priority: 100,
widgetView: Module.TwoColumn12ContainerWidgetView
});
BeforeStartApp.registerLayoutWidget({
name: 'twoColumn21Layout',
priority: 100,
widgetView: Module.TwoColumn21ContainerWidgetView
});
}); });
return Module; return Module;

View File

@ -266,7 +266,6 @@ define([
}, },
validateNewsletter: function (jsonObject) { validateNewsletter: function (jsonObject) {
var body = ''; var body = '';
var contents;
if (!App._contentContainer.isValid()) { if (!App._contentContainer.isValid()) {
this.showValidationError(App._contentContainer.validationError); this.showValidationError(App._contentContainer.validationError);
return; return;
@ -282,10 +281,9 @@ define([
return; return;
} }
contents = JSON.stringify(jsonObject);
if ((App.getNewsletter().get('type') === 'notification') && if ((App.getNewsletter().get('type') === 'notification') &&
contents.indexOf('"type":"automatedLatestContent"') < 0 && body.indexOf('"type":"automatedLatestContent"') < 0 &&
contents.indexOf('"type":"automatedLatestContentLayout"') < 0 body.indexOf('"type":"automatedLatestContentLayout"') < 0
) { ) {
this.showValidationError(MailPoet.I18n.t('automatedLatestContentMissing')); this.showValidationError(MailPoet.I18n.t('automatedLatestContentMissing'));
return; return;

View File

@ -10,8 +10,7 @@ const displayTutorial = () => {
return; return;
} }
MailPoet.Modal.popup({ MailPoet.Modal.popup({
title: MailPoet.I18n.t('tutorialVideoTitle'), template: `<div class="mailpoet_drag_and_drop_tutorial"><h2>${MailPoet.I18n.t('tutorialVideoTitle')}</h2><video style="height:640px;" src="${window.config.dragDemoUrl}" controls autoplay></video></div>`,
template: `<video style="height:640px;" src="${window.config.dragDemoUrl}" controls autoplay></video>`,
onCancel: () => { onCancel: () => {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router'; import { Link } from 'react-router';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
const Breadcrumb = React.createClass({ class Breadcrumb extends React.Component {
getInitialState: function getInitialState() { constructor(props) {
const steps = this.props.steps || [ super(props);
const steps = props.steps || [
{ {
name: 'type', name: 'type',
label: MailPoet.I18n.t('selectType'), label: MailPoet.I18n.t('selectType'),
@ -24,12 +26,14 @@ const Breadcrumb = React.createClass({
label: MailPoet.I18n.t('send'), label: MailPoet.I18n.t('send'),
}, },
]; ];
return {
this.state = {
step: null, step: null,
steps, steps,
}; };
}, }
render: function render() {
render() {
const steps = this.state.steps.map((step, index) => { const steps = this.state.steps.map((step, index) => {
const stepClasses = classNames( const stepClasses = classNames(
{ mailpoet_current: (this.props.step === step.name) } { mailpoet_current: (this.props.step === step.name) }
@ -58,8 +62,17 @@ const Breadcrumb = React.createClass({
{ steps } { steps }
</p> </p>
); );
}, }
}); }
Breadcrumb.propTypes = {
steps: PropTypes.arrayOf(PropTypes.object),
step: PropTypes.string,
};
Breadcrumb.defaultProps = {
steps: undefined,
step: null,
};
module.exports = Breadcrumb; module.exports = Breadcrumb;

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import { Link } from 'react-router'; import { Link } from 'react-router';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
import ListingTabs from 'newsletters/listings/tabs.jsx'; import ListingTabs from 'newsletters/listings/tabs.jsx';
@ -157,8 +159,17 @@ const newsletterActions = [
}, },
]; ];
const NewsletterListNotification = React.createClass({ const NewsletterListNotification = createReactClass({ // eslint-disable-line react/prefer-es6-class
displayName: 'NewsletterListNotification',
propTypes: {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
},
mixins: [MailerMixin, CronMixin], mixins: [MailerMixin, CronMixin],
updateStatus: function updateStatus(e) { updateStatus: function updateStatus(e) {
// make the event persist so that we can still override the selected value // make the event persist so that we can still override the selected value
// in the ajax callback // in the ajax callback
@ -185,6 +196,7 @@ const NewsletterListNotification = React.createClass({
e.target.value = response.status; e.target.value = response.status;
}); });
}, },
renderStatus: function renderStatus(newsletter) { renderStatus: function renderStatus(newsletter) {
return ( return (
<select <select
@ -197,6 +209,7 @@ const NewsletterListNotification = React.createClass({
</select> </select>
); );
}, },
renderSettings: function renderSettings(newsletter) { renderSettings: function renderSettings(newsletter) {
let sendingFrequency; let sendingFrequency;
@ -265,6 +278,7 @@ const NewsletterListNotification = React.createClass({
</span> </span>
); );
}, },
renderHistoryLink: function renderHistoryLink(newsletter) { renderHistoryLink: function renderHistoryLink(newsletter) {
const childrenCount = Number((newsletter.children_count)); const childrenCount = Number((newsletter.children_count));
if (childrenCount === 0) { if (childrenCount === 0) {
@ -274,10 +288,12 @@ const NewsletterListNotification = React.createClass({
} }
return ( return (
<Link <Link
data-automation-id={`history-${newsletter.id}`}
to={`/notification/history/${newsletter.id}`} to={`/notification/history/${newsletter.id}`}
>{ MailPoet.I18n.t('viewHistory') }</Link> >{ MailPoet.I18n.t('viewHistory') }</Link>
); );
}, },
renderItem: function renderItem(newsletter, actions) { renderItem: function renderItem(newsletter, actions) {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
@ -311,6 +327,7 @@ const NewsletterListNotification = React.createClass({
</div> </div>
); );
}, },
render: function render() { render: function render() {
return ( return (
<div> <div>

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import { Link } from 'react-router'; import { Link } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
import ListingTabs from 'newsletters/listings/tabs.jsx'; import ListingTabs from 'newsletters/listings/tabs.jsx';
@ -57,8 +59,16 @@ let newsletterActions = [
Hooks.addFilter('mailpoet_newsletters_listings_notification_history_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_notification_history_actions', StatisticsMixin.addStatsCTAAction);
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletterActions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletterActions);
const NewsletterListNotificationHistory = React.createClass({ const NewsletterListNotificationHistory = createReactClass({ // eslint-disable-line react/prefer-es6-class, max-len
displayName: 'NewsletterListNotificationHistory',
propTypes: {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
},
mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin], mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin],
renderItem: function renderItem(newsletter, actions, meta) { renderItem: function renderItem(newsletter, actions, meta) {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
@ -96,6 +106,7 @@ const NewsletterListNotificationHistory = React.createClass({
</div> </div>
); );
}, },
render: function render() { render: function render() {
return ( return (
<div> <div>

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import { confirmAlert } from 'react-confirm-alert'; import { confirmAlert } from 'react-confirm-alert';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
import ListingTabs from 'newsletters/listings/tabs.jsx'; import ListingTabs from 'newsletters/listings/tabs.jsx';
@ -172,8 +174,16 @@ let newsletterActions = [
Hooks.addFilter('mailpoet_newsletters_listings_standard_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_standard_actions', StatisticsMixin.addStatsCTAAction);
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletterActions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletterActions);
const NewsletterListStandard = React.createClass({ const NewsletterListStandard = createReactClass({ // eslint-disable-line react/prefer-es6-class
displayName: 'NewsletterListStandard',
propTypes: {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
},
mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin], mixins: [QueueMixin, StatisticsMixin, MailerMixin, CronMixin],
renderItem: function renderItem(newsletter, actions, meta) { renderItem: function renderItem(newsletter, actions, meta) {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
@ -212,6 +222,7 @@ const NewsletterListStandard = React.createClass({
</div> </div>
); );
}, },
render: function render() { render: function render() {
return ( return (
<div> <div>

View File

@ -3,30 +3,30 @@ import { Link } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import PropTypes from 'prop-types';
class ListingTabs extends React.Component {
state = {
tab: null,
tabs: Hooks.applyFilters('mailpoet_newsletters_listings_tabs', [
{
name: 'standard',
label: MailPoet.I18n.t('tabStandardTitle'),
link: '/standard',
},
{
name: 'welcome',
label: MailPoet.I18n.t('tabWelcomeTitle'),
link: '/welcome',
},
{
name: 'notification',
label: MailPoet.I18n.t('tabNotificationTitle'),
link: '/notification',
},
]),
};
const ListingTabs = React.createClass({
getInitialState() {
return {
tab: null,
tabs: Hooks.applyFilters('mailpoet_newsletters_listings_tabs', [
{
name: 'standard',
label: MailPoet.I18n.t('tabStandardTitle'),
link: '/standard',
},
{
name: 'welcome',
label: MailPoet.I18n.t('tabWelcomeTitle'),
link: '/welcome',
},
{
name: 'notification',
label: MailPoet.I18n.t('tabNotificationTitle'),
link: '/notification',
},
]),
};
},
render() { render() {
const tabs = this.state.tabs.map((tab) => { const tabs = this.state.tabs.map((tab) => {
const tabClasses = classNames( const tabClasses = classNames(
@ -38,6 +38,7 @@ const ListingTabs = React.createClass({
<Link <Link
key={`tab-${tab.label}`} key={`tab-${tab.label}`}
className={tabClasses} className={tabClasses}
data-automation-id={`tab-${tab.label}`}
to={tab.link} to={tab.link}
onClick={() => MailPoet.trackEvent(`Tab Emails > ${tab.name} clicked`, onClick={() => MailPoet.trackEvent(`Tab Emails > ${tab.name} clicked`,
{ 'MailPoet Free version': window.mailpoet_version } { 'MailPoet Free version': window.mailpoet_version }
@ -51,7 +52,11 @@ const ListingTabs = React.createClass({
{ tabs } { tabs }
</h2> </h2>
); );
}, }
}); }
ListingTabs.propTypes = {
tab: PropTypes.string.isRequired,
};
module.exports = ListingTabs; module.exports = ListingTabs;

View File

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
import ListingTabs from 'newsletters/listings/tabs.jsx'; import ListingTabs from 'newsletters/listings/tabs.jsx';
@ -153,8 +155,16 @@ let newsletterActions = [
Hooks.addFilter('mailpoet_newsletters_listings_welcome_notification_actions', StatisticsMixin.addStatsCTAAction); Hooks.addFilter('mailpoet_newsletters_listings_welcome_notification_actions', StatisticsMixin.addStatsCTAAction);
newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletterActions); newsletterActions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletterActions);
const NewsletterListWelcome = React.createClass({ const NewsletterListWelcome = createReactClass({ // eslint-disable-line react/prefer-es6-class
displayName: 'NewsletterListWelcome',
propTypes: {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
},
mixins: [StatisticsMixin, MailerMixin, CronMixin], mixins: [StatisticsMixin, MailerMixin, CronMixin],
updateStatus: function updateStatus(e) { updateStatus: function updateStatus(e) {
// make the event persist so that we can still override the selected value // make the event persist so that we can still override the selected value
// in the ajax callback // in the ajax callback
@ -181,6 +191,7 @@ const NewsletterListWelcome = React.createClass({
e.target.value = response.status; e.target.value = response.status;
}); });
}, },
renderStatus: function renderStatus(newsletter) { renderStatus: function renderStatus(newsletter) {
const totalSent = (parseInt(newsletter.total_sent, 10)) ? const totalSent = (parseInt(newsletter.total_sent, 10)) ?
MailPoet.I18n.t('sentToXSubscribers') MailPoet.I18n.t('sentToXSubscribers')
@ -203,6 +214,7 @@ const NewsletterListWelcome = React.createClass({
</div> </div>
); );
}, },
renderSettings: function renderSettings(newsletter) { renderSettings: function renderSettings(newsletter) {
let sendingEvent; let sendingEvent;
let sendingDelay; let sendingDelay;
@ -278,6 +290,7 @@ const NewsletterListWelcome = React.createClass({
</span> </span>
); );
}, },
renderItem: function renderItem(newsletter, actions) { renderItem: function renderItem(newsletter, actions) {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
@ -316,6 +329,7 @@ const NewsletterListWelcome = React.createClass({
</div> </div>
); );
}, },
render: function render() { render: function render() {
return ( return (
<div> <div>

View File

@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Router, Route, IndexRedirect, useRouterHistory } from 'react-router'; import { IndexRedirect, Route, Router, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import _ from 'underscore'; import _ from 'underscore';
import PropTypes from 'prop-types';
import NewsletterTypes from 'newsletters/types.jsx'; import NewsletterTypes from 'newsletters/types.jsx';
import NewsletterTemplates from 'newsletters/templates.jsx'; import NewsletterTemplates from 'newsletters/templates.jsx';
import NewsletterSend from 'newsletters/send.jsx'; import NewsletterSend from 'newsletters/send.jsx';
import NewsletterCongratulate from 'newsletters/send/congratulate/congratulate.jsx';
import NewsletterTypeStandard from 'newsletters/types/standard.jsx'; import NewsletterTypeStandard from 'newsletters/types/standard.jsx';
import NewsletterTypeNotification from 'newsletters/types/notification/notification.jsx'; import NewsletterTypeNotification from 'newsletters/types/notification/notification.jsx';
import NewsletterTypeWelcome from 'newsletters/types/welcome/welcome.jsx'; import NewsletterTypeWelcome from 'newsletters/types/welcome/welcome.jsx';
@ -19,11 +21,15 @@ import NewsletterListNotificationHistory from 'newsletters/listings/notification
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({ class App extends React.Component {
render() { render() {
return this.props.children; return this.props.children;
}, }
}); }
App.propTypes = {
children: PropTypes.element.isRequired,
};
const container = document.getElementById('newsletters_container'); const container = document.getElementById('newsletters_container');
@ -84,6 +90,11 @@ if (container) {
path: 'template/:id', path: 'template/:id',
component: NewsletterTemplates, component: NewsletterTemplates,
}, },
/* congratulate */
{
path: 'send/congratulate/:id',
component: NewsletterCongratulate,
},
/* Sending options */ /* Sending options */
{ {
path: 'send/:id', path: 'send/:id',
@ -93,7 +104,7 @@ if (container) {
routes = Hooks.applyFilters('mailpoet_newsletters_before_router', [...routes, ...getAutomaticEmailsRoutes()]); routes = Hooks.applyFilters('mailpoet_newsletters_before_router', [...routes, ...getAutomaticEmailsRoutes()]);
const mailpoetListing = ReactDOM.render(( // eslint-disable-line react/no-render-return-value window.mailpoet_listing = ReactDOM.render(( // eslint-disable-line react/no-render-return-value
<Router history={history}> <Router history={history}>
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRedirect to="standard" /> <IndexRedirect to="standard" />
@ -110,6 +121,4 @@ if (container) {
</Route> </Route>
</Router> </Router>
), container); ), container);
window.mailpoet_listing = mailpoetListing;
} }

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import _ from 'underscore'; import _ from 'underscore';
import Breadcrumb from 'newsletters/breadcrumb.jsx'; import Breadcrumb from 'newsletters/breadcrumb.jsx';
@ -10,11 +11,21 @@ import HelpTooltip from 'help-tooltip.jsx';
import jQuery from 'jquery'; import jQuery from 'jquery';
import { fromUrl } from 'common/thumbnail.jsx'; import { fromUrl } from 'common/thumbnail.jsx';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import PropTypes from 'prop-types';
const NewsletterSend = React.createClass({ const NewsletterSend = createReactClass({ // eslint-disable-line react/prefer-es6-class
contextTypes: { displayName: 'NewsletterSend',
router: React.PropTypes.object.isRequired,
propTypes: {
params: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
}, },
contextTypes: {
router: PropTypes.object.isRequired,
},
getInitialState: function getInitialState() { getInitialState: function getInitialState() {
return { return {
fields: [], fields: [],
@ -22,14 +33,26 @@ const NewsletterSend = React.createClass({
loading: true, loading: true,
}; };
}, },
componentDidMount: function componentDidMount() {
this.loadItem(this.props.params.id);
jQuery('#mailpoet_newsletter').parsley();
},
componentWillReceiveProps: function componentWillReceiveProps(props) {
this.loadItem(props.params.id);
},
getFieldsByNewsletter: function getFieldsByNewsletter(newsletter) { getFieldsByNewsletter: function getFieldsByNewsletter(newsletter) {
const type = this.getSubtype(newsletter); const type = this.getSubtype(newsletter);
return type.getFields(newsletter); return type.getFields(newsletter);
}, },
getSendButtonOptions: function getSendButtonOptions() { getSendButtonOptions: function getSendButtonOptions() {
const type = this.getSubtype(this.state.item); const type = this.getSubtype(this.state.item);
return type.getSendButtonOptions(this.state.item); return type.getSendButtonOptions(this.state.item);
}, },
getSubtype: function getSubtype(newsletter) { getSubtype: function getSubtype(newsletter) {
switch (newsletter.type) { switch (newsletter.type) {
case 'notification': return NotificationNewsletterFields; case 'notification': return NotificationNewsletterFields;
@ -37,16 +60,11 @@ const NewsletterSend = React.createClass({
default: return Hooks.applyFilters('mailpoet_newsletters_send_newsletter_fields', StandardNewsletterFields, newsletter); default: return Hooks.applyFilters('mailpoet_newsletters_send_newsletter_fields', StandardNewsletterFields, newsletter);
} }
}, },
isValid: function isValid() { isValid: function isValid() {
return jQuery('#mailpoet_newsletter').parsley().isValid(); return jQuery('#mailpoet_newsletter').parsley().isValid();
}, },
componentDidMount: function componentDidMount() {
this.loadItem(this.props.params.id);
jQuery('#mailpoet_newsletter').parsley();
},
componentWillReceiveProps: function componentWillReceiveProps(props) {
this.loadItem(props.params.id);
},
loadItem: function loadItem(id) { loadItem: function loadItem(id) {
this.setState({ loading: true }); this.setState({ loading: true });
@ -72,6 +90,7 @@ const NewsletterSend = React.createClass({
}); });
}); });
}, },
saveTemplate: function saveTemplate(response, done) { saveTemplate: function saveTemplate(response, done) {
fromUrl(response.meta.preview_url) fromUrl(response.meta.preview_url)
.then((thumbnail) => { .then((thumbnail) => {
@ -97,6 +116,7 @@ const NewsletterSend = React.createClass({
this.showError({ errors: [err] }); this.showError({ errors: [err] });
}); });
}, },
handleSend: function handleSend(e) { handleSend: function handleSend(e) {
e.preventDefault(); e.preventDefault();
@ -124,6 +144,7 @@ const NewsletterSend = React.createClass({
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
}); });
}, },
sendNewsletter: function sendNewsletter(newsletter) { sendNewsletter: function sendNewsletter(newsletter) {
return MailPoet.Ajax.post( return MailPoet.Ajax.post(
Hooks.applyFilters( Hooks.applyFilters(
@ -141,6 +162,11 @@ const NewsletterSend = React.createClass({
).done((response) => { ).done((response) => {
// save template in recently sent category // save template in recently sent category
this.saveTemplate(newsletter, () => { this.saveTemplate(newsletter, () => {
if (window.mailpoet_show_congratulate_after_first_newsletter) {
MailPoet.Modal.loading(false);
this.context.router.push(`/send/congratulate/${this.state.item.id}`);
return;
}
// redirect to listing based on newsletter type // redirect to listing based on newsletter type
this.context.router.push(Hooks.applyFilters('mailpoet_newsletters_send_server_request_response_redirect', `/${this.state.item.type || ''}`, this.state.item)); this.context.router.push(Hooks.applyFilters('mailpoet_newsletters_send_server_request_response_redirect', `/${this.state.item.type || ''}`, this.state.item));
const customResponse = Hooks.applyFilters('mailpoet_newsletters_send_server_request_response', this.state.item, response); const customResponse = Hooks.applyFilters('mailpoet_newsletters_send_server_request_response', this.state.item, response);
@ -171,6 +197,7 @@ const NewsletterSend = React.createClass({
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
}); });
}, },
activateNewsletter: function activateEmail(newsletter) { activateNewsletter: function activateEmail(newsletter) {
return MailPoet.Ajax.post({ return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
@ -183,6 +210,11 @@ const NewsletterSend = React.createClass({
}).done((response) => { }).done((response) => {
// save template in recently sent category // save template in recently sent category
this.saveTemplate(newsletter, () => { this.saveTemplate(newsletter, () => {
if (window.mailpoet_show_congratulate_after_first_newsletter) {
MailPoet.Modal.loading(false);
this.context.router.push(`/send/congratulate/${this.state.item.id}`);
return;
}
// redirect to listing based on newsletter type // redirect to listing based on newsletter type
this.context.router.push(`/${this.state.item.type || ''}`); this.context.router.push(`/${this.state.item.type || ''}`);
const opts = this.state.item.options; const opts = this.state.item.options;
@ -213,6 +245,7 @@ const NewsletterSend = React.createClass({
MailPoet.Modal.loading(false); MailPoet.Modal.loading(false);
}); });
}, },
handleResume: function handleResume(e) { handleResume: function handleResume(e) {
e.preventDefault(); e.preventDefault();
if (!this.isValid()) { if (!this.isValid()) {
@ -251,6 +284,7 @@ const NewsletterSend = React.createClass({
} }
return false; return false;
}, },
handleSave: function handleSave(e) { handleSave: function handleSave(e) {
e.preventDefault(); e.preventDefault();
@ -264,6 +298,7 @@ const NewsletterSend = React.createClass({
this.showError(err); this.showError(err);
}); });
}, },
handleRedirectToDesign: function handleRedirectToDesign(e) { handleRedirectToDesign: function handleRedirectToDesign(e) {
e.preventDefault(); e.preventDefault();
const redirectTo = e.target.href; const redirectTo = e.target.href;
@ -278,6 +313,7 @@ const NewsletterSend = React.createClass({
this.showError(err); this.showError(err);
}); });
}, },
saveNewsletter: function saveNewsletter() { saveNewsletter: function saveNewsletter() {
const data = this.state.item; const data = this.state.item;
data.queue = undefined; data.queue = undefined;
@ -298,10 +334,9 @@ const NewsletterSend = React.createClass({
endpoint: 'newsletters', endpoint: 'newsletters',
action: 'save', action: 'save',
data: newsletterData, data: newsletterData,
}).always(() => {
this.setState({ loading: false });
}); });
}, },
showError: (response) => { showError: (response) => {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(
@ -310,6 +345,7 @@ const NewsletterSend = React.createClass({
); );
} }
}, },
handleFormChange: function handleFormChange(e) { handleFormChange: function handleFormChange(e) {
const item = this.state.item; const item = this.state.item;
const field = e.target.name; const field = e.target.name;
@ -321,6 +357,7 @@ const NewsletterSend = React.createClass({
}); });
return true; return true;
}, },
render: function render() { render: function render() {
const isPaused = this.state.item.status === 'sending' const isPaused = this.state.item.status === 'sending'
&& this.state.item.queue && this.state.item.queue

View File

@ -0,0 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet';
import moment from 'moment';
import Success from './success.jsx';
import Fail from './fail.jsx';
import Loading from './loading.jsx';
const SECONDS_WAITING_FOR_SUCCESS = 20;
const SECONDS_MINIMUIM_LOADING_SCREEN_DISPLAYED = 6;
function successPageClosed() {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'settings',
action: 'set',
data: { show_congratulate_after_first_newsletter: false },
}).always(() => {
window.location = window.mailpoet_main_page;
});
}
function renderSuccess(newsletter, testingPassed) {
if (testingPassed) {
MailPoet.trackEvent('Cron testing done', {
'Cron is working': 'true',
});
}
return (<Success
illustrationImageUrl={window.mailpoet_congratulations_success_image}
successClicked={successPageClosed}
newsletter={newsletter}
/>);
}
function renderFail() {
MailPoet.trackEvent('Cron testing done', {
'Cron is working': 'false',
});
return (<Fail
failClicked={() => {
window.location = window.mailpoet_main_page;
}}
/>);
}
function renderLoading(showRichLoadingScreen) {
return (<Loading
illustrationImageUrl={window.mailpoet_congratulations_loading_image}
successClicked={successPageClosed}
showRichLoadingScreen={showRichLoadingScreen}
/>);
}
class Congratulate extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
fail: false,
newsletter: null,
testingPassed: false,
timeStart: moment(),
minimumLoadingTimePassed: false,
};
this.tick = this.tick.bind(this);
}
componentDidMount() {
this.loadNewsletter(this.props.params.id);
this.tick();
}
componentWillReceiveProps(props) {
this.loadNewsletter(props.params.id);
}
tick() {
if (moment().subtract(SECONDS_WAITING_FOR_SUCCESS, 'second').isAfter(this.state.timeStart)) {
this.setState({ error: true, loading: false });
}
if (this.state.loading) {
this.loadNewsletter(this.props.params.id);
}
if (moment().subtract(SECONDS_MINIMUIM_LOADING_SCREEN_DISPLAYED, 'seconds').isAfter(this.state.timeStart)) {
this.setState({ minimumLoadingTimePassed: true });
}
if (this.state.loading || !this.state.minimumLoadingTimePassed) {
setTimeout(this.tick, 2000);
}
}
loadNewsletter(id) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'get',
data: {
id,
},
})
.done(response => this.newsletterLoaded(response.data));
}
newsletterLoaded(newsletter) {
if ((newsletter.type !== 'standard') || (newsletter.status === 'scheduled')) {
this.setState({ newsletter, loading: false, minimumLoadingTimePassed: true });
} else if ((newsletter.status === 'sent') || (newsletter.status === 'sending')) {
this.setState({ newsletter, loading: false, testingPassed: true });
} else {
this.setState({ newsletter });
}
}
renderContent() {
if (this.state.loading || !this.state.minimumLoadingTimePassed) {
return renderLoading(!!this.state.newsletter);
} else if (this.state.error) {
return renderFail();
}
return renderSuccess(this.state.newsletter, this.state.testingPassed);
}
render() {
return (<div className="newsletter_congratulate_page">{this.renderContent()}</div>);
}
}
Congratulate.propTypes = {
params: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
};
module.exports = Congratulate;

View File

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactStringReplace from 'react-string-replace';
import MailPoet from '../../../mailpoet';
function Fail(props) {
return (
<div className="mailpoet_centered">
<h1>{MailPoet.I18n.t('congratulationsSendFailHeader')}</h1>
<p>
{ ReactStringReplace(
MailPoet.I18n.t('congratulationsSendFailExplain'),
/\[link\](.*?)\[\/link\]/g,
(match, i) => (
<a
key={i}
target="_blank"
rel="noopener noreferrer"
href="https://kb.mailpoet.com/article/231-sending-does-not-work"
>{ match }</a>
)
)
}
</p>
<button className="button" onClick={props.failClicked}>{MailPoet.I18n.t('close')}</button>
</div>
);
}
Fail.propTypes = {
failClicked: PropTypes.func.isRequired,
};
module.exports = Fail;

View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import MailPoet from '../../../mailpoet';
import LoadingDots from '../../../loading.jsx';
function renderRichData(showRichData, illustrationImageUrl) {
if (showRichData) {
return (
<div>
<h1 className="mailpoet_newsletter_loading_header">{MailPoet.I18n.t('congratulationsLoadingHeader')}</h1>
<img src={illustrationImageUrl} alt="" width="800px" height="266px" />
</div>
);
}
return (<div />);
}
function Loading(props) {
return (
<div className="mailpoet_newsletter_loading">
<LoadingDots />
{renderRichData(props.showRichLoadingScreen, props.illustrationImageUrl)}
</div>
);
}
Loading.propTypes = {
illustrationImageUrl: PropTypes.string.isRequired,
showRichLoadingScreen: PropTypes.bool.isRequired,
};
module.exports = Loading;

View File

@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import MailPoet from '../../../mailpoet';
function renderHeader(newsletter) {
if (newsletter.type === 'welcome') {
return MailPoet.I18n.t('congratulationsWelcomeEmailSuccessHeader');
} else if (newsletter.type === 'notification') {
return MailPoet.I18n.t('congratulationsPostNotificationSuccessHeader');
} else if (newsletter.type === 'automatic') {
return MailPoet.I18n.t('congratulationsWooSuccessHeader');
} else if (newsletter.status === 'scheduled') {
return MailPoet.I18n.t('congratulationsScheduleSuccessHeader');
}
return MailPoet.I18n.t('congratulationsSendSuccessHeader');
}
function Success(props) {
return (
<div className="mailpoet_congratulate_success">
<h1>{renderHeader(props.newsletter)}</h1>
<img src={props.illustrationImageUrl} alt="" width="750" height="250" />
<button className="button" onClick={props.successClicked}>{MailPoet.I18n.t('close')}</button>
</div>
);
}
Success.propTypes = {
successClicked: PropTypes.func.isRequired,
illustrationImageUrl: PropTypes.string.isRequired,
newsletter: PropTypes.shape({
status: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}).isRequired,
};
module.exports = Success;

View File

@ -0,0 +1,172 @@
import React from 'react';
import jQuery from 'jquery';
import _ from 'underscore';
import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
const datepickerTranslations = {
closeText: MailPoet.I18n.t('close'),
currentText: MailPoet.I18n.t('today'),
nextText: MailPoet.I18n.t('next'),
prevText: MailPoet.I18n.t('previous'),
monthNames: [
MailPoet.I18n.t('january'),
MailPoet.I18n.t('february'),
MailPoet.I18n.t('march'),
MailPoet.I18n.t('april'),
MailPoet.I18n.t('may'),
MailPoet.I18n.t('june'),
MailPoet.I18n.t('july'),
MailPoet.I18n.t('august'),
MailPoet.I18n.t('september'),
MailPoet.I18n.t('october'),
MailPoet.I18n.t('november'),
MailPoet.I18n.t('december'),
],
monthNamesShort: [
MailPoet.I18n.t('januaryShort'),
MailPoet.I18n.t('februaryShort'),
MailPoet.I18n.t('marchShort'),
MailPoet.I18n.t('aprilShort'),
MailPoet.I18n.t('mayShort'),
MailPoet.I18n.t('juneShort'),
MailPoet.I18n.t('julyShort'),
MailPoet.I18n.t('augustShort'),
MailPoet.I18n.t('septemberShort'),
MailPoet.I18n.t('octoberShort'),
MailPoet.I18n.t('novemberShort'),
MailPoet.I18n.t('decemberShort'),
],
dayNames: [
MailPoet.I18n.t('sunday'),
MailPoet.I18n.t('monday'),
MailPoet.I18n.t('tuesday'),
MailPoet.I18n.t('wednesday'),
MailPoet.I18n.t('thursday'),
MailPoet.I18n.t('friday'),
MailPoet.I18n.t('saturday'),
],
dayNamesShort: [
MailPoet.I18n.t('sundayShort'),
MailPoet.I18n.t('mondayShort'),
MailPoet.I18n.t('tuesdayShort'),
MailPoet.I18n.t('wednesdayShort'),
MailPoet.I18n.t('thursdayShort'),
MailPoet.I18n.t('fridayShort'),
MailPoet.I18n.t('saturdayShort'),
],
dayNamesMin: [
MailPoet.I18n.t('sundayMin'),
MailPoet.I18n.t('mondayMin'),
MailPoet.I18n.t('tuesdayMin'),
MailPoet.I18n.t('wednesdayMin'),
MailPoet.I18n.t('thursdayMin'),
MailPoet.I18n.t('fridayMin'),
MailPoet.I18n.t('saturdayMin'),
],
};
class DateText extends React.Component {
componentDidMount() {
const $element = jQuery(this.dateInput);
const that = this;
if ($element.datepicker) {
// Override jQuery UI datepicker Date parsing and formatting
jQuery.datepicker.parseDate = function parseDate(format, value) {
// Transform string format to Date object
return MailPoet.Date.toDate(value, {
parseFormat: this.props.displayFormat,
format,
});
};
jQuery.datepicker.formatDate = function formatDate(format, value) {
// Transform Date object to string format
const newValue = MailPoet.Date.format(value, {
format,
});
return newValue;
};
$element.datepicker(_.extend({
dateFormat: this.props.displayFormat,
isRTL: false,
onSelect: function onSelect(value) {
that.onChange({
target: {
name: that.getFieldName(),
value,
},
});
},
}, datepickerTranslations));
this.datepickerInitialized = true;
}
}
componentWillUnmount() {
if (this.datepickerInitialized) {
jQuery(this.dateInput).datepicker('destroy');
}
}
onChange = (event) => {
const changeEvent = event;
// Swap display format to storage format
const displayDate = changeEvent.target.value;
const storageDate = this.getStorageDate(displayDate);
changeEvent.target.value = storageDate;
this.props.onChange(changeEvent);
};
getFieldName = () => this.props.name || 'date';
getDisplayDate = (date) => {
const formatting = {
parseFormat: this.props.storageFormat,
format: this.props.displayFormat,
};
return MailPoet.Date.format(date, formatting);
};
getStorageDate = (date) => {
const formatting = {
parseFormat: this.props.displayFormat,
format: this.props.storageFormat,
};
return MailPoet.Date.format(date, formatting);
};
render() {
return (
<input
type="text"
size="30"
name={this.getFieldName()}
value={this.getDisplayDate(this.props.value)}
readOnly
disabled={this.props.disabled}
onChange={this.onChange}
ref={(c) => { this.dateInput = c; }}
{...this.props.validation}
/>
);
}
}
DateText.propTypes = {
displayFormat: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
name: PropTypes.string,
storageFormat: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
disabled: PropTypes.bool.isRequired,
validation: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
DateText.defaultProps = {
name: 'date',
};
module.exports = DateText;

View File

@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import DateText from 'newsletters/send/date_text.jsx';
import TimeSelect from 'newsletters/send/time_select.jsx';
class DateTime extends React.Component {
constructor(props) {
super(props);
this.state = this.buildStateFromProps(props);
}
componentWillReceiveProps(nextProps) {
this.setState(this.buildStateFromProps(nextProps));
}
getDateTime = () => [this.state.date, this.state.time].join(this.DATE_TIME_SEPARATOR);
DATE_TIME_SEPARATOR = ' ';
buildStateFromProps = (props) => {
const value = props.value || this.props.defaultDateTime;
const [date, time] = value.split(this.DATE_TIME_SEPARATOR);
return {
date,
time,
};
};
handleChange = (event) => {
const newState = {};
newState[event.target.name] = event.target.value;
this.setState(newState, this.propagateChange);
};
propagateChange = () => {
if (this.props.onChange) {
this.props.onChange({
target: {
name: this.props.name || '',
value: this.getDateTime(),
},
});
}
};
render() {
return (
<span>
<DateText
name="date"
value={this.state.date}
onChange={this.handleChange}
displayFormat={this.props.dateDisplayFormat}
storageFormat={this.props.dateStorageFormat}
disabled={this.props.disabled}
validation={this.props.dateValidation}
/>
<TimeSelect
name="time"
value={this.state.time}
onChange={this.handleChange}
disabled={this.props.disabled}
validation={this.props.timeValidation}
timeOfDayItems={this.props.timeOfDayItems}
/>
</span>
);
}
}
DateTime.propTypes = {
defaultDateTime: PropTypes.string.isRequired,
dateDisplayFormat: PropTypes.string.isRequired,
dateStorageFormat: PropTypes.string.isRequired,
onChange: PropTypes.func,
name: PropTypes.string,
disabled: PropTypes.bool,
dateValidation: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
timeValidation: PropTypes.any, // eslint-disable-line react/forbid-prop-types
timeOfDayItems: PropTypes.objectOf(PropTypes.string).isRequired,
};
DateTime.defaultProps = {
onChange: undefined,
name: '',
disabled: false,
timeValidation: undefined,
};
module.exports = DateTime;

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import jQuery from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Hooks from 'wp-js-hooks'; import Hooks from 'wp-js-hooks';
import PropTypes from 'prop-types';
import DateTime from 'newsletters/send/date_time.jsx';
const currentTime = window.mailpoet_current_time || '00:00'; const currentTime = window.mailpoet_current_time || '00:00';
const defaultDateTime = `${window.mailpoet_current_date} 00:00:00`; const defaultDateTime = `${window.mailpoet_current_date} 00:00:00`;
@ -10,248 +12,33 @@ const timeOfDayItems = window.mailpoet_schedule_time_of_day;
const dateDisplayFormat = window.mailpoet_date_display_format; const dateDisplayFormat = window.mailpoet_date_display_format;
const dateStorageFormat = window.mailpoet_date_storage_format; const dateStorageFormat = window.mailpoet_date_storage_format;
const datepickerTranslations = { class StandardScheduling extends React.Component {
closeText: MailPoet.I18n.t('close'), getCurrentValue = () => {
currentText: MailPoet.I18n.t('today'), const schedulingOptions = {
nextText: MailPoet.I18n.t('next'), isScheduled: '0',
prevText: MailPoet.I18n.t('previous'), scheduledAt: defaultDateTime,
monthNames: [
MailPoet.I18n.t('january'),
MailPoet.I18n.t('february'),
MailPoet.I18n.t('march'),
MailPoet.I18n.t('april'),
MailPoet.I18n.t('may'),
MailPoet.I18n.t('june'),
MailPoet.I18n.t('july'),
MailPoet.I18n.t('august'),
MailPoet.I18n.t('september'),
MailPoet.I18n.t('october'),
MailPoet.I18n.t('november'),
MailPoet.I18n.t('december'),
],
monthNamesShort: [
MailPoet.I18n.t('januaryShort'),
MailPoet.I18n.t('februaryShort'),
MailPoet.I18n.t('marchShort'),
MailPoet.I18n.t('aprilShort'),
MailPoet.I18n.t('mayShort'),
MailPoet.I18n.t('juneShort'),
MailPoet.I18n.t('julyShort'),
MailPoet.I18n.t('augustShort'),
MailPoet.I18n.t('septemberShort'),
MailPoet.I18n.t('octoberShort'),
MailPoet.I18n.t('novemberShort'),
MailPoet.I18n.t('decemberShort'),
],
dayNames: [
MailPoet.I18n.t('sunday'),
MailPoet.I18n.t('monday'),
MailPoet.I18n.t('tuesday'),
MailPoet.I18n.t('wednesday'),
MailPoet.I18n.t('thursday'),
MailPoet.I18n.t('friday'),
MailPoet.I18n.t('saturday'),
],
dayNamesShort: [
MailPoet.I18n.t('sundayShort'),
MailPoet.I18n.t('mondayShort'),
MailPoet.I18n.t('tuesdayShort'),
MailPoet.I18n.t('wednesdayShort'),
MailPoet.I18n.t('thursdayShort'),
MailPoet.I18n.t('fridayShort'),
MailPoet.I18n.t('saturdayShort'),
],
dayNamesMin: [
MailPoet.I18n.t('sundayMin'),
MailPoet.I18n.t('mondayMin'),
MailPoet.I18n.t('tuesdayMin'),
MailPoet.I18n.t('wednesdayMin'),
MailPoet.I18n.t('thursdayMin'),
MailPoet.I18n.t('fridayMin'),
MailPoet.I18n.t('saturdayMin'),
],
};
const DateText = React.createClass({
onChange: function onChange(event) {
const changeEvent = event;
// Swap display format to storage format
const displayDate = changeEvent.target.value;
const storageDate = this.getStorageDate(displayDate);
changeEvent.target.value = storageDate;
this.props.onChange(changeEvent);
},
componentDidMount: function componentDidMount() {
const $element = jQuery(this.dateInput);
const that = this;
if ($element.datepicker) {
// Override jQuery UI datepicker Date parsing and formatting
jQuery.datepicker.parseDate = function parseDate(format, value) {
// Transform string format to Date object
return MailPoet.Date.toDate(value, {
parseFormat: dateDisplayFormat,
format,
});
};
jQuery.datepicker.formatDate = function formatDate(format, value) {
// Transform Date object to string format
const newValue = MailPoet.Date.format(value, {
format,
});
return newValue;
};
$element.datepicker(_.extend({
dateFormat: this.props.displayFormat,
isRTL: false,
onSelect: function onSelect(value) {
that.onChange({
target: {
name: that.getFieldName(),
value,
},
});
},
}, datepickerTranslations));
this.datepickerInitialized = true;
}
},
componentWillUnmount: function componentWillUnmount() {
if (this.datepickerInitialized) {
jQuery(this.dateInput).datepicker('destroy');
}
},
getFieldName: function getFieldName() {
return this.props.name || 'date';
},
getDisplayDate: function getDisplayDate(date) {
return MailPoet.Date.format(date, {
parseFormat: this.props.storageFormat,
format: this.props.displayFormat,
});
},
getStorageDate: function getStorageDate(date) {
return MailPoet.Date.format(date, {
parseFormat: this.props.displayFormat,
format: this.props.storageFormat,
});
},
render: function render() {
return (
<input
type="text"
size="10"
name={this.getFieldName()}
value={this.getDisplayDate(this.props.value)}
readOnly
disabled={this.props.disabled}
onChange={this.onChange}
ref={(c) => { this.dateInput = c; }}
{...this.props.validation}
/>
);
},
});
const TimeSelect = React.createClass({
render: function render() {
const options = Object.keys(timeOfDayItems).map(
value => (
<option
key={`option-${timeOfDayItems[value]}`}
value={value}
>
{ timeOfDayItems[value] }
</option>
)
);
return (
<select
name={this.props.name || 'time'}
value={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
{...this.props.validation}
>
{options}
</select>
);
},
});
const DateTime = React.createClass({
DATE_TIME_SEPARATOR: ' ',
getInitialState: function getInitialState() {
return this.buildStateFromProps(this.props);
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this.setState(this.buildStateFromProps(nextProps));
},
buildStateFromProps: function buildStateFromProps(props) {
const value = props.value || defaultDateTime;
const [date, time] = value.split(this.DATE_TIME_SEPARATOR);
return {
date,
time,
}; };
},
handleChange: function handleChange(event) {
const newState = {};
newState[event.target.name] = event.target.value;
this.setState(newState, this.propagateChange);
},
propagateChange: function propagateChange() {
if (this.props.onChange) {
this.props.onChange({
target: {
name: this.props.name || '',
value: this.getDateTime(),
},
});
}
},
getDateTime: function getDateTime() {
return [this.state.date, this.state.time].join(this.DATE_TIME_SEPARATOR);
},
render: function render() {
return (
<span>
<DateText
name="date"
value={this.state.date}
onChange={this.handleChange}
displayFormat={dateDisplayFormat}
storageFormat={dateStorageFormat}
disabled={this.props.disabled}
validation={this.props.dateValidation}
/>
<TimeSelect
name="time"
value={this.state.time}
onChange={this.handleChange}
disabled={this.props.disabled}
validation={this.props.timeValidation}
/>
</span>
);
},
});
const StandardScheduling = React.createClass({
getCurrentValue: function getCurrentValue() {
return _.defaults( return _.defaults(
this.props.item[this.props.field.name] || {}, this.props.item[this.props.field.name] || {},
{ schedulingOptions
isScheduled: '0',
scheduledAt: defaultDateTime,
}
); );
}, };
handleValueChange: function handleValueChange(event) {
getDateValidation = () => ({
'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noScheduledDateError'),
'data-parsley-errors-container': '#mailpoet_scheduling',
});
isScheduled = () => this.getCurrentValue().isScheduled === '1';
handleCheckboxChange = (event) => {
const changeEvent = event;
changeEvent.target.value = this.isScheduledInput.checked ? '1' : '0';
return this.handleValueChange(changeEvent);
};
handleValueChange = (event) => {
const oldValue = this.getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
newValue[event.target.name] = event.target.value; newValue[event.target.name] = event.target.value;
@ -262,23 +49,9 @@ const StandardScheduling = React.createClass({
value: _.extend({}, oldValue, newValue), value: _.extend({}, oldValue, newValue),
}, },
}); });
}, };
handleCheckboxChange: function handleCheckboxChange(event) {
const changeEvent = event; render() {
changeEvent.target.value = this.isScheduledInput.checked ? '1' : '0';
return this.handleValueChange(changeEvent);
},
isScheduled: function isScheduled() {
return this.getCurrentValue().isScheduled === '1';
},
getDateValidation: function getDateValidation() {
return {
'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noScheduledDateError'),
'data-parsley-errors-container': '#mailpoet_scheduling',
};
},
render: function render() {
let schedulingOptions; let schedulingOptions;
if (this.isScheduled()) { if (this.isScheduled()) {
@ -290,6 +63,10 @@ const StandardScheduling = React.createClass({
onChange={this.handleValueChange} onChange={this.handleValueChange}
disabled={this.props.field.disabled} disabled={this.props.field.disabled}
dateValidation={this.getDateValidation()} dateValidation={this.getDateValidation()}
defaultDateTime={defaultDateTime}
timeOfDayItems={timeOfDayItems}
dateDisplayFormat={dateDisplayFormat}
dateStorageFormat={dateStorageFormat}
/> />
&nbsp; &nbsp;
<span> <span>
@ -313,8 +90,21 @@ const StandardScheduling = React.createClass({
{schedulingOptions} {schedulingOptions}
</div> </div>
); );
}, }
}); }
StandardScheduling.propTypes = {
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
field: PropTypes.shape({
name: PropTypes.string.isRequired,
disabled: PropTypes.bool,
}).isRequired,
onValueChange: PropTypes.func.isRequired,
};
StandardScheduling.defaultProps = {
item: {},
};
let fields = [ let fields = [
{ {

View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
class TimeSelect extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
const options = Object.keys(this.props.timeOfDayItems).map(
value => (
<option
key={`option-${this.props.timeOfDayItems[value]}`}
value={value}
>
{ this.props.timeOfDayItems[value] }
</option>
)
);
return (
<select
name={this.props.name || 'time'}
value={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange}
{...this.props.validation}
>
{options}
</select>
);
}
}
TimeSelect.propTypes = {
timeOfDayItems: PropTypes.objectOf(PropTypes.string).isRequired,
name: PropTypes.string,
value: PropTypes.string.isRequired,
disabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
validation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
TimeSelect.defaultProps = {
name: 'time',
disabled: false,
validation: {},
};
module.exports = TimeSelect;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Breadcrumb from 'newsletters/breadcrumb.jsx'; import Breadcrumb from 'newsletters/breadcrumb.jsx';
@ -5,11 +6,12 @@ import Hooks from 'wp-js-hooks';
import _ from 'underscore'; import _ from 'underscore';
import 'react-router'; import 'react-router';
const NewsletterTypes = React.createClass({ class NewsletterTypes extends React.Component {
contextTypes: { static contextTypes = {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, };
setupNewsletter: function setupNewsletter(type) {
setupNewsletter = (type) => {
if (type !== undefined) { if (type !== undefined) {
this.context.router.push(`/new/${type}`); this.context.router.push(`/new/${type}`);
MailPoet.trackEvent('Emails > Type selected', { MailPoet.trackEvent('Emails > Type selected', {
@ -17,8 +19,32 @@ const NewsletterTypes = React.createClass({
'Email type': type, 'Email type': type,
}); });
} }
}, };
createNewsletter: function createNewsletter(type) {
getAutomaticEmails = () => {
if (!window.mailpoet_automatic_emails) return [];
return _.map(window.mailpoet_automatic_emails, (automaticEmail) => {
const email = automaticEmail;
const onClick = _.partial(this.setupNewsletter, automaticEmail.slug);
email.action = (() => (
<div>
<a
className="button button-primary"
onClick={onClick}
role="button"
tabIndex={0}
>
{ MailPoet.I18n.t('setUp') }
</a>
</div>
))();
return email;
});
};
createNewsletter = (type) => {
MailPoet.trackEvent('Emails > Type selected', { MailPoet.trackEvent('Emails > Type selected', {
'MailPoet Free version': window.mailpoet_version, 'MailPoet Free version': window.mailpoet_version,
'Email type': type, 'Email type': type,
@ -41,30 +67,9 @@ const NewsletterTypes = React.createClass({
); );
} }
}); });
}, };
getAutomaticEmails: function getAutomaticEmails() {
if (!window.mailpoet_automatic_emails) return [];
return _.map(window.mailpoet_automatic_emails, (automaticEmail) => { render() {
const email = automaticEmail;
const onClick = _.partial(this.setupNewsletter, automaticEmail.slug);
email.action = (() => (
<div>
<a
className="button button-primary"
onClick={onClick}
role="button"
tabIndex={0}
>
{ MailPoet.I18n.t('setUp') }
</a>
</div>
))();
return email;
});
},
render: function render() {
const createStandardNewsletter = _.partial(this.createNewsletter, 'standard'); const createStandardNewsletter = _.partial(this.createNewsletter, 'standard');
const createNotificationNewsletter = _.partial(this.setupNewsletter, 'notification'); const createNotificationNewsletter = _.partial(this.setupNewsletter, 'notification');
const createWelcomeNewsletter = _.partial(this.setupNewsletter, 'welcome'); const createWelcomeNewsletter = _.partial(this.setupNewsletter, 'welcome');
@ -169,7 +174,7 @@ const NewsletterTypes = React.createClass({
</ul> </ul>
</div> </div>
); );
}, }
}); }
module.exports = NewsletterTypes; module.exports = NewsletterTypes;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Breadcrumb from 'newsletters/breadcrumb.jsx'; import Breadcrumb from 'newsletters/breadcrumb.jsx';
@ -10,27 +11,28 @@ const field = {
component: Scheduling, component: Scheduling,
}; };
const NewsletterNotification = React.createClass({ class NewsletterNotification extends React.Component {
contextTypes: { static contextTypes = {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, };
getInitialState: function getInitialState() {
return { state = {
options: { options: {
intervalType: 'daily', intervalType: 'daily',
timeOfDay: 0, timeOfDay: 0,
weekDay: 1, weekDay: 1,
monthDay: 0, monthDay: 0,
nthWeekDay: 1, nthWeekDay: 1,
}, },
}; };
},
handleValueChange: function handleValueChange(event) { handleValueChange = (event) => {
const state = this.state; const state = this.state;
state[event.target.name] = event.target.value; state[event.target.name] = event.target.value;
this.setState(state); this.setState(state);
}, };
handleNext: function handleNext() {
handleNext = () => {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: 'newsletters', endpoint: 'newsletters',
@ -49,11 +51,13 @@ const NewsletterNotification = React.createClass({
); );
} }
}); });
}, };
showTemplateSelection: function showTemplateSelection(newsletterId) {
showTemplateSelection = (newsletterId) => {
this.context.router.push(`/template/${newsletterId}`); this.context.router.push(`/template/${newsletterId}`);
}, };
render: function render() {
render() {
return ( return (
<div> <div>
<h1>{MailPoet.I18n.t('postNotificationNewsletterTypeTitle')}</h1> <h1>{MailPoet.I18n.t('postNotificationNewsletterTypeTitle')}</h1>
@ -77,8 +81,8 @@ const NewsletterNotification = React.createClass({
</p> </p>
</div> </div>
); );
}, }
}); }
module.exports = NewsletterNotification; module.exports = NewsletterNotification;

View File

@ -1,5 +1,6 @@
import _ from 'underscore'; import _ from 'underscore';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Select from 'form/fields/select.jsx'; import Select from 'form/fields/select.jsx';
import { import {
intervalValues, intervalValues,
@ -34,11 +35,10 @@ const nthWeekDayField = {
values: nthWeekDayValues, values: nthWeekDayValues,
}; };
const NotificationScheduling = React.createClass({ class NotificationScheduling extends React.Component {
getCurrentValue: function getCurrentValue() { getCurrentValue = () => this.props.item[this.props.field.name] || {};
return (this.props.item[this.props.field.name] || {});
}, handleValueChange = (name, value) => {
handleValueChange: function handleValueChange(name, value) {
const oldValue = this.getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
@ -50,38 +50,15 @@ const NotificationScheduling = React.createClass({
value: _.extend({}, oldValue, newValue), value: _.extend({}, oldValue, newValue),
}, },
}); });
}, };
handleIntervalChange: function handleIntervalChange(event) {
return this.handleValueChange( handleIntervalChange = event => this.handleValueChange('intervalType', event.target.value);
'intervalType', handleTimeOfDayChange = event => this.handleValueChange('timeOfDay', event.target.value);
event.target.value handleWeekDayChange = event => this.handleValueChange('weekDay', event.target.value);
); handleMonthDayChange = event => this.handleValueChange('monthDay', event.target.value);
}, handleNthWeekDayChange = event => this.handleValueChange('nthWeekDay', event.target.value);
handleTimeOfDayChange: function handleTimeOfDayChange(event) {
return this.handleValueChange( render() {
'timeOfDay',
event.target.value
);
},
handleWeekDayChange: function handleWeekDayChange(event) {
return this.handleValueChange(
'weekDay',
event.target.value
);
},
handleMonthDayChange: function handleMonthDayChange(event) {
return this.handleValueChange(
'monthDay',
event.target.value
);
},
handleNthWeekDayChange: function handleNthWeekDayChange(event) {
return this.handleValueChange(
'nthWeekDay',
event.target.value
);
},
render: function render() {
const value = this.getCurrentValue(); const value = this.getCurrentValue();
let timeOfDaySelection; let timeOfDaySelection;
let weekDaySelection; let weekDaySelection;
@ -143,7 +120,15 @@ const NotificationScheduling = React.createClass({
{timeOfDaySelection} {timeOfDaySelection}
</div> </div>
); );
}, }
}); }
NotificationScheduling.propTypes = {
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
field: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
onValueChange: PropTypes.func.isRequired,
};
module.exports = NotificationScheduling; module.exports = NotificationScheduling;

View File

@ -1,15 +1,14 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import Breadcrumb from 'newsletters/breadcrumb.jsx'; import Breadcrumb from 'newsletters/breadcrumb.jsx';
const NewsletterStandard = React.createClass({ class NewsletterStandard extends React.Component {
contextTypes: { static contextTypes = {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, };
showTemplateSelection: function showTemplateSelection(newsletterId) {
this.context.router.push(`/template/${newsletterId}`); componentDidMount() {
},
componentDidMount: function componentDidMount() {
// No options for this type, create a newsletter upon mounting // No options for this type, create a newsletter upon mounting
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
@ -28,16 +27,21 @@ const NewsletterStandard = React.createClass({
); );
} }
}); });
}, }
render: function render() {
showTemplateSelection = (newsletterId) => {
this.context.router.push(`/template/${newsletterId}`);
};
render() {
return ( return (
<div> <div>
<h1>{MailPoet.I18n.t('regularNewsletterTypeTitle')}</h1> <h1>{MailPoet.I18n.t('regularNewsletterTypeTitle')}</h1>
<Breadcrumb step="type" /> <Breadcrumb step="type" />
</div> </div>
); );
}, }
}); }
module.exports = NewsletterStandard; module.exports = NewsletterStandard;

View File

@ -4,6 +4,7 @@ import MailPoet from 'mailpoet';
import Select from 'form/fields/select.jsx'; import Select from 'form/fields/select.jsx';
import Text from 'form/fields/text.jsx'; import Text from 'form/fields/text.jsx';
import { timeDelayValues } from 'newsletters/scheduling/common.jsx'; import { timeDelayValues } from 'newsletters/scheduling/common.jsx';
import PropTypes from 'prop-types';
const availableRoles = window.mailpoet_roles || {}; const availableRoles = window.mailpoet_roles || {};
const availableSegments = _.filter( const availableSegments = _.filter(
@ -47,14 +48,14 @@ const afterTimeTypeField = {
values: timeDelayValues, values: timeDelayValues,
}; };
const WelcomeScheduling = React.createClass({ class WelcomeScheduling extends React.Component {
contextTypes: { static contextTypes = {
router: React.PropTypes.object.isRequired, router: PropTypes.object.isRequired,
}, };
getCurrentValue: function getCurrentValue() {
return (this.props.item[this.props.field.name] || {}); getCurrentValue = () => this.props.item[this.props.field.name] || {};
},
handleValueChange: function handleValueChange(name, value) { handleValueChange = (name, value) => {
const oldValue = this.getCurrentValue(); const oldValue = this.getCurrentValue();
const newValue = {}; const newValue = {};
@ -66,38 +67,15 @@ const WelcomeScheduling = React.createClass({
value: _.extend({}, oldValue, newValue), value: _.extend({}, oldValue, newValue),
}, },
}); });
}, };
handleEventChange: function handleEventChange(event) {
return this.handleValueChange( handleEventChange = event => this.handleValueChange('event', event.target.value);
'event', handleSegmentChange = event => this.handleValueChange('segment', event.target.value);
event.target.value handleRoleChange = event => this.handleValueChange('role', event.target.value);
); handleAfterTimeNumberChange = event => this.handleValueChange('afterTimeNumber', event.target.value);
}, handleAfterTimeTypeChange = event => this.handleValueChange('afterTimeType', event.target.value);
handleSegmentChange: function handleSegmentChange(event) {
return this.handleValueChange( handleNext = () => {
'segment',
event.target.value
);
},
handleRoleChange: function handleRoleChange(event) {
return this.handleValueChange(
'role',
event.target.value
);
},
handleAfterTimeNumberChange: function handleAfterTimeNumberChange(event) {
return this.handleValueChange(
'afterTimeNumber',
event.target.value
);
},
handleAfterTimeTypeChange: function handleAfterTimeTypeChange(event) {
return this.handleValueChange(
'afterTimeType',
event.target.value
);
},
handleNext: function handleNext() {
MailPoet.Ajax.post({ MailPoet.Ajax.post({
api_version: window.mailpoet_api_version, api_version: window.mailpoet_api_version,
endpoint: 'newsletters', endpoint: 'newsletters',
@ -116,11 +94,13 @@ const WelcomeScheduling = React.createClass({
); );
} }
}); });
}, };
showTemplateSelection: function showTemplateSelection(newsletterId) {
showTemplateSelection = (newsletterId) => {
this.context.router.push(`/template/${newsletterId}`); this.context.router.push(`/template/${newsletterId}`);
}, };
render: function render() {
render() {
const value = this.getCurrentValue(); const value = this.getCurrentValue();
let roleSegmentSelection; let roleSegmentSelection;
let timeNumber; let timeNumber;
@ -171,7 +151,15 @@ const WelcomeScheduling = React.createClass({
/> />
</div> </div>
); );
}, }
}); }
WelcomeScheduling.propTypes = {
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
field: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
onValueChange: PropTypes.func.isRequired,
};
module.exports = WelcomeScheduling; module.exports = WelcomeScheduling;

View File

@ -1,12 +1,27 @@
function displayPoll() { function displayPoll() {
if (window.mailpoet_display_nps_poll && window.satismeter) { if (
window.mailpoet_display_nps_poll
&& window.satismeter
&& window.mailpoet_installed_at_isoFormat
) {
// New users poll
window.satismeter({ window.satismeter({
writeKey: '6L479eVPXk7pBn6S', writeKey: '6L479eVPXk7pBn6S',
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url, userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
traits: { traits: {
name: window.mailpoet_current_wp_user.user_nicename, name: window.mailpoet_current_wp_user.user_nicename,
email: window.mailpoet_current_wp_user.user_email, email: window.mailpoet_current_wp_user.user_email,
createdAt: window.mailpoet_settings.installed_at, createdAt: window.mailpoet_installed_at_isoFormat,
},
});
// Old users poll
window.satismeter({
writeKey: 'k0aJAsQAWI2ERyGv',
userId: window.mailpoet_current_wp_user.ID + window.mailpoet_site_url,
traits: {
name: window.mailpoet_current_wp_user.user_nicename,
email: window.mailpoet_current_wp_user.user_email,
createdAt: window.mailpoet_installed_at_isoFormat,
}, },
}); });
} }

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
@ -191,8 +192,8 @@ const itemActions = [
}, },
]; ];
const SegmentList = React.createClass({ class SegmentList extends React.Component {
renderItem: function renderItem(segment, actions) { renderItem = (segment, actions) => {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
@ -248,8 +249,9 @@ const SegmentList = React.createClass({
</td> </td>
</div> </div>
); );
}, };
render: function render() {
render() {
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
@ -272,7 +274,12 @@ const SegmentList = React.createClass({
/> />
</div> </div>
); );
}, }
}); }
SegmentList.propTypes = {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
module.exports = SegmentList; module.exports = SegmentList;

View File

@ -2,17 +2,22 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router'; import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import PropTypes from 'prop-types';
import SegmentList from 'segments/list.jsx'; import SegmentList from 'segments/list.jsx';
import SegmentForm from 'segments/form.jsx'; import SegmentForm from 'segments/form.jsx';
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({ class App extends React.Component {
render() { render() {
return this.props.children; return this.props.children;
}, }
}); }
App.propTypes = {
children: PropTypes.element.isRequired,
};
const container = document.getElementById('segments_container'); const container = document.getElementById('segments_container');

View File

@ -0,0 +1,14 @@
import ReactDOM from 'react-dom';
import React from 'react';
import Announcement from './new_subscriber_announcement.jsx';
const container = document.getElementById('new_subscriber_announcement');
if (container) {
ReactDOM.render(
<Announcement
installedAt={window.mailpoet_installed_at}
imageUrl={window.mailpoet_new_subscriber_announcement_image}
/>, container
);
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import MailPoet from 'mailpoet';
import moment from 'moment';
import InAppAnnouncement from 'in_app_announcements/in_app_announcement.jsx';
const NewSubscriberNotificationAnnouncement = props => (
<InAppAnnouncement
validUntil={moment(props.installedAt).add(3, 'months').toDate()}
height="700px"
showOnlyOnceSlug="new_subscriber_notification"
showToNewUser={false}
>
<div className="new_subscriber_notification_announcement">
<h1>{MailPoet.I18n.t('announcementHeader')}</h1>
<img src={props.imageUrl} width="600px" height="460px" alt="" />
<p>
{MailPoet.I18n.t('announcementParagraph1')}<br />
{MailPoet.I18n.t('announcementParagraph2')}
</p>
</div>
</InAppAnnouncement>
);
NewSubscriberNotificationAnnouncement.propTypes = {
installedAt: PropTypes.string.isRequired,
imageUrl: PropTypes.string.isRequired,
};
module.exports = NewSubscriberNotificationAnnouncement;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import PropTypes from 'prop-types';
import Form from 'form/form.jsx'; import Form from 'form/form.jsx';
import ReactStringReplace from 'react-string-replace'; import ReactStringReplace from 'react-string-replace';
@ -175,8 +176,8 @@ function afterFormContent() {
); );
} }
const SubscriberForm = React.createClass({ class SubscriberForm extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
render: function render() { render() {
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
@ -194,7 +195,11 @@ const SubscriberForm = React.createClass({
/> />
</div> </div>
); );
}, }
}); }
SubscriberForm.propTypes = {
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
module.exports = SubscriberForm; module.exports = SubscriberForm;

View File

@ -4,6 +4,7 @@ import { Link } from 'react-router';
import jQuery from 'jquery'; import jQuery from 'jquery';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import Listing from 'listing/listing.jsx'; import Listing from 'listing/listing.jsx';
import Selection from 'form/fields/selection.jsx'; import Selection from 'form/fields/selection.jsx';
@ -242,8 +243,8 @@ const itemActions = [
}, },
]; ];
const SubscriberList = React.createClass({ class SubscriberList extends React.Component {
getSegmentFromId: function getSegmentFromId(segmentId) { getSegmentFromId = (segmentId) => {
let result = false; let result = false;
window.mailpoet_segments.forEach((segment) => { window.mailpoet_segments.forEach((segment) => {
if (segment.id === segmentId) { if (segment.id === segmentId) {
@ -251,8 +252,9 @@ const SubscriberList = React.createClass({
} }
}); });
return result; return result;
}, };
renderItem: function renderItem(subscriber, actions) {
renderItem = (subscriber, actions) => {
const rowClasses = classNames( const rowClasses = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
@ -333,8 +335,9 @@ const SubscriberList = React.createClass({
</td> </td>
</div> </div>
); );
}, };
render: function render() {
render() {
return ( return (
<div> <div>
<h1 className="title"> <h1 className="title">
@ -368,7 +371,12 @@ const SubscriberList = React.createClass({
/> />
</div> </div>
); );
}, }
}); }
SubscriberList.propTypes = {
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
module.exports = SubscriberList; module.exports = SubscriberList;

View File

@ -2,16 +2,21 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router'; import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import PropTypes from 'prop-types';
import SubscriberList from 'subscribers/list.jsx'; import SubscriberList from 'subscribers/list.jsx';
import SubscriberForm from 'subscribers/form.jsx'; import SubscriberForm from 'subscribers/form.jsx';
const history = useRouterHistory(createHashHistory)({ queryKey: false }); const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({ class App extends React.Component {
render() { render() {
return this.props.children; return this.props.children;
}, }
}); }
App.propTypes = {
children: PropTypes.element.isRequired,
};
const container = document.getElementById('subscribers_container'); const container = document.getElementById('subscribers_container');

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import SteppedProgressBar from '../common/stepped_progess_bar.jsx'; import SteppedProgressBar from '../common/stepped_progess_bar.jsx';
@ -13,9 +14,9 @@ const WelcomeWizardHeader = props => (
); );
WelcomeWizardHeader.propTypes = { WelcomeWizardHeader.propTypes = {
current_step: React.PropTypes.number.isRequired, current_step: PropTypes.number.isRequired,
steps_count: React.PropTypes.number.isRequired, steps_count: PropTypes.number.isRequired,
logo_src: React.PropTypes.string.isRequired, logo_src: PropTypes.string.isRequired,
}; };
module.exports = WelcomeWizardHeader; module.exports = WelcomeWizardHeader;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import ReactStringReplace from 'react-string-replace'; import ReactStringReplace from 'react-string-replace';
@ -62,7 +63,7 @@ const WelcomeWizardHelpInfoStep = props => (
module.exports = WelcomeWizardHelpInfoStep; module.exports = WelcomeWizardHelpInfoStep;
WelcomeWizardHelpInfoStep.propTypes = { WelcomeWizardHelpInfoStep.propTypes = {
next: React.PropTypes.func.isRequired, next: PropTypes.func.isRequired,
}; };
module.exports = WelcomeWizardHelpInfoStep; module.exports = WelcomeWizardHelpInfoStep;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
@ -12,7 +13,7 @@ const WelcomeWizardMigratedUserStep = props => (
); );
WelcomeWizardMigratedUserStep.propTypes = { WelcomeWizardMigratedUserStep.propTypes = {
next: React.PropTypes.func.isRequired, next: PropTypes.func.isRequired,
}; };
module.exports = WelcomeWizardMigratedUserStep; module.exports = WelcomeWizardMigratedUserStep;

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import jQuery from 'jquery'; import jQuery from 'jquery';
@ -47,13 +48,13 @@ const WelcomeWizardSenderStep = props => (
); );
WelcomeWizardSenderStep.propTypes = { WelcomeWizardSenderStep.propTypes = {
finish: React.PropTypes.func.isRequired, finish: PropTypes.func.isRequired,
loading: React.PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
update_sender: React.PropTypes.func.isRequired, update_sender: PropTypes.func.isRequired,
submit_sender: React.PropTypes.func.isRequired, submit_sender: PropTypes.func.isRequired,
sender: React.PropTypes.shape({ sender: PropTypes.shape({
name: React.PropTypes.string, name: PropTypes.string,
address: React.PropTypes.string, address: PropTypes.string,
}), }),
}; };

View File

@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MailPoet from 'mailpoet'; import MailPoet from 'mailpoet';
import ReactStringReplace from 'react-string-replace'; import ReactStringReplace from 'react-string-replace';
@ -48,8 +49,8 @@ const WelcomeWizardUsageTrackingStep = props => (
module.exports = WelcomeWizardUsageTrackingStep; module.exports = WelcomeWizardUsageTrackingStep;
WelcomeWizardUsageTrackingStep.propTypes = { WelcomeWizardUsageTrackingStep.propTypes = {
allow_action: React.PropTypes.func.isRequired, allow_action: PropTypes.func.isRequired,
allow_text: React.PropTypes.string.isRequired, allow_text: PropTypes.string.isRequired,
skip_action: React.PropTypes.func.isRequired, skip_action: PropTypes.func.isRequired,
loading: React.PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
}; };

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