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

View File

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

View File

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

View File

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

6
.gitignore vendored
View File

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

View File

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

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
RUN composer global require --optimize-autoloader "hirak/prestissimo"
WORKDIR /wp-core/wp-content/plugins/mailpoet
ENV WP_TEST_PATH=/wp-core
ENV WP_ROOT=/wp-core
ADD docker-entrypoint.sh /

302
README.md
View File

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

View File

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

View File

@ -36,3 +36,5 @@
@require 'welcome_wizard'
@require 'intro'
@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
left: 8px
.mailpoet_in_app_announcement_background_videos
.mailpoet_in_app_announcement_background_videos, .mailpoet_drag_and_drop_tutorial
text-align: center
h2
font-size: 28px
video
margin-top: 20px
.new_subscriber_notification_announcement
h2
font-size: 28px
text-align: center

View File

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

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
input.mailpoet_option_offset_left_small
margin-left: 10px !important
.mailpoet_form_field span.select2-container
width: 103px !important
span.select2-container--open > span.select2-dropdown
width: 150px !important
span.select2-container--open > span.select2-dropdown li.select2-results__option
font-size: 13px
margin: 0px !important
& .select2-results__group
font-weight: normal
color: #bfbfbf
& .select2-results__option
padding-left: 15px
font-size: 13px
&[aria-selected=true]
background-color: #eee
color: #444
.mailpoet-fonts-notice
color: #999

View File

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

View File

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

View File

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

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';
const KeyValueTable = props => (
@ -13,13 +14,13 @@ const KeyValueTable = props => (
);
KeyValueTable.propTypes = {
max_width: React.PropTypes.string,
rows: React.PropTypes.arrayOf(React.PropTypes.shape({
key: React.PropTypes.string.isRequired,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.element,
max_width: PropTypes.string,
rows: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.element,
]).isRequired,
})).isRequired,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 'Trebuchet MS': return new Handlebars.SafeString("'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif");
case 'Verdana': return new Handlebars.SafeString('Verdana, Geneva, sans-serif');
case 'Arvo': return new Handlebars.SafeString('arvo, courier, georgia, serif');
case 'Lato': return new Handlebars.SafeString("lato, 'helvetica neue', helvetica, arial, sans-serif");
case 'Lora': return new Handlebars.SafeString("lora, georgia, 'times new roman', serif");
case 'Merriweather': return new Handlebars.SafeString("merriweather, georgia, 'times new roman', serif");
case 'Merriweather Sans': return new Handlebars.SafeString("'merriweather sans', 'helvetica neue', helvetica, arial, sans-serif");
case 'Noticia Text': return new Handlebars.SafeString("'noticia text', georgia, 'times new roman', serif");
case 'Open Sans': return new Handlebars.SafeString("'open sans', 'helvetica neue', helvetica, arial, sans-serif");
case 'Playfair Display': return new Handlebars.SafeString("playfair display, georgia, 'times new roman', serif");
case 'Roboto': return new Handlebars.SafeString("roboto, 'helvetica neue', helvetica, arial, sans-serif");
case 'Source Sans Pro': return new Handlebars.SafeString("'source sans pro', 'helvetica neue', helvetica, arial, sans-serif");
default: return font;
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 MailPoet from 'mailpoet';
import InAppAnnouncementDot from './in_app_announcement_dot.jsx';
@ -78,11 +79,11 @@ const validateBooleanWithWindowDependency = (props, propName, componentName, win
};
InAppAnnouncement.propTypes = {
width: React.PropTypes.string,
height: React.PropTypes.string,
className: React.PropTypes.string,
children: React.PropTypes.element.isRequired,
validUntil: React.PropTypes.instanceOf(Date),
width: PropTypes.string,
height: PropTypes.string,
className: PropTypes.string,
children: PropTypes.element.isRequired,
validUntil: PropTypes.instanceOf(Date),
showToNewUser: (props, propName, componentName) => (
validateBooleanWithWindowDependency(props, propName, componentName, 'mailpoet_is_new_user')
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

13
assets/js/src/loading.jsx Normal file
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>' +
'</div>',
loading: '<div id="mailpoet_loading" style="display:none;">' +
loading: '<div id="mailpoet_loading" class="mailpoet_loading" style="display:none;">' +
'<div id="mailpoet_modal_loading_1" class="mailpoet_modal_loading"></div>' +
'<div id="mailpoet_modal_loading_2" class="mailpoet_modal_loading"></div>' +
'<div id="mailpoet_modal_loading_3" class="mailpoet_modal_loading"></div>' +

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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