Compare commits

...

267 Commits

Author SHA1 Message Date
e4213437e9 Bump up release version to 0.0.35 2016-07-08 14:57:30 +03:00
386bdceed3 Merge pull request #543 from mailpoet/api_refactor
API refactor
2016-07-08 13:55:01 +03:00
87eda71931 Merge pull request #544 from mailpoet/issue_434
Settings Page
2016-07-08 13:28:06 +03:00
cef9f1dcf8 - Updates constant names 2016-07-07 14:32:05 -04:00
c78b2088eb - Updates the check for invalid API endpoint 2016-07-07 14:20:27 -04:00
150364de3a - Fixes API endpoint naming convention
- Generates/saves cron daemon token as soon as its executed
2016-07-07 14:00:07 -04:00
1cd9f3eb67 - Removes counter from cron daemon
- Invokes token regeneration/comparion at a later stage
2016-07-07 10:24:24 -04:00
89253125af - Fixes a typo 2016-07-07 10:00:12 -04:00
e7ee356f90 cleanup permissions related classes 2016-07-07 15:49:03 +02:00
a88017400b Remove email validation as it was not working properly
- using the MailPoet sending method forces signup confirmation
- save settings when activating a sending method (works when pressing enter in an input when setting up method)
2016-07-07 15:36:59 +02:00
f557881462 - Updates code based on review comments 2016-07-07 09:01:59 -04:00
8dba4727c4 - Updates open/click link generation logic to utilize API's buildRequest
method
2016-07-06 22:57:39 -04:00
8ec094089f - Removes TODO notice 2016-07-06 20:18:02 -04:00
3c353e715b - Fixes view in browser API URL 2016-07-06 19:56:26 -04:00
ab33a9c352 - Updates cron API URL
- Removes cron daemon counter
- Generates/saves cron daemon token as soon as its executed
2016-07-06 19:48:16 -04:00
406b509ac4 Remove saving of roles & permissions when saving settings
- make sure we try to activate the sending method instead of saving settings when pressing enter in an input
- added Default sender row with global from/reply_to
- hide notification emails setting
- removed notification from/reply to email (for the time being, we will reintroduce it if need be later on)
2016-07-06 17:29:24 +02:00
bd814baf28 - Fixes data not being passed to API buildRequest method 2016-07-06 10:02:47 -04:00
2db681d908 - Adds and centralizes API data encoding/decoding method 2016-07-06 09:22:34 -04:00
37e3af584e added parsley validation on settings form - need to fix permissions 2016-07-06 14:12:30 +02:00
5fc863bd82 Settings page update (issue #434) 2016-07-06 14:10:46 +02:00
76649f9590 removed debug mode and roles and permissions from advanced tab 2016-07-06 13:56:11 +02:00
cb2faec8b2 - Refactors API
- Updates existing classes to use the refactored API methods
2016-07-05 20:17:25 -04:00
e8604284fe Merge pull request #540 from mailpoet/issue_431
Bulk actions messages + remaining UI items from issue 431
2016-07-05 17:59:15 +03:00
d2ccdef6c7 better alternative to remove duplicate MailPoet submenu 2016-07-05 16:55:03 +02:00
38199dc96f - Adds validation for API data 2016-07-05 10:20:30 -04:00
630b219e96 Merge pull request #539 from mailpoet/twig_caching
Fix Twig cache regeneration
2016-07-05 09:57:02 -04:00
64155bc121 Merge pull request #541 from mailpoet/populator_settings
Do not reset plugin settings on plugin reactivation
2016-07-05 14:42:28 +02:00
7fb45a15ee Fix code style errors 2016-07-05 15:28:38 +03:00
ed5294477f Fix Populator to not overwrite existing settings 2016-07-05 15:23:11 +03:00
d152b073a6 fixed onSuccess on bulk actions and locale formatted numbers in success messages 2016-07-05 13:58:12 +02:00
f8efb3934b remove 'MailPoet' submenu and make newsletters the default page 2016-07-05 13:16:14 +02:00
5a21d3fdc8 added missing 'row-title' class on listings 2016-07-05 11:44:49 +02:00
710ede15ce sending method daily emails frequency in locale 2016-07-05 10:49:12 +02:00
150286ab6b Enable regenerating templates that have changed 2016-07-04 18:05:50 +03:00
9e758e8a33 Bump up release version to 0.0.34 2016-07-01 16:57:48 +03:00
059165e5d2 Merge pull request #536 from mailpoet/manage_subscriptions
Manage subscriptions
2016-07-01 16:17:50 +03:00
fe154d9251 fixed code sniffer reported errors 2016-07-01 14:17:39 +02:00
a8ffbc2d0e handle empty/unchecked/checked checkboxes properly in both react and forms 2016-07-01 14:14:18 +02:00
9de3a245b0 fixed both radio & checkbox fields so that it selects the proper value 2016-07-01 14:14:18 +02:00
5eef709af5 Uniform date display format for Manage Subscriptions & Subscriber new/edit
- use isWPUser instead of wp_user !== null
2016-07-01 14:14:18 +02:00
7b0c130d0a updated unit test for custom fields of date type 2016-07-01 14:14:18 +02:00
7f265675b0 changed the way custom field date type is handled (react + form + db) 2016-07-01 14:14:18 +02:00
ba15db9829 fixed value loading for textarea 2016-07-01 14:14:18 +02:00
d15473a8e4 disabled first/last name inputs for WP User on manage subscription page 2016-07-01 14:14:18 +02:00
d9f93dc6e7 Merge pull request #537 from mailpoet/qa
QA Tools and improvements
2016-06-30 18:17:05 +02:00
634c5b699d Remove leftover merge conflict, fix empty ALC block message #505 2016-06-30 19:12:26 +03:00
23e8ce38dd Merge remote-tracking branch 'origin/qa' into qa
Conflicts:
	lib/Config/Initializer.php
	lib/Cron/Workers/SendingQueue/SendingQueue.php
	lib/Models/SendingQueue.php
	lib/Router/Router.php
2016-06-30 19:01:44 +03:00
c62ae2ce80 Add PHP CodeSniffer option to reduce severity, fixed syntax error 2016-06-30 18:52:07 +03:00
d0813bb4e2 Fix class and method names to use camel case 2016-06-30 18:52:07 +03:00
0ac701eb20 Change line endings from DOS CRLF to Unix LF 2016-06-30 18:52:07 +03:00
607395be6f Fix spacing around commas 2016-06-30 18:52:07 +03:00
55d48df8a4 Fix indentation issues 2016-06-30 18:50:48 +03:00
e0282ae45b Fix empty catch statement error 2016-06-30 18:50:48 +03:00
235fdea00f Remove commented out code, raise code similarity trigger treshold 2016-06-30 18:50:48 +03:00
b8c6d54f48 Fix "Closing brace must be on a line by itself" code sniffer errors 2016-06-30 18:50:48 +03:00
67661e3aad Remove useless constructors 2016-06-30 18:50:48 +03:00
c03facdc45 Add space after comma in function call parameters 2016-06-30 18:49:50 +03:00
9ddc1ef555 Remove statements that cannot be executed 2016-06-30 18:49:50 +03:00
9cfc2fd940 Remove an unnecessary return statement 2016-06-30 18:49:50 +03:00
24e108bce7 Remove spaces after type casts 2016-06-30 18:49:50 +03:00
48f0c03425 Fix spacing between control structure and opening parenthesis 2016-06-30 18:46:33 +03:00
0bfbe6dc79 Change TRUE, FALSE, NULL capitalization to lowercase 2016-06-30 18:46:33 +03:00
ad0a9838bc Disable line width limits 2016-06-30 18:46:33 +03:00
81ec293e54 Remove rule that prevents statements with only comments in body 2016-06-30 18:46:32 +03:00
b8b3d76a1d Remove PHPMD and PHPCPD tools we don't use 2016-06-30 18:46:32 +03:00
805e641d40 Add PHP lint and PHP code sniffer 2016-06-30 18:46:32 +03:00
18326f9df1 Merge pull request #535 from mailpoet/sending_queue_refactor
Fixes error that resulted in additional newsletter to be sent
2016-06-30 16:39:45 +02:00
46dda84012 - Moves queue subscriber handling logic to the queu model 2016-06-30 10:23:06 -04:00
9979261cb6 fixed a few more warnings 2016-06-30 15:42:58 +02:00
e8887e2aa5 Add PHP CodeSniffer option to reduce severity, fixed syntax error 2016-06-30 15:24:50 +03:00
4a91fae984 Fix class and method names to use camel case 2016-06-30 15:13:48 +03:00
0fe975f614 - Declares array 2016-06-30 07:52:16 -04:00
c7fd7b8a32 Change line endings from DOS CRLF to Unix LF 2016-06-30 14:39:28 +03:00
b7e3c3ae81 Fix spacing around commas 2016-06-30 14:03:07 +03:00
b2c3206185 - Fixes error that resulted in additional newsletter to be sent 2016-06-30 06:42:56 -04:00
b7d8d482fe Fix indentation issues 2016-06-30 13:29:23 +03:00
8a9d14319b Fix empty catch statement error 2016-06-30 12:40:22 +03:00
c396254e64 Remove commented out code, raise code similarity trigger treshold 2016-06-29 21:19:24 +03:00
e4dbeca664 Fix "Closing brace must be on a line by itself" code sniffer errors 2016-06-29 20:48:14 +03:00
168540d6d2 Remove useless constructors 2016-06-29 20:42:03 +03:00
c62cd6c023 Add space after comma in function call parameters 2016-06-29 19:26:07 +03:00
033e0581f1 Remove statements that cannot be executed 2016-06-29 19:20:50 +03:00
9f978d3362 Remove an unnecessary return statement 2016-06-29 19:09:07 +03:00
841340a42d Remove spaces after type casts 2016-06-29 19:04:23 +03:00
9595e9629f Fix spacing between control structure and opening parenthesis 2016-06-29 18:54:01 +03:00
56ba543f8d Change TRUE, FALSE, NULL capitalization to lowercase 2016-06-29 18:38:38 +03:00
1cead6c6cd Disable line width limits 2016-06-29 17:57:38 +03:00
f5f7ce4c42 Remove rule that prevents statements with only comments in body 2016-06-29 17:15:00 +03:00
b13075b8f2 Remove PHPMD and PHPCPD tools we don't use 2016-06-29 16:49:22 +03:00
c4db9e3227 Add PHP lint and PHP code sniffer 2016-06-29 16:19:50 +03:00
f47bfb5439 Merge pull request #532 from mailpoet/newsletter_creation
Newsletter creation: Step 1 and 3 changes
2016-06-28 15:25:14 +02:00
cc4639cb23 Set default values when immediately sending scheduled newsletter 2016-06-28 15:57:02 +03:00
69094f57fd Fix typos 2016-06-28 15:01:31 +03:00
ffe7b80888 Simplify variable declarations 2016-06-28 14:12:09 +03:00
fc846b808e Remove obsolete debugging statement 2016-06-28 14:12:09 +03:00
1cbf6b67b2 Remove console.log statements 2016-06-28 14:12:09 +03:00
286c02bdd9 Fix standard newsletter scheduling to always include scheduleAt 2016-06-28 14:12:08 +03:00
5f1d76225b - Add sorting of segment names in Welcome newsletter segment selector;
- Add an option to FormFieldSelect to allow sorting options;
- Change "Send" button label for scheduled newsletters;
- Disable "Send" button for sending/already sent newsletters.
2016-06-28 14:12:08 +03:00
c05ea1b968 Change "Go back to editor" to save form fields first 2016-06-28 14:12:08 +03:00
2d45ab2e88 Add WP user segment selection to Notification and Standard newsletters 2016-06-28 14:12:08 +03:00
ca9b1e25a7 Change notification newsletter time to be displayed in WP format 2016-06-28 14:12:08 +03:00
2927875e16 Regenerate thumbnails of default newsletter templates 2016-06-28 14:12:07 +03:00
486a97fa30 Vertically center template thumbs and don't enforce min-height for them 2016-06-28 14:12:07 +03:00
c22d434dff Merge pull request #531 from mailpoet/unit_test_catchup
Unit test update
2016-06-28 12:22:50 +03:00
306cdeb68f Models unit tests update 2016-06-27 13:53:56 +02:00
7ee83dad06 Merge pull request #527 from mailpoet/sending_queue_refactor
Sending queue refactor
2016-06-23 18:16:21 +03:00
d414313749 - Fixes const definition for PHP 5.5 2016-06-22 13:35:48 -04:00
66d329f630 - Configures mailer inside the mailer task class 2016-06-22 11:21:11 -04:00
f524ffcb28 - Updates mailer task to store mailer instance 2016-06-22 11:15:40 -04:00
264b7e180b listing handler and bulk actions tests completed 2016-06-22 13:47:54 +02:00
88dc7f4199 removing DKIM and useless classes 2016-06-22 13:47:54 +02:00
9652f75028 Merge pull request #530 from mailpoet/fix_safari_es6_bug
removed ES6 syntax from non converted JS file - fixes #529 (Safari bug)
2016-06-22 14:29:57 +03:00
36c32db2d1 Merge pull request #528 from mailpoet/listing_sorting
Listing sorting + bugfixes
2016-06-22 14:24:05 +03:00
fd12bd557e fixed 'Setup' link in homepage 2016-06-22 12:21:26 +02:00
7bd8ed4639 Use promises for handleBulkAction
- fixed filters not being updated when going back/forward
- improved redirection to "all" group after emptying the trash (former way became buggy)
- fixed error thrown by "onGetItems" -> this logic has to go at some point
- Newsletters listing are sorted by "updated_at" desc
- Subscribers are sorted by "created_at" desc (Subscribed on)
2016-06-21 22:36:13 +02:00
ca0e511efd removed ES6 syntax from non converted JS file - fixes #529 (Safari bug) 2016-06-21 16:47:25 +02:00
e5f3fabcda - Moves mailer logic into Mailer Task class 2016-06-21 10:14:19 -04:00
efc9bac760 - Updates unit tests 2016-06-20 23:36:30 -04:00
ce6327c3d5 - Re-adds the old multidimensional array flatten method 2016-06-20 23:35:47 -04:00
f32d6bb331 - Joins bulk and individual processing into one method
- Refactors code as per code review comments
2016-06-20 23:12:32 -04:00
e807aad814 - Updates array flatten function for multidimensional arrays
- Removes custom array unique method for multidimensional arrays
2016-06-20 11:50:54 -04:00
b87754ca30 Listing setParam only needs to be run when url history is specified
- added missing code to deleteManySubscriptions() so that it doesn't remove from all segments
2016-06-20 17:28:19 +02:00
22dfb372ec - Updates bulk insert logic 2016-06-20 10:34:41 -04:00
674bbd728e updated Subscriber unit test to use model constants - no fix here 2016-06-20 16:30:34 +02:00
68c09b8678 Sorting for all listings & bugfixes for all listings except Newsletters
- newsletters listing now uses hash history
- newsletters are sorted by Subject (a->z)
- segments are sorted by Name (a->z)
- re-added WordPress Users list as a segment you can send a newsletter to
- added explicit error messages when an auto newsletter isn't fully configured
- added missing strings for "selectAll" in Segments listing
- fixed filters() in Subscribers listing (wrong count as it was not taking groups/filters/search into account)
2016-06-20 16:23:27 +02:00
c83ab0886f - Rebases master 2016-06-19 22:10:18 -04:00
999a0b3ede - Refactors sending queue worker by breaking it into smaller tasks
- Adds arrayUnique method to Helpers for multidimensional arrays
2016-06-17 14:52:56 -04:00
6daecd6466 - Fixes URL extraction (undefined index notice)
- Updates link replacement in text body
- Updates links saving logic
2016-06-17 14:52:33 -04:00
7af2775972 Allowed ability to set default sort_by/order on listings
- improved performance of listings (less refresh of items)
- fixed sorting issue where the order would not be reversed
2016-06-17 17:27:40 +02:00
4bb1acf493 Bump up release version to 0.0.33 2016-06-17 17:14:19 +03:00
c8cd3d3eb5 Merge pull request #526 from mailpoet/copy_review
copy_review
2016-06-17 16:30:35 +03:00
2360c4d6e4 Fix periodicity strings 2016-06-17 16:28:40 +03:00
36e9168eef Escape quotes where needed 2016-06-17 16:06:03 +03:00
fb79d189d7 Edits June 17 2016 2016-06-17 15:09:25 +03:00
12330d6d34 Copy edits 6/15/2016 2016-06-17 15:09:25 +03:00
5efbcfd9c1 Update June 13 2016-06-17 15:09:24 +03:00
75240fc2e1 Merge pull request #508 from mailpoet/newsletters_listing
Newsletters multi-listing
2016-06-17 15:01:30 +03:00
b6fabcc739 removing some leftover trailing commas. 2016-06-17 13:16:20 +02:00
269ddae93a Refactored scheduling options for React (semi-converted to ES6 too)
- fixed issue with Pausing sending (missing self::)
2016-06-17 13:05:46 +02:00
90c3f0e4e4 only update status to Sent for Standard newsletters 2016-06-16 20:31:47 +02:00
dd8c54aae3 removed useless newsletters/list.jsx
- removed constant from Scheduler since it's defined on the SendingQueue model
2016-06-16 20:08:42 +02:00
aa3a46b941 Status update of newsletters completed
- duplicate newsletter now includes options as well
- fixed NaN issue in statistics when newsletter is being sent
- use constant for scheduled (and put it as the sendingQueue Model level)
2016-06-16 20:01:53 +02:00
744455f0df removed useless methods 2016-06-16 12:21:54 +02:00
9aa25446d1 fixed unit tests 2016-06-15 16:33:48 +02:00
6199caea29 - Notification settings column
- added "width" option to listing headers/columns
2016-06-15 16:33:48 +02:00
d6a68dd4d0 settings column done for welcome emails + WordPress capitalization fix 2016-06-15 16:33:48 +02:00
ee6e261c42 Conditional display of statistics column (for standard)
- improved duplicate action (for standard)
- moved STATUS_COMPLETED constant from worker to SendingQueue model where it belongs
2016-06-15 16:26:42 +02:00
cabfd8a946 better with the proper type 2016-06-15 16:26:42 +02:00
cf712636ed progress on notification type listing + NL model improvements 2016-06-15 16:26:42 +02:00
873c3d15a0 Fixed Setting::getValue issue where defaults were not returned for single keys
- updated static strings with constants
2016-06-15 16:26:42 +02:00
bc1bd3bad1 commenting on a react quirk 2016-06-15 16:26:42 +02:00
9f971632c9 update status in welcome listing 2016-06-15 16:26:42 +02:00
91bc0505ac Welcome emails progress 2016-06-15 16:26:42 +02:00
90c94624cc added preview link for standard newsletters 2016-06-15 16:26:42 +02:00
cd412894c6 Refactored filtering (groups / status / type)
- standard listing close to completion (missing item actions)
- enabled tracking by default on install
2016-06-15 16:26:42 +02:00
ecf15d53d9 Newsletters listing
- added stylesheet for newsletters listing
- added "status" database column on Newsletters for grouping in listings
- added duplicate link to standard newsletters
2016-06-15 16:15:02 +02:00
a593347336 call groups/filters only if Model has defined those methods 2016-06-15 16:15:02 +02:00
22566869cb tab system for newsletters listing 2016-06-15 16:14:06 +02:00
c959e7ec96 fixed total count and filtering + basic tab implementation in React 2016-06-15 16:14:06 +02:00
86a2846215 Tab system for listings 2016-06-15 16:14:06 +02:00
3b97a26a8a Newsletters multi-listing 2016-06-15 16:14:06 +02:00
dc6c973574 Merge pull request #523 from mailpoet/editor_ui
Email Editor: round 2 fixes
2016-06-14 15:26:22 +02:00
2a3a561464 updated shortcode (user -> subscriber) in FrankRoast template 2016-06-14 15:25:23 +02:00
e5f45fb7ad Merge pull request #524 from mailpoet/link_processing_fix
Prevents URLs in link titles from being processed when tracking is enabled
2016-06-14 15:21:18 +02:00
f22cadd319 - Declares hash length as constant
- Introduces check for nonexistent values/updates loop condition
2016-06-14 09:02:08 -04:00
5ea25ec697 Fix the way we query for WP subscriber in email preview 2016-06-14 15:43:13 +03:00
c0a250fc0f Turn sidebar/sidepanel text size into a variable 2016-06-14 15:26:00 +03:00
e69aa792c4 - Prevents URLs in link titles from being processed when tracking is enabled. Closes #519 2016-06-13 21:13:23 -04:00
3b4ac4d2d2 Change newsletter preview to use current user as subscriber 2016-06-13 15:02:20 +03:00
781973777e Vertically and horizontally center block deletion confirmation dialog 2016-06-13 14:13:47 +03:00
8698d2c6ba Change placeholder text of preheader input 2016-06-13 13:06:59 +03:00
47c15eca83 Change sidebar and sidepanel text font size to 13px 2016-06-13 13:06:59 +03:00
64f4bed080 Bump up release version to 0.0.32 2016-06-10 17:25:38 +03:00
fe47ba8a38 Merge pull request #522 from mailpoet/tests_fix
Fix PHP unit tests
2016-06-10 10:12:53 -04:00
eb02adc7ba Exclude lib/Util/Helpers.php class from unit test coverage calculations 2016-06-10 16:54:11 +03:00
f257b503e9 - Fix unit tests to account for translation changes;
- Exclude 3rd party utility libraries from coverage calculations;
2016-06-10 15:06:44 +03:00
bfdabe3554 Merge pull request #521 from mailpoet/copy_review
Copy review
2016-06-10 12:36:47 +03:00
77dd71935a Update - June 10 2016 2016-06-10 11:13:40 +02:00
4b418f041b Merge pull request #518 from mailpoet/alc_posts_ui
ALC & Posts widgets UI fixes
2016-06-09 16:19:57 +02:00
c8a0e006a0 Fix Select2 placeholder in Posts settings 2016-06-09 15:42:01 +03:00
359119d896 Disable dragging with right click, fixes #517 2016-06-09 13:34:26 +03:00
1a3c767601 - Fix double HR tag issue on ALC/Posts block settings;
- Change "Drop content here" message to a custom one for Posts/ALC blocks
2016-06-09 13:34:26 +03:00
6a97e82d42 Fix select2 placeholder text not appearing for Posts widget 2016-06-09 13:34:26 +03:00
3edfd32879 - Add highlighting of blocks that are being edited;
- Refactor block settings views;
- Change Posts widget to display 8 posts in settings;
- Move ALC/Posts category selector label to Select2 placeholder.
2016-06-09 13:34:26 +03:00
33bdde1156 Merge pull request #516 from mailpoet/unit_tests
Adds unit test for open/unsubscribe statistics
2016-06-09 12:54:16 +03:00
710cab64c3 - Fixes error due to: "the blacklist functionality has been removed from
PHPUnit 5, please remove blacklist section from configuration"
2016-06-08 21:26:23 -04:00
ed707b1738 - Adds unit test for unsubscribe statistics 2016-06-08 21:25:40 -04:00
398903e8b8 - Adds unit test for open statistics 2016-06-08 12:38:52 -04:00
d590f5ea98 Merge pull request #512 from mailpoet/preview_link_refactoring
Extracts browser preview URl logic into a separate class
2016-06-08 17:12:10 +02:00
d6cbe5aac8 - Fixes incorrect shortcode name
- Updates unit test
2016-06-08 11:09:33 -04:00
08e6430c7d June 8 2016 Copy review 2016-06-08 17:02:50 +02:00
945fe66bbb Merge pull request #514 from mailpoet/model_cleanup
Removes unused method from the base model
2016-06-08 15:13:51 +02:00
8e3eb2b795 Merge pull request #515 from mailpoet/unit_tests
Adds unit test for click statistics
2016-06-08 16:02:42 +03:00
52fbc0ee8a Merge pull request #513 from mailpoet/renderer_fix
Rendering fix
2016-06-08 12:56:22 +03:00
a355228b93 Test Commit
This is an initial test commit for the copy review.
2016-06-08 11:31:04 +02:00
2cb0b3b071 - Adds unit test for click statistics 2016-06-07 21:57:04 -04:00
bc1fb235d3 - Removes unused method from the base model. Closes #511 2016-06-07 18:47:00 -04:00
713dda913e - Fixes rendering issue where DOMDocument throws a notice on unescaped
html entity
2016-06-07 12:27:40 -04:00
d182638971 - Updates references to the new view in browser URL class
- Removes unnecessary rtrim condition in URL generation
2016-06-07 10:53:01 -04:00
a5c620acf3 - Updates the way the view in browser URL is constructed 2016-06-07 10:41:19 -04:00
c176ad1d16 - Updates based on code review comments 2016-06-07 10:14:37 -04:00
14c2b4d90f - Changes location for the main view in browser class
- Updates code formattign for case statements
2016-06-07 09:28:29 -04:00
03eb4ad0fc - Changes location for the view in browser URL class 2016-06-07 09:16:48 -04:00
ba9cd15651 - Extracts view in browser URl logic into a separate class 2016-06-07 09:08:01 -04:00
329ec63dfd Bump up release version to 0.0.31 2016-06-03 18:29:40 +03:00
4925c7868e Merge pull request #510 from mailpoet/twig_deprecation
Twig deprecation notice (latest version)
2016-06-03 18:10:53 +03:00
13d28d0aa7 implemented interface in our Twig extension to comply with latest Twig standards 2016-06-03 15:07:30 +02:00
c7d3c79fe3 Merge pull request #509 from mailpoet/unit_tests
- Increases Mailer unit test coverage to 100%
2016-06-02 19:38:43 +03:00
1e9da724ea - Updates exception test logic 2016-06-02 12:33:58 -04:00
645d4e15ab - Updates unit test 2016-06-02 11:03:16 -04:00
cad5b242b2 - Increases Mailer unit test coverage to 100% 2016-06-02 10:11:36 -04:00
99a81042c1 Merge pull request #507 from mailpoet/custom_shortcodes
Implements shortcodes for custom fields
2016-06-01 17:09:38 +03:00
61987a204e - Fixes custom field shortcode matching logic 2016-06-01 09:59:45 -04:00
a208104fc8 - Fixes naming convention 2016-06-01 09:40:59 -04:00
00ccc8adf4 Merge pull request #506 from mailpoet/alc_update
Multiple ALC block support for newsletter editor
2016-06-01 15:15:26 +02:00
df0ed9ce53 Rename mailpoet_custom_fields symlink to mailpoet_shortcodes 2016-06-01 16:00:05 +03:00
26d9b915a2 - Adds unit test for shortcodes helper class 2016-05-31 21:30:38 -04:00
16cb91990b - Updates unit test 2016-05-31 20:08:45 -04:00
9642d3e672 - Renames all references of "custom fields" to "shortcodes" 2016-05-31 11:25:16 -04:00
aed60e6905 - Updates menu/editor view to work with the refactored shortcodes logic 2016-05-31 11:04:10 -04:00
da7615ba4c - Removes redundant shortcode description
- Implements shortcode processing for custom fields
2016-05-31 11:03:04 -04:00
3eb6a21980 - Centralizes a list of all shortcodes
- Returns all shortcodes with custom fields
2016-05-31 11:02:08 -04:00
b4e371302c Fix PHP coding style based on feedback 2016-05-31 17:50:38 +03:00
e6724b1d4a Change unsubscribe verifier to check for "Unsubscribe" shortcode
presence
2016-05-31 16:29:10 +03:00
2b6e87c3a7 Force TinyMCE to use absolute URLs 2016-05-31 16:12:33 +03:00
b01ee80ec2 Update Backbone, Marionette, Backbone Radio, TinyMCE dependency versions 2016-05-31 15:14:36 +03:00
5d48ecac80 Add a method to bulk update ALC blocks in newsletter editor 2016-05-31 13:53:45 +03:00
ebdb826011 Bump up release version to 0.0.30 2016-05-27 18:31:35 +03:00
9dc725e34d Merge pull request #488 from mailpoet/wp_users
Wp users
2016-05-27 18:26:45 +03:00
f47c331a5b updated db schema and fixed unit test missing Segment cleanup after 2016-05-27 15:38:24 +02:00
b45c70f32b removed status from subscribeManyToSegments() query 2016-05-27 14:18:02 +02:00
cf33d6f066 removed extra spaces 2016-05-27 14:15:46 +02:00
8292e9a744 Revert batch processing on bulk actions - too buggy
- minor fixes and cleanup
2016-05-27 14:15:46 +02:00
3c46a5b434 Optimized Bulk actions
- Updated SQL schema for every created_at column so that it has a default value
- Updated unit tests based on recent changes (new methods in SubscriberSegment model)
- Added check for HelpScout initialization code so that it doesn't throw errors
2016-05-27 14:15:46 +02:00
4a4c4e093a Added unit tests for the WP segment
- moved WP segment creation to the Segment model
2016-05-27 14:14:35 +02:00
4fa8a650b8 Added unit tests for SubscriberSegment / Subscriber models 2016-05-27 14:14:35 +02:00
da755b7902 Renamed method names for better clarity + refactoring
- renamed getWPUsers() to getWPSegment()
- renamed SubscriberSegment methods
2016-05-27 14:14:35 +02:00
ceebb18bdf minor spacing fix 2016-05-27 14:14:35 +02:00
d10a29598d prevent deletion of WP Users segment in Segments listing 2016-05-27 14:14:35 +02:00
8c56c8da5e Fixed bulk actions (return false if no items were selected)
- added missing check for WPUsers segment in case it does not exist
2016-05-27 14:14:35 +02:00
c4ddb38d18 Prevent WP users from being trashed/deleted
- return actual rowCount of affected rows for bulk actions (based on PDO last statement)
- prevent removal of WP Users segment relationship with subscribers.
2016-05-27 14:14:35 +02:00
15a21e5745 fix segments loaded on subscribers page + removed counts for bulk actions' segments 2016-05-27 14:14:35 +02:00
7df1a856ea Merge pull request #501 from mailpoet/import_fix
Import fix
2016-05-27 14:19:32 +03:00
69381205a2 - Updates unit test 2016-05-27 07:16:11 -04:00
9e0d8056b3 - Changes success/error notices font size to 13px in import and export 2016-05-27 07:16:11 -04:00
4b85c57436 - Updates Export notification class
- Updates Export "back to subscribers" button language
2016-05-27 07:16:11 -04:00
377498be1d - Removes validation of MailChimp API key
- Refactors import class
- Creates new method in Newsletter model to select welcome notifications
  for specific segments
- Updates Step 2 (error) and Step 3 (success) notices
- Gives MenuBootstrap class a comprehensible name
2016-05-27 07:16:11 -04:00
142421ad48 - Updates unit test 2016-05-27 07:16:11 -04:00
768115b794 - Disables "next step" button on import's step 2 when no segments are
selected
2016-05-27 07:16:11 -04:00
8b9d76db8a - Displays notice on step 3 of import when subscribers are added to a
segment with welcome notification enabled
2016-05-27 07:16:11 -04:00
f17c78fda2 - Updates Segment model to return segments even when there are no
subscribers
2016-05-27 07:16:11 -04:00
3d45a8b7d4 - Updates subscriber/subscriber_segment status using const values 2016-05-27 07:14:34 -04:00
3888241cbd - Simplified date matching logic by using Moment.js 2016-05-27 07:14:34 -04:00
603b6749de - Styles the import results notice to look like WP's "update" 2016-05-27 07:14:34 -04:00
22918ecfd1 - Updates the wording of the "back to list" button 2016-05-27 07:14:34 -04:00
70ded73b51 - Updates the look of the MailChimp API key "verify" button 2016-05-27 07:14:34 -04:00
da147047ec - Updates import email regex to use standard HTML5 regex
- Improves email detection/filtering logic
2016-05-27 07:14:34 -04:00
0e24174373 Merge pull request #486 from mailpoet/export_fix
Fixes segment subscriber count when status is "unsubscribed"
2016-05-26 11:40:38 +02:00
bc9b4eeb19 - Update Segment model/test to use const values for subscriber status 2016-05-25 17:31:38 -04:00
c6b13c5175 Merge pull request #498 from mailpoet/rendering_fix
Fixes a couple of rendering issues
2016-05-25 14:23:15 +03:00
f754b1d1b2 - Applies text alignment to ALC block
- Prevents duplicate column content
2016-05-24 15:41:26 -04:00
bd5300d69a Merge pull request #495 from mailpoet/standard_newsletter_fix
Fix scheduling immediate standard newsletters
2016-05-24 12:02:01 -04:00
9996f3ef41 Change Scheduler to use Newsletter object, not array 2016-05-24 17:57:34 +03:00
0f95d7bc8a Use Scheduler to schedule next post notification sending timestamp 2016-05-24 17:08:34 +03:00
14098643ae Fix scheduling immediate standard newsletters 2016-05-24 16:04:42 +03:00
7c2d5a45c5 - Updates unit test 2016-05-20 12:12:29 -04:00
9d5902e179 - Fixes segment subscriber count when status is "unsubscribed" 2016-05-20 11:35:58 -04:00
251 changed files with 11506 additions and 7181 deletions

View File

@ -141,6 +141,26 @@ class RoboFile extends \Robo\Tasks {
$this->_exec('vendor/bin/codecept run -g failed');
}
function qaLint() {
$this->_exec('./tasks/php_lint.sh lib/ tests/');
}
function qaCodeSniffer($severity='errors') {
if ($severity === 'all') {
$severityFlag = '-w';
} else {
$severityFlag = '-n';
}
$this->_exec(
'./vendor/bin/phpcs '.
'--standard=./tasks/code_sniffer/MailPoet '.
'--ignore=./lib/Util/Sudzy/*,./lib/Util/CSS.php,./lib/Util/XLSXWriter.php,'.
'./lib/Config/PopulatorData/Templates/* '.
'lib/ '.
$severityFlag
);
}
protected function loadEnv() {
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();

View File

@ -9,6 +9,8 @@
@require 'form_editor'
@require 'listing'
@require 'listing/newsletters'
@require 'box'
@require 'breadcrumb'

View File

@ -27,13 +27,12 @@
img
min-width: 150px
min-height: 150px
height: auto
width: 110%
position: relative
top: 0
top: 50%
left: 50%
transform: translate(-50%, 0%)
transform: translate(-50%, -50%)
.mailpoet_overlay
position: absolute

View File

@ -0,0 +1,3 @@
#newsletters_container
h2.nav-tab-wrapper
margin-bottom: 1rem

View File

@ -46,6 +46,7 @@ $master-column-tool-width = 24px
opacity: 0
overflow: hidden
display: block
margin: 0
.mailpoet_delete_block_activated
width: auto
@ -125,7 +126,6 @@ $master-column-tool-width = 24px
border-radius(3px)
background-color: $warning-background-color
padding: 3px 5px
line-height: 1.2em
.mailpoet_delete_block_activate
overflow: hidden

View File

@ -4,7 +4,6 @@
.mailpoet_form_field_title
clear: both
font-size: 1.1em
margin-bottom: 5px
.mailpoet_form_field_title_small

View File

@ -17,6 +17,7 @@ $widget-icon-width = 30px
border-left: $content-border-color
border-bottom: $content-border-color
color: $sidebar-text-color
font-size: $sidebar-text-size
.mailpoet_sidebar_region
margin-bottom: 0

View File

@ -3,6 +3,7 @@ $sidepanel-active-heading-color = $primary-active-color
/* Sidepanel */
.mailpoet_editor_settings
color: $sidebar-text-color
font-size: $sidebar-text-size
p
font-size: 1em
@ -18,7 +19,6 @@ $sidepanel-active-heading-color = $primary-active-color
.mailpoet_sidepanel_field_title
clear: both
font-size: 1.1em
margin-bottom: 5px
.mailpoet_sidepanel_field_title_small

View File

@ -23,6 +23,7 @@ $block-text-line-height = $text-line-height
border: 1px solid $transparent-color
&:hover > .mailpoet_block_highlight
&.mailpoet_highlight > .mailpoet_block_highlight
border: 1px dashed $block-hover-highlight-color

View File

@ -17,7 +17,7 @@ $three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
padding-left: 0
padding-right: 0
&:hover
&:hover > .mailpoet_block_highlight
border: 0
.mailpoet_container_vertical > *

View File

@ -31,7 +31,7 @@ div.mce-toolbar-grp.mce-container
box-shadow(0px 0px 3px 1px rgba(0, 0, 0, 0.05))
.mce-window
/* Fix TinyMCE mailpoet_custom_fields window lack of hiding overflow */
/* Fix TinyMCE mailpoet_shortcodes window lack of hiding overflow */
div.mce-container-body.mce-abs-layout
overflow: hidden
@ -40,8 +40,8 @@ div.mce-toolbar-grp.mce-container
width: -webkit-calc( 100% - 36px )
width: calc( 100% - 36px )
/* TinyMCE mailpoet_custom_fields toolbar icon */
.mce-i-mailpoet_custom_fields:before
/* TinyMCE mailpoet_shortcodes toolbar icon */
.mce-i-mailpoet_shortcodes:before
font: 400 20px/1 dashicons!important
content: "\f307"
@ -84,7 +84,7 @@ position: relative
body
overflow-x: auto
/* Hide the "Details" section of Wordpress Media manager */
/* Hide the "Details" section of WordPress Media manager */
.media-sidebar
display: none
@ -98,7 +98,7 @@ body
.attachments-browser .uploader-inline
right: 0
/* Remove max width from date selector in Wordpress Media Manager */
/* Remove max width from date selector in WordPress Media Manager */
#media-attachment-date-filters
max-width: calc(100% - 12px)

View File

@ -26,3 +26,4 @@ $error-text-color = #d54e21
$newsletter-width = 660px
$text-line-height = 1.6em
$sidebar-text-size = 13px

View File

@ -18,6 +18,7 @@ textarea.parsley-error
list-style-type none
font-size 0.9em
line-height 0.9em
color #B94A48
opacity 0
transition all .3s ease-in
-o-transition all .3s ease-in

View File

@ -1,14 +1,10 @@
#mailpoet_settings
// common
.mailpoet_panel
display: none
display none
.form-table th
width:20em
// advanced
#mailpoet_role_permissions
margin-top: 20px;
width 20em
// sending methods
.mailpoet_sending_methods
@ -28,8 +24,7 @@
line-height 54px
font-size 1.5em
.mailpoet_description
line-height 1.5em
font-size 1.1em
font-size 14px
.mailpoet_status
background-color #2f2f2f
color #fff
@ -60,11 +55,11 @@
// responsive
@media screen and (max-width: 782px)
.form-table th
width: auto
width auto
.mailpoet_sending_methods
li
float none
width: auto
margin-right: 0
width auto
margin-right 0

View File

@ -1 +0,0 @@
../src/newsletter_editor/tinymce/mailpoet_custom_fields

View File

@ -0,0 +1 @@
../src/newsletter_editor/tinymce/mailpoet_shortcodes

View File

@ -43,8 +43,9 @@ define('date',
options = options || {};
this.init(options);
return Moment(date, this.convertFormat(options.parseFormat))
.format(this.convertFormat(this.options.format));
var date = Moment(date, this.convertFormat(options.parseFormat));
if (options.offset === 0) date = date.utc();
return date.format(this.convertFormat(this.options.format));
},
toDate: function(date, options) {
options = options || {};
@ -68,7 +69,7 @@ define('date',
});
},
convertFormat: function(format) {
const format_mappings = {
var format_mappings = {
date: {
D: 'ddd',
l: 'dddd',
@ -124,9 +125,9 @@ define('date',
if (!format || format.length <= 0) return format;
const replacements = format_mappings['date'];
var replacements = format_mappings['date'];
let outputFormat = '';
var outputFormat = '';
Object.keys(replacements).forEach(function(key) {
if (format.indexOf(key) !== -1) {

View File

@ -6,7 +6,7 @@ function(
) {
const FormFieldCheckbox = React.createClass({
onValueChange: function(e) {
e.target.value = this.refs.checkbox.checked ? '1' : '';
e.target.value = this.refs.checkbox.checked ? '1' : '0';
return this.props.onValueChange(e);
},
render: function() {
@ -14,7 +14,9 @@ function(
return false;
}
const isChecked = !!(this.props.item[this.props.field.name]);
// isChecked will be true only if the value is "1"
// it will be false in case value is "0" or empty
const isChecked = !!(~~(this.props.item[this.props.field.name]));
const options = Object.keys(this.props.field.values).map(
(value, index) => {
return (

View File

@ -27,7 +27,7 @@ define([
}
return (
<select
name={ this.props.name + '[year]' }
name={ `${this.props.name}[year]` }
value={ this.props.year }
onChange={ this.props.onValueChange }
>
@ -57,7 +57,7 @@ define([
}
return (
<select
name={ this.props.name + '[month]' }
name={ `${this.props.name}[month]` }
value={ this.props.month }
onChange={ this.props.onValueChange }
>
@ -88,7 +88,7 @@ define([
return (
<select
name={ this.props.name + '[day]' }
name={ `${this.props.name}[day]` }
value={ this.props.day }
onChange={ this.props.onValueChange }
>
@ -102,46 +102,99 @@ define([
constructor(props) {
super(props);
this.state = {
year: undefined,
month: undefined,
day: undefined
year: '',
month: '',
day: ''
}
}
componentDidMount() {
this.extractDateParts();
}
componentDidUpdate(prevProps, prevState) {
if (
(this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id)
) {
this.extractTimeStamp();
this.extractDateParts();
}
}
extractTimeStamp() {
const timeStamp = parseInt(this.props.item[this.props.field.name], 10);
extractDateParts() {
const value = (this.props.item[this.props.field.name] !== undefined)
? this.props.item[this.props.field.name].trim()
: '';
if(value === '') {
return;
}
const dateType = this.props.field.params.date_type;
const dateParts = value.split('-');
let year = '';
let month = '';
let day = '';
switch(dateType) {
case 'year_month_day':
year = ~~(dateParts[0]);
month = ~~(dateParts[1]);
day = ~~(dateParts[2]);
break;
case 'year_month':
year = ~~(dateParts[0]);
month = ~~(dateParts[1]);
break;
case 'month':
month = ~~(dateParts[0]);
break;
case 'year':
year = ~~(dateParts[0]);
break;
}
this.setState({
year: Moment.unix(timeStamp).year(),
// Moment returns the month as [0..11]
// We increment it to match PHP's mktime() which expects [1..12]
month: Moment.unix(timeStamp).month() + 1,
day: Moment.unix(timeStamp).date()
year: year,
month: month,
day: day
});
}
updateTimeStamp(field) {
let newTimeStamp = Moment(
`${this.state.month}/${this.state.day}/${this.state.year}`,
'M/D/YYYY'
).valueOf();
if (~~(newTimeStamp) > 0) {
// convert milliseconds to seconds
newTimeStamp /= 1000;
return this.props.onValueChange({
target: {
name: field,
value: newTimeStamp
}
});
formatValue() {
const dateType = this.props.field.params.date_type;
let value;
switch(dateType) {
case 'year_month_day':
value = {
'year': this.state.year,
'month': this.state.month,
'day': this.state.day
};
break;
case 'year_month':
value = {
'year': this.state.year,
'month': this.state.month
};
break;
case 'month':
value = {
'month': this.state.month
};
break;
case 'year':
value = {
'year': this.state.year
};
break;
}
return value;
}
onValueChange(e) {
// extract property from name
@ -153,25 +206,29 @@ define([
field = matches[1];
property = matches[2];
let value = parseInt(e.target.value, 10);
let value = ~~(e.target.value);
this.setState({
[`${property}`]: value
}, () => {
this.updateTimeStamp(field);
this.props.onValueChange({
target: {
name: field,
value: this.formatValue()
}
});
});
}
}
render() {
const monthNames = window.mailpoet_month_names || [];
const dateFormats = window.mailpoet_date_formats || {};
const dateType = this.props.field.params.date_type;
const dateSelects = dateType.split('_');
const dateSelects = dateFormats[dateType][0].split('/');
const fields = dateSelects.map(type => {
switch(type) {
case 'year':
case 'yyyy':
return (<FormFieldDateYear
onValueChange={ this.onValueChange.bind(this) }
ref={ 'year' }
@ -182,7 +239,7 @@ define([
/>);
break;
case 'month':
case 'mm':
return (<FormFieldDateMonth
onValueChange={ this.onValueChange.bind(this) }
ref={ 'month' }
@ -194,7 +251,7 @@ define([
/>);
break;
case 'day':
case 'dd':
return (<FormFieldDateDay
onValueChange={ this.onValueChange.bind(this) }
ref={ 'day' }

View File

@ -1,4 +1,5 @@
import React from 'react'
import _ from 'underscore'
const FormFieldSelect = React.createClass({
render() {
@ -8,6 +9,7 @@ const FormFieldSelect = React.createClass({
let filter = false;
let placeholder = false;
let sortBy = false;
if (this.props.field.placeholder !== undefined) {
placeholder = (
@ -19,7 +21,27 @@ const FormFieldSelect = React.createClass({
filter = this.props.field.filter;
}
const options = Object.keys(this.props.field.values).map(
if (_.isFunction(this.props.field.sortBy)) {
sortBy = this.props.field.sortBy;
}
let keys;
if (sortBy) {
// Extract keys from sorted [key, value] select value pairs, sorted by
// provided sorting order.
keys =
_.map(
_.sortBy(
_.pairs(this.props.field.values),
(item) => sortBy(item[0], item[1])
),
(item) => item[0]
);
} else {
keys = Object.keys(this.props.field.values)
}
const options = keys.map(
(value, index) => {
if (filter !== false && filter(this.props.item, value) === false) {

View File

@ -13,8 +13,7 @@ const columns = [
},
{
name: 'segments',
label: MailPoet.I18n.t('segments'),
sortable: false
label: MailPoet.I18n.t('segments')
},
{
name: 'created_at',
@ -90,7 +89,7 @@ const item_actions = [
}
},
{
name: 'duplicate_form',
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(item, refresh) {
return MailPoet.Ajax.post({
@ -98,9 +97,11 @@ const item_actions = [
action: 'duplicate',
data: item.id
}).done(function(response) {
if (response !== false && response['name'] !== undefined) {
MailPoet.Notice.success(
(MailPoet.I18n.t('formDuplicated')).replace('%$1s', response.name)
);
}
refresh();
});
}

View File

@ -47,13 +47,13 @@ function(
data.action = this.state.action;
var callback = function() {};
var onSuccess = function() {};
if(action['onSuccess'] !== undefined) {
callback = action.onSuccess;
onSuccess = action.onSuccess;
}
if(data.action) {
this.props.onBulkAction(selected_ids, data, callback);
this.props.onBulkAction(selected_ids, data).then(onSuccess);
}
this.setState({

View File

@ -10,10 +10,10 @@ function(
) {
var ListingFilters = React.createClass({
handleFilterAction: function() {
let filters = {}
let filters = {};
this.getAvailableFilters().map((filter, i) => {
filters[this.refs['filter-'+i].name] = this.refs['filter-'+i].value
})
});
return this.props.onSelectFilter(filters);
},
handleEmptyTrash: function() {
@ -21,7 +21,6 @@ function(
},
getAvailableFilters: function() {
let filters = this.props.filters;
return Object.keys(filters).filter(function(filter) {
return !(
filters[filter].length === 0
@ -30,26 +29,29 @@ function(
&& !filters[filter][0].value
)
);
})
});
},
componentDidUpdate: function() {
const selected_filters = this.props.filter;
const available_filters = this.getAvailableFilters().map(
function(filter, i) {
if (selected_filters[filter] !== undefined && selected_filters[filter]) {
jQuery(this.refs['filter-'+i])
.val(selected_filters[filter])
.trigger('change');
}
}.bind(this)
);
},
render: function() {
const filters = this.props.filters;
const selected_filters = this.props.filter;
const available_filters = this.getAvailableFilters()
.map(function(filter, i) {
let default_value = false;
if (selected_filters[filter] !== undefined && selected_filters[filter]) {
default_value = selected_filters[filter]
} else {
jQuery(`select[name="${filter}"]`).val('');
}
return (
<select
ref={ `filter-${i}` }
key={ `filter-${i}` }
name={ filter }
defaultValue={ default_value }
>
{ filters[filter].map(function(option, j) {
return (
@ -63,7 +65,7 @@ function(
);
}.bind(this));
let button = false;
let button;
if (available_filters.length > 0) {
button = (
@ -76,7 +78,7 @@ function(
);
}
let empty_trash = false;
let empty_trash;
if (this.props.group === 'trash') {
empty_trash = (
<input

View File

@ -1,21 +1,15 @@
define([
'react',
'classnames',
'mailpoet'
], function(
React,
classNames,
MailPoet
) {
import MailPoet from 'mailpoet'
import React from 'react'
import classNames from 'classnames'
var ListingHeader = React.createClass({
const ListingHeader = React.createClass({
handleSelectItems: function() {
return this.props.onSelectItems(
this.refs.toggle.checked
);
},
render: function() {
var columns = this.props.columns.map(function(column, index) {
const columns = this.props.columns.map(function(column, index) {
column.is_primary = (index === 0);
column.sorted = (this.props.sort_by === column.name)
? this.props.sort_order
@ -29,7 +23,7 @@ define([
);
}.bind(this));
var checkbox = false;
let checkbox;
if(this.props.is_selectable === true) {
checkbox = (
@ -57,21 +51,21 @@ define([
}
});
var ListingColumn = React.createClass({
const ListingColumn = React.createClass({
handleSort: function() {
var sort_by = this.props.column.name,
sort_order = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
const sort_by = this.props.column.name;
const sort_order = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
this.props.onSort(sort_by, sort_order);
},
render: function() {
var classes = classNames(
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) }
);
var label;
let label;
if(this.props.column.sortable === true) {
label = (
@ -87,12 +81,11 @@ define([
<th
className={ classes }
id={this.props.column.name }
scope="col">
{label}
</th>
scope="col"
width={ this.props.column.width || null }
>{label}</th>
);
}
});
return ListingHeader;
});
module.exports = ListingHeader;

View File

@ -1,33 +1,16 @@
define(
[
'mailpoet',
'jquery',
'react',
'react-router',
'classnames',
'listing/bulk_actions.jsx',
'listing/header.jsx',
'listing/pages.jsx',
'listing/search.jsx',
'listing/groups.jsx',
'listing/filters.jsx'
],
function(
MailPoet,
jQuery,
React,
Router,
classNames,
ListingBulkActions,
ListingHeader,
ListingPages,
ListingSearch,
ListingGroups,
ListingFilters
) {
var Link = Router.Link;
import MailPoet from 'mailpoet'
import jQuery from 'jquery'
import React from 'react'
import { Router, Link } from 'react-router'
import classNames from 'classnames'
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'
var ListingItem = React.createClass({
const ListingItem = React.createClass({
getInitialState: function() {
return {
toggled: true
@ -150,8 +133,10 @@ define(
);
}
let actions;
if (this.props.group === 'trash') {
var actions = (
actions = (
<div>
<div className="row-actions">
<span>
@ -183,7 +168,7 @@ define(
</div>
);
} else {
var actions = (
actions = (
<div>
<div className="row-actions">
{ item_actions }
@ -197,7 +182,7 @@ define(
);
}
var row_classes = classNames({ 'is-expanded': !this.state.toggled })
const row_classes = classNames({ 'is-expanded': !this.state.toggled });
return (
<tr className={ row_classes }>
@ -209,7 +194,7 @@ define(
});
var ListingItems = React.createClass({
const ListingItems = React.createClass({
render: function() {
if (this.props.items.length === 0) {
return (
@ -231,8 +216,7 @@ define(
</tbody>
);
} else {
var selectAllClasses = classNames(
const select_all_classes = classNames(
'mailpoet_select_all',
{ 'mailpoet_hidden': (
this.props.selection === false
@ -243,7 +227,7 @@ define(
return (
<tbody>
<tr className={ selectAllClasses }>
<tr className={ select_all_classes }>
<td colSpan={
this.props.columns.length
+ (this.props.is_selectable ? 1 : 0)
@ -294,7 +278,7 @@ define(
}
});
var Listing = React.createClass({
const Listing = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
@ -305,8 +289,8 @@ define(
page: 1,
count: 0,
limit: 10,
sort_by: 'id',
sort_order: 'desc',
sort_by: null,
sort_order: null,
items: [],
groups: [],
group: 'all',
@ -316,61 +300,57 @@ define(
selection: false
};
},
componentDidUpdate: function(prevProps, prevState) {
// reset group to "all" if trash gets emptied
if(
// we were viewing the trash
(prevState.group === 'trash' && prevState.count > 0)
&&
// we are still viewing the trash but there are no items left
(this.state.group === 'trash' && this.state.count === 0)
&&
// only do this when no filter is set
(Object.keys(this.state.filter).length === 0)
) {
this.handleGroup('all');
}
},
getParam: function(param) {
var regex = /(.*)\[(.*)\]/
var matches = regex.exec(param)
const regex = /(.*)\[(.*)\]/;
const matches = regex.exec(param);
return [matches[1], matches[2]]
},
initWithParams: function(params) {
let state = this.state || {}
let original_state = state
let state = this.getInitialState();
// check for url params
if (params.splat !== undefined) {
params.splat.split('/').map(param => {
let [key, value] = this.getParam(param);
switch(key) {
case 'filter':
let filters = {}
let filters = {};
value.split('&').map(function(pair) {
let [k, v] = pair.split('=')
filters[k] = v
}
)
);
state.filter = filters
state.filter = filters;
break;
default:
state[key] = value
state[key] = value;
}
})
});
}
// default overrides
// limit per page
if (this.props.limit !== undefined) {
state.limit = Math.abs(~~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, function() {
this.getItems();
}.bind(this));
},
setParams: function() {
var params = Object.keys(this.state)
if (this.props.location) {
let params = Object.keys(this.state)
.filter(key => {
return (
[
@ -384,22 +364,27 @@ define(
)
})
.map(key => {
let value = this.state[key]
let value = this.state[key];
if (value === Object(value)) {
value = jQuery.param(value)
} else if (value === Boolean(value)) {
value = value.toString()
}
if(value !== '') {
if (value !== '' && value !== null) {
return `${key}[${value}]`
}
})
.filter(key => { return (key !== undefined) })
.join('/');
params = '/'+params
if(this.props.location) {
// prepend url with "tab" if specified
if (this.props.tab !== undefined) {
params = `/${ this.props.tab }/${ params }`;
} else {
params = `/${ params }`;
}
if (this.props.location.pathname !== params) {
this.context.router.push(`${params}`);
}
@ -407,8 +392,8 @@ define(
},
componentDidMount: function() {
if (this.isMounted()) {
const params = this.props.params || {}
this.initWithParams(params)
const params = this.props.params || {};
this.initWithParams(params);
if (this.props.auto_refresh) {
jQuery(document).on('heartbeat-tick.mailpoet', function(e, data) {
@ -418,8 +403,8 @@ define(
}
},
componentWillReceiveProps: function(nextProps) {
const params = nextProps.params || {}
this.initWithParams(params)
const params = nextProps.params || {};
this.initWithParams(params);
},
getItems: function() {
if (this.isMounted()) {
@ -431,6 +416,7 @@ define(
endpoint: this.props.endpoint,
action: 'listing',
data: {
tab: (this.props.tab) ? this.props.tab : '',
offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit,
group: this.state.group,
@ -448,9 +434,10 @@ define(
loading: false
}, function() {
if (this.props['onGetItems'] !== undefined) {
this.props.onGetItems(
~~(this.state.groups[0]['count'])
);
const count = (response.groups[0] !== undefined)
? ~~(response.groups[0].count)
: 0;
this.props.onGetItems(count);
}
}.bind(this));
}.bind(this));
@ -517,16 +504,21 @@ define(
}.bind(this));
},
handleEmptyTrash: function() {
this.handleBulkAction('all', {
return this.handleBulkAction('all', {
action: 'delete',
group: 'trash'
}, function(response) {
}).then(function(response) {
if (~~(response) > 0) {
MailPoet.Notice.success(
MailPoet.I18n.t('permanentlyDeleted').replace('%d', response)
);
});
}
// redirect to default group
this.handleGroup('all');
}.bind(this));
},
handleBulkAction: function(selected_ids, params, callback) {
handleBulkAction: function(selected_ids, params) {
if (
this.state.selection === false
&& this.state.selected_ids.length === 0
@ -539,6 +531,7 @@ define(
var data = params || {};
data.listing = {
tab: (this.props.tab) ? this.props.tab : '',
offset: 0,
limit: 0,
filter: this.state.filter,
@ -549,14 +542,13 @@ define(
data.listing.selection = selected_ids;
}
MailPoet.Ajax.post({
return MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'bulkAction',
data: data
}).done(function(response) {
}).done(() => {
this.getItems();
callback(response);
}.bind(this));
});
},
handleSearch: function(search) {
this.setState({
@ -566,7 +558,6 @@ define(
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
handleSort: function(sort_by, sort_order = 'asc') {
@ -575,7 +566,6 @@ define(
sort_order: sort_order,
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
handleSelectItem: function(id, is_checked) {
@ -635,7 +625,6 @@ define(
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
handleGroup: function(group) {
@ -649,7 +638,6 @@ define(
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
handleSetPage: function(page) {
@ -659,23 +647,28 @@ define(
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
handleRenderItem: function(item, actions) {
var render = this.props.onRenderItem(item, actions);
const render = this.props.onRenderItem(item, actions);
return render.props.children;
},
handleRefreshItems: function() {
this.getItems();
},
render: function() {
var items = this.state.items,
sort_by = this.state.sort_by,
sort_order = this.state.sort_order;
const items = this.state.items;
const sort_by = this.state.sort_by;
const sort_order = this.state.sort_order;
// columns
let columns = this.props.columns || [];
columns = columns.filter(function(column) {
return (column.display === undefined || !!(column.display) === true);
});
// bulk actions
var bulk_actions = this.props.bulk_actions || [];
let bulk_actions = this.props.bulk_actions || [];
if (this.state.group === 'trash' && bulk_actions.length > 0) {
bulk_actions = [
@ -693,9 +686,9 @@ define(
}
// item actions
var item_actions = this.props.item_actions || [];
const item_actions = this.props.item_actions || [];
var table_classes = classNames(
const table_classes = classNames(
'mailpoet_listing_table',
'wp-list-table',
'widefat',
@ -705,7 +698,7 @@ define(
);
// search
var search = (
let search = (
<ListingSearch
onSearch={ this.handleSearch }
search={ this.state.search }
@ -716,7 +709,7 @@ define(
}
// groups
var groups = (
let groups = (
<ListingGroups
groups={ this.state.groups }
group={ this.state.group }
@ -759,7 +752,7 @@ define(
selection={ this.state.selection }
sort_by={ sort_by }
sort_order={ sort_order }
columns={ this.props.columns }
columns={ columns }
is_selectable={ bulk_actions.length > 0 } />
</thead>
@ -769,7 +762,7 @@ define(
onRestoreItem={ this.handleRestoreItem }
onTrashItem={ this.handleTrashItem }
onRefreshItems={ this.handleRefreshItems }
columns={ this.props.columns }
columns={ columns }
is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem }
onSelectAll={ this.handleSelectAll }
@ -789,7 +782,7 @@ define(
selection={ this.state.selection }
sort_by={ sort_by }
sort_order={ sort_order }
columns={ this.props.columns }
columns={ columns }
is_selectable={ bulk_actions.length > 0 } />
</tfoot>
@ -812,6 +805,4 @@ define(
}
});
return Listing;
}
);
module.exports = Listing;

View File

@ -112,7 +112,16 @@ define([
}
}
},
}).preventDefault('auto');
})
.preventDefault('auto')
.actionChecker(function (pointer, event, action) {
// Disable dragging with right click
if (event.button !== 0) {
return null;
}
return action;
});
if (this.options.drop !== undefined) {
interactable.getDropModel = this.options.drop;

View File

@ -0,0 +1,23 @@
/**
* Highlight Editing Behavior
*
* Highlights a block that is being edited
*/
define([
'backbone.marionette',
'newsletter_editor/behaviors/BehaviorsLookup',
], function(Marionette, BehaviorsLookup) {
BehaviorsLookup.HighlightEditingBehavior = Marionette.Behavior.extend({
modelEvents: {
'startEditing': 'enableHighlight',
'stopEditing': 'disableHighlight',
},
enableHighlight: function() {
this.$el.addClass('mailpoet_highlight');
},
disableHighlight: function() {
this.$el.removeClass('mailpoet_highlight');
},
});
});

View File

@ -13,6 +13,7 @@ define([
'newsletter_editor/blocks/divider',
'newsletter_editor/components/communication',
'mailpoet',
'backbone.supermodel',
'underscore',
'jquery'
], function(
@ -22,6 +23,7 @@ define([
DividerBlock,
CommunicationComponent,
MailPoet,
SuperModel,
_,
jQuery
) {
@ -31,6 +33,36 @@ define([
var Module = {},
base = BaseBlock;
Module.ALCSupervisor = SuperModel.extend({
initialize: function() {
this.listenTo(App.getChannel(), 'automatedLatestContentRefresh', this.refresh);
},
refresh: function() {
var models = App.findModels(function(model) {
return model.get('type') === 'automatedLatestContent';
}) || [];
if (models.length === 0) return;
var blocks = _.map(models, function(model) {
return model.toJSON();
});
CommunicationComponent.getBulkTransformedPosts({
blocks: blocks,
}).then(_.partial(this.refreshBlocks, models));
},
refreshBlocks: function(models, renderedBlocks) {
_.each(
_.zip(models, renderedBlocks),
function(args) {
var model = args[0],
contents = args[1];
model.trigger('refreshPosts', contents);
}
);
},
});
Module.AutomatedLatestContentBlockModel = base.BlockModel.extend({
stale: ['_container'],
defaults: function() {
@ -72,34 +104,32 @@ define([
},
initialize: function() {
base.BlockView.prototype.initialize.apply(this, arguments);
this.fetchPosts();
this.on('change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:featuredImagePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:showAuthor change:authorPrecededBy change:showCategories change:categoriesPrecededBy change:readMoreType change:readMoreText change:sortBy change:showDivider', this._scheduleFetchPosts, this);
this.listenTo(this.get('readMoreButton'), 'change', this._scheduleFetchPosts);
this.listenTo(this.get('divider'), 'change', this._scheduleFetchPosts);
},
fetchPosts: function() {
var that = this;
CommunicationComponent.getTransformedPosts(this.toJSON()).done(function(content) {
that.get('_container').get('blocks').reset(content, {parse: true});
that.trigger('postsChanged');
}).fail(function(error) {
MailPoet.Notice.error(MailPoet.I18n.t('failedToFetchRenderedPosts'));
this.on('add remove update reset', function(model, collection, options) {
App.getChannel().trigger('automatedLatestContentRefresh');
});
this.on('refreshPosts', this.updatePosts, this);
},
updatePosts: function(posts) {
this.get('_container.blocks').reset(posts, {parse: true});
},
/**
* Batch more changes during a specific time, instead of fetching
* ALC posts on each model change
*/
_scheduleFetchPosts: function() {
var timeout = 500,
var TIMEOUT = 500,
that = this;
if (this._fetchPostsTimer !== undefined) {
clearTimeout(this._fetchPostsTimer);
}
this._fetchPostsTimer = setTimeout(function() {
that.fetchPosts();
//that.fetchPosts();
App.getChannel().trigger('automatedLatestContentRefresh');
that._fetchPostsTimer = undefined;
}, timeout);
}, TIMEOUT);
},
});
@ -124,6 +154,7 @@ define([
renderOptions = {
disableTextEditor: true,
disableDragAndDrop: true,
emptyContainerMessage: MailPoet.I18n.t('noPostsToDisplay'),
};
this.toolsView = new Module.AutomatedLatestContentBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
@ -164,9 +195,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
@ -181,6 +209,7 @@ define([
this.$('.mailpoet_automated_latest_content_categories_and_tags').select2({
multiple: true,
allowClear: true,
placeholder: MailPoet.I18n.t('categoriesAndTags'),
ajax: {
data: function (params) {
return {
@ -287,9 +316,11 @@ define([
if (value == 'titleOnly') {
this.$('.mailpoet_automated_latest_content_title_as_list').removeClass('mailpoet_hidden');
this.$('.mailpoet_automated_latest_content_image_full_width_option').addClass('mailpoet_hidden');
this.$('.mailpoet_automated_latest_content_image_separator').addClass('mailpoet_hidden');
} else {
this.$('.mailpoet_automated_latest_content_title_as_list').addClass('mailpoet_hidden');
this.$('.mailpoet_automated_latest_content_image_full_width_option').removeClass('mailpoet_hidden');
this.$('.mailpoet_automated_latest_content_image_separator').removeClass('mailpoet_hidden');
// Reset titleFormat if it was set to List when switching away from displayType=titleOnly
if (this.model.get('titleFormat') === 'ul') {
@ -363,5 +394,10 @@ define([
});
});
App.on('start', function() {
App._ALCSupervisor = new Module.ALCSupervisor();
App._ALCSupervisor.refresh();
});
return Module;
});

View File

@ -40,6 +40,9 @@ define([
// Remove stale attributes from resulting JSON object
return _.omit(SuperModel.prototype.toJSON.call(this), this.stale);
},
getChildren: function() {
return [];
},
});
Module.BlockView = AugmentedView.extend({
@ -77,6 +80,7 @@ define([
}
},
},
HighlightEditingBehavior: {},
},
templateHelpers: function() {
return {
@ -215,8 +219,12 @@ define([
Module.BlockSettingsView = Marionette.LayoutView.extend({
className: 'mailpoet_editor_settings',
initialize: function() {
MailPoet.Modal.panel({
behaviors: {
ColorPickerBehavior: {},
},
initialize: function(params) {
this.model.trigger('startEditing');
var panelParams = {
element: this.$el,
template: '',
position: 'right',
@ -224,7 +232,13 @@ define([
onCancel: function() {
this.destroy();
}.bind(this),
});
};
this.renderOptions = params.renderOptions || {};
if (this.renderOptions.displayFormat === 'subpanel') {
MailPoet.Modal.subpanel(panelParams);
} else {
MailPoet.Modal.panel(panelParams);
}
},
close: function(event) {
this.destroy();
@ -253,6 +267,7 @@ define([
},
onBeforeDestroy: function() {
MailPoet.Modal.close();
this.model.trigger('stopEditing');
},
});

View File

@ -99,23 +99,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
initialize: function(params) {
var panelParams = {
element: this.$el,
template: '',
position: 'right',
width: App.getConfig().get('sidepanelWidth'),
};
this.renderOptions = params.renderOptions || {};
if (this.renderOptions.displayFormat === 'subpanel') {
MailPoet.Modal.subpanel(panelParams);
} else {
MailPoet.Modal.panel(panelParams);
}
},
templateHelpers: function() {
return {
model: this.model.toJSON(),

View File

@ -65,6 +65,13 @@ define([
}
return response;
},
getChildren: function() {
var models = this.get('blocks').map(function(model, index, list) {
return [model, model.getChildren()];
});
return _.flatten(models);
},
});
Module.ContainerBlockView = Marionette.CompositeView.extend({
@ -118,6 +125,7 @@ define([
return view.renderOptions.depth === 1;
},
},
HighlightEditingBehavior: {}
},
onDragSubstituteBy: function() {
// For two and three column layouts display their respective widgets,
@ -286,6 +294,7 @@ define([
templateHelpers: function() {
return {
isRoot: this.renderOptions.depth === 0,
emptyContainerMessage: this.renderOptions.emptyContainerMessage || '',
};
},
});
@ -302,9 +311,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
regions: {
columnsSettingsRegion: '.mailpoet_container_columns_settings',
},

View File

@ -102,23 +102,6 @@ define([
'change:styles.block.borderColor': 'repaintDividerStyleOptions',
};
},
behaviors: {
ColorPickerBehavior: {},
},
initialize: function(params) {
var panelParams = {
element: this.$el,
template: '',
position: 'right',
width: App.getConfig().get('sidepanelWidth'),
};
this.renderOptions = params.renderOptions || {};
if (this.renderOptions.displayFormat === 'subpanel') {
MailPoet.Modal.subpanel(panelParams);
} else {
MailPoet.Modal.panel(panelParams);
}
},
templateHelpers: function() {
return {
model: this.model.toJSON(),

View File

@ -56,13 +56,15 @@ define([
inline: true,
menubar: false,
toolbar: "bold italic link unlink forecolor mailpoet_custom_fields",
toolbar: "bold italic link unlink forecolor mailpoet_shortcodes",
valid_elements: "p[class|style],span[class|style],a[href|class|title|target|style],strong[class|style],em[class|style],strike,br",
invalid_elements: "script",
block_formats: 'Paragraph=p',
relative_urls: false,
remove_script_host: false,
plugins: "link textcolor colorpicker mailpoet_custom_fields",
plugins: "link textcolor colorpicker mailpoet_shortcodes",
setup: function(editor) {
editor.on('change', function(e) {
@ -78,8 +80,8 @@ define([
});
},
mailpoet_custom_fields: App.getConfig().get('customFields').toJSON(),
mailpoet_custom_fields_window_title: MailPoet.I18n.t('customFieldsWindowTitle'),
mailpoet_shortcodes: App.getConfig().get('shortcodes').toJSON(),
mailpoet_shortcodes_window_title: MailPoet.I18n.t('shortcodesWindowTitle'),
});
},
});
@ -104,9 +106,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
templateHelpers: function() {
return {
model: this.model.toJSON(),

View File

@ -56,13 +56,15 @@ define([
inline: true,
menubar: false,
toolbar: "bold italic link unlink forecolor mailpoet_custom_fields",
toolbar: "bold italic link unlink forecolor mailpoet_shortcodes",
valid_elements: "p[class|style],span[class|style],a[href|class|title|target|style],strong[class|style],em[class|style],strike,br",
invalid_elements: "script",
block_formats: 'Paragraph=p',
relative_urls: false,
remove_script_host: false,
plugins: "link textcolor colorpicker mailpoet_custom_fields",
plugins: "link textcolor colorpicker mailpoet_shortcodes",
setup: function(editor) {
editor.on('change', function(e) {
@ -78,8 +80,8 @@ define([
});
},
mailpoet_custom_fields: App.getConfig().get('customFields').toJSON(),
mailpoet_custom_fields_window_title: MailPoet.I18n.t('customFieldsWindowTitle'),
mailpoet_shortcodes: App.getConfig().get('shortcodes').toJSON(),
mailpoet_shortcodes_window_title: MailPoet.I18n.t('shortcodesWindowTitle'),
});
},
});
@ -104,9 +106,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
templateHelpers: function() {
return {
model: this.model.toJSON(),

View File

@ -23,7 +23,19 @@ define([
'newsletter_editor/blocks/button',
'newsletter_editor/blocks/divider',
'select2'
], function(Backbone, Marionette, Radio, _, jQuery, MailPoet, App, CommunicationComponent, BaseBlock, ButtonBlock, DividerBlock) {
], function(
Backbone,
Marionette,
Radio,
_,
jQuery,
MailPoet,
App,
CommunicationComponent,
BaseBlock,
ButtonBlock,
DividerBlock
) {
"use strict";
@ -163,6 +175,7 @@ define([
renderOptions = {
disableTextEditor: true,
disableDragAndDrop: true,
emptyContainerMessage: MailPoet.I18n.t('noPostsToDisplay'),
};
this.postsRegion.show(new ContainerView({ model: this.model.get('_transformedPosts'), renderOptions: renderOptions }));
},
@ -195,6 +208,7 @@ define([
};
},
initialize: function() {
this.model.trigger('startEditing');
this.selectionView = new PostSelectionSettingsView({ model: this.model });
this.displayOptionsView = new PostsDisplayOptionsSettingsView({ model: this.model });
},
@ -202,21 +216,23 @@ define([
var that = this,
blockView = this.model.request('blockView');
this.selectionRegion.show(this.selectionView);
this.displayOptionsRegion.show(this.displayOptionsView);
this.showChildView('selectionRegion', this.selectionView);
this.showChildView('displayOptionsRegion', this.displayOptionsView);
MailPoet.Modal.panel({
element: this.$el,
template: '',
position: 'right',
overlay: true,
highlight: blockView.$el,
width: App.getConfig().get('sidepanelWidth'),
onCancel: function() {
// Self destroy the block if the user closes settings modal
that.model.destroy();
},
});
// Inform child views that they have been attached to document
this.selectionView.triggerMethod('attach');
this.displayOptionsView.triggerMethod('attach');
},
switchToDisplayOptions: function() {
// Switch content view
@ -266,14 +282,19 @@ define([
Marionette.CompositeView.apply(this, arguments);
},
onRender: function() {
// Dynamically update available post types
CommunicationComponent.getPostTypes().done(_.bind(this._updateContentTypes, this));
},
onAttach: function() {
var that = this;
// Dynamically update available post types
CommunicationComponent.getPostTypes().done(_.bind(this._updateContentTypes, this));
//CommunicationComponent.getPostTypes().done(_.bind(this._updateContentTypes, this));
this.$('.mailpoet_posts_categories_and_tags').select2({
multiple: true,
allowClear: true,
placeholder: MailPoet.I18n.t('categoriesAndTags'),
ajax: {
data: function (params) {
return {
@ -405,9 +426,6 @@ define([
"change .mailpoet_posts_sort_by": _.partial(this.changeField, "sortBy"),
};
},
behaviors: {
ColorPickerBehavior: {},
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
@ -450,9 +468,11 @@ define([
if (value == 'titleOnly') {
this.$('.mailpoet_posts_title_as_list').removeClass('mailpoet_hidden');
this.$('.mailpoet_posts_image_full_width_option').addClass('mailpoet_hidden');
this.$('.mailpoet_posts_image_separator').addClass('mailpoet_hidden');
} else {
this.$('.mailpoet_posts_title_as_list').addClass('mailpoet_hidden');
this.$('.mailpoet_posts_image_full_width_option').removeClass('mailpoet_hidden');
this.$('.mailpoet_posts_image_separator').removeClass('mailpoet_hidden');
// Reset titleFormat if it was set to List when switching away from displayType=titleOnly
if (this.model.get('titleFormat') === 'ul') {

View File

@ -139,6 +139,7 @@ define([
}
},
},
HighlightEditingBehavior: {},
},
onDragSubstituteBy: function() { return Module.SocialWidgetView; },
constructor: function() {

View File

@ -70,9 +70,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
behaviors: {
ColorPickerBehavior: {},
},
});
Module.SpacerWidgetView = base.WidgetView.extend({

View File

@ -53,14 +53,16 @@ define([
menubar: false,
toolbar1: "formatselect bold italic forecolor | link unlink",
toolbar2: "alignleft aligncenter alignright alignjustify | bullist numlist blockquote | code mailpoet_custom_fields",
toolbar2: "alignleft aligncenter alignright alignjustify | bullist numlist blockquote | code mailpoet_shortcodes",
//forced_root_block: 'p',
valid_elements: "p[class|style],span[class|style],a[href|class|title|target|style],h1[class|style],h2[class|style],h3[class|style],ol[class|style],ul[class|style],li[class|style],strong[class|style],em[class|style],strike,br,blockquote[class|style],table[class|style],tr[class|style],th[class|style],td[class|style]",
invalid_elements: "script",
block_formats: 'Heading 1=h1;Heading 2=h2;Heading 3=h3;Paragraph=p',
relative_urls: false,
remove_script_host: false,
plugins: "link code textcolor colorpicker mailpoet_custom_fields",
plugins: "link code textcolor colorpicker mailpoet_shortcodes",
setup: function(editor) {
editor.on('change', function(e) {
@ -76,8 +78,8 @@ define([
});
},
mailpoet_custom_fields: App.getConfig().get('customFields').toJSON(),
mailpoet_custom_fields_window_title: MailPoet.I18n.t('customFieldsWindowTitle'),
mailpoet_shortcodes: App.getConfig().get('shortcodes').toJSON(),
mailpoet_shortcodes_window_title: MailPoet.I18n.t('shortcodesWindowTitle'),
});
}
},

View File

@ -62,6 +62,13 @@ define([
});
};
Module.getBulkTransformedPosts = function(options) {
return Module._query({
action: 'getBulkTransformedPosts',
options: options,
});
};
Module.saveNewsletter = function(options) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',

View File

@ -60,6 +60,11 @@ define([
return Module.newsletter;
};
Module.findModels = function(predicate) {
var blocks = App._contentContainer.getChildren();
return _.filter(blocks, predicate);
};
App.on('before:start', function(options) {
// Expose block methods globally
App.registerBlockType = Module.registerBlockType;
@ -68,6 +73,7 @@ define([
App.toJSON = Module.toJSON;
App.getBody = Module.getBody;
App.getNewsletter = Module.getNewsletter;
App.findModels = Module.findModels;
Module.newsletter = new Module.NewsletterModel(_.omit(_.clone(options.newsletter), ['body']));
});

View File

@ -283,8 +283,10 @@ define([
return;
}
var contents = JSON.stringify(jsonObject);
if (App.getConfig().get('validation.validateUnsubscribeLinkPresent') &&
JSON.stringify(jsonObject).indexOf("[link:subscription_unsubscribe_url]") < 0) {
contents.indexOf("[link:subscription_unsubscribe_url]") < 0 &&
contents.indexOf("[link:subscription_unsubscribe]") < 0) {
this.showValidationError(MailPoet.I18n.t('unsubscribeLinkMissing'));
return;
}

View File

@ -1,58 +0,0 @@
/**
* wysija_custom_fields/plugin.js
*
* TinyMCE plugin for adding dynamic data placeholders to newsletters.
*
* This adds a button to the editor toolbar which displays a modal window of
* available dynamic data placeholder buttons. On click each button inserts
* its placeholder into editor text.
*/
/*jshint unused:false */
/*global tinymce:true */
tinymce.PluginManager.add('mailpoet_custom_fields', function(editor, url) {
var appendLabelAndClose = function(text) {
editor.insertContent('[' + text + ']');
editor.windowManager.close();
},
generateOnClickFunc = function(id) {
return function() {
appendLabelAndClose(id);
};
};
editor.addButton('mailpoet_custom_fields', {
icon: 'mailpoet_custom_fields',
onclick: function() {
var customFields = [],
configCustomFields = editor.settings.mailpoet_custom_fields;
for (var segment in configCustomFields) {
if (configCustomFields.hasOwnProperty(segment)) {
customFields.push({
type: 'label',
text: segment,
});
for (var i = 0; i < configCustomFields[segment].length; i += 1) {
customFields.push({
type: 'button',
text: configCustomFields[segment][i].text,
onClick: generateOnClickFunc(configCustomFields[segment][i].shortcode)
});
}
}
}
// Open window
editor.windowManager.open({
height: parseInt(editor.getParam("plugin_mailpoet_custom_fields_height", 400)),
width: parseInt(editor.getParam("plugin_mailpoet_custom_fields_width", 450)),
autoScroll: true,
title: editor.settings.mailpoet_custom_fields_window_title,
body: customFields,
buttons: [],
});
},
});
});

View File

@ -0,0 +1,58 @@
/**
* wysija_shortcodes/plugin.js
*
* TinyMCE plugin for adding dynamic data placeholders to newsletters.
*
* This adds a button to the editor toolbar which displays a modal window of
* available dynamic data placeholder buttons. On click each button inserts
* its placeholder into editor text.
*/
/*jshint unused:false */
/*global tinymce:true */
tinymce.PluginManager.add('mailpoet_shortcodes', function(editor, url) {
var appendLabelAndClose = function(text) {
editor.insertContent('[' + text + ']');
editor.windowManager.close();
},
generateOnClickFunc = function(id) {
return function() {
appendLabelAndClose(id);
};
};
editor.addButton('mailpoet_shortcodes', {
icon: 'mailpoet_shortcodes',
onclick: function() {
var shortcodes = [],
configShortcodes = editor.settings.mailpoet_shortcodes;
for (var segment in configShortcodes) {
if (configShortcodes.hasOwnProperty(segment)) {
shortcodes.push({
type: 'label',
text: segment,
});
for (var i = 0; i < configShortcodes[segment].length; i += 1) {
shortcodes.push({
type: 'button',
text: configShortcodes[segment][i].text,
onClick: generateOnClickFunc(configShortcodes[segment][i].shortcode)
});
}
}
}
// Open window
editor.windowManager.open({
height: parseInt(editor.getParam("plugin_mailpoet_shortcodes_height", 400)),
width: parseInt(editor.getParam("plugin_mailpoet_shortcodes_width", 450)),
autoScroll: true,
title: editor.settings.mailpoet_shortcodes_window_title,
body: shortcodes,
buttons: [],
});
},
});
});

View File

@ -1,311 +0,0 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'jquery',
'mailpoet'
],
function(
React,
Router,
Listing,
classNames,
jQuery,
MailPoet
) {
var Link = Router.Link;
var columns = [
{
name: 'subject',
label: MailPoet.I18n.t('subject'),
sortable: true
},
{
name: 'status',
label: MailPoet.I18n.t('status')
},
{
name: 'segments',
label: MailPoet.I18n.t('lists')
},
{
name: 'statistics',
label: MailPoet.I18n.t('statistics')
},
{
name: 'created_at',
label: MailPoet.I18n.t('createdOn'),
sortable: true
},
{
name: 'updated_at',
label: MailPoet.I18n.t('lastModifiedOn'),
sortable: true
}
];
var messages = {
onTrash: function(response) {
var count = ~~response;
var message = null;
if(count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterTrashed')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersTrashed')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onDelete: function(response) {
var count = ~~response;
var message = null;
if(count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterDeleted')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersDeleted')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore: function(response) {
var count = ~~response;
var message = null;
if(count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterRestored')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersRestored')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
}
};
var bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
var item_actions = [
{
name: 'edit',
link: function(item) {
return (
<a href={ `?page=mailpoet-newsletter-editor&id=${ item.id }` }>
{MailPoet.I18n.t('edit')}
</a>
);
}
},
{
name: 'trash'
}
];
var NewsletterList = React.createClass({
pauseSending: function(item) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'pause',
data: item.id
}).done(function() {
jQuery('#resume_'+item.id).show();
jQuery('#pause_'+item.id).hide();
});
},
resumeSending: function(item) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'resume',
data: item.id
}).done(function() {
jQuery('#pause_'+item.id).show();
jQuery('#resume_'+item.id).hide();
});
},
renderStatus: function(item) {
if(!item.queue) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else {
if (item.queue.status === 'scheduled') {
return (
<span>{MailPoet.I18n.t('scheduledFor')} { MailPoet.Date.format(item.queue.scheduled_at) } </span>
)
}
var progressClasses = classNames(
'mailpoet_progress',
{ 'mailpoet_progress_complete': item.queue.status === 'completed'}
);
// calculate percentage done
var percentage = Math.round(
(item.queue.count_processed * 100) / (item.queue.count_total)
);
var label = false;
if(item.queue.status === 'completed') {
label = (
<span>
{
MailPoet.I18n.t('newsletterQueueCompleted')
.replace("%$1d", item.queue.count_processed - item.queue.count_failed)
.replace("%$2d", item.queue.count_total)
}
</span>
);
} else {
label = (
<span>
{ item.queue.count_processed } / { item.queue.count_total }
&nbsp;&nbsp;
<a
id={ 'resume_'+item.id }
className="button"
style={{ display: (item.queue.status === 'paused') ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.resumeSending.bind(null, item) }
>{MailPoet.I18n.t('resume')}</a>
<a
id={ 'pause_'+item.id }
className="button mailpoet_pause"
style={{ display: (item.queue.status === null) ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.pauseSending.bind(null, item) }
>{MailPoet.I18n.t('pause')}</a>
</span>
);
}
return (
<div>
<div className={ progressClasses }>
<span
className="mailpoet_progress_bar"
style={ { width: percentage + "%"} }
></span>
<span className="mailpoet_progress_label">
{ percentage + "%" }
</span>
</div>
<p style={{ textAlign:'center' }}>
{ label }
</p>
</div>
);
}
},
renderStatistics: function(item) {
if(!item.statistics || !item.queue || item.queue.count_processed == 0 || item.queue.status === 'scheduled') {
return (
<span>
{MailPoet.I18n.t('notSentYet')}
</span>
);
}
var percentage_clicked = Math.round(
(item.statistics.clicked * 100) / (item.queue.count_processed)
);
var percentage_opened = Math.round(
(item.statistics.opened * 100) / (item.queue.count_processed)
);
var percentage_unsubscribed = Math.round(
(item.statistics.unsubscribed * 100) / (item.queue.count_processed)
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
},
renderItem: function(newsletter, actions) {
var rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
var segments = newsletter.segments.map(function(segment) {
return segment.name
}).join(', ');
var statistics_column =
(!mailpoet_settings.tracking || !mailpoet_settings.tracking.enabled) ?
false :
<td className="column {statistics_class}" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
</td>;
return (
<div>
<td className={ rowClasses }>
<strong>
<a>{ newsletter.subject }</a>
</strong>
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
</td>
{ statistics_column }
<td className="column-date" data-colname={ MailPoet.I18n.t('createdOn') }>
<abbr>{ MailPoet.Date.format(newsletter.created_at) }</abbr>
</td>
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
if (!mailpoet_settings.tracking || !mailpoet_settings.tracking.enabled) {
columns = _.without(columns, _.findWhere(columns, {name: 'statistics'}));
}
return (
<div>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<Listing
limit={ mailpoet_listing_per_page }
params={ this.props.params }
endpoint="newsletters"
onRenderItem={this.renderItem}
columns={columns}
bulk_actions={ bulk_actions }
item_actions={ item_actions }
messages={ messages }
auto_refresh={ true } />
</div>
);
}
});
return NewsletterList;
}
);

View File

@ -0,0 +1,317 @@
import React from 'react'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import {
timeOfDayValues,
weekDayValues,
monthDayValues,
nthWeekDayValues
} from 'newsletters/scheduling/common.jsx'
const messages = {
onTrash(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterTrashed')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersTrashed')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onDelete(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterDeleted')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersDeleted')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterRestored')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersRestored')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
}
};
const columns = [
{
name: 'subject',
label: MailPoet.I18n.t('subject'),
sortable: true
},
{
name: 'status',
label: MailPoet.I18n.t('status'),
width: 100
},
{
name: 'settings',
label: MailPoet.I18n.t('settings')
},
{
name: 'history',
label: MailPoet.I18n.t('history'),
width: 100
},
{
name: 'updated_at',
label: MailPoet.I18n.t('lastModifiedOn'),
sortable: true
}
];
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
return (
<a href={ newsletter.preview_url } target="_blank">
{MailPoet.I18n.t('preview')}
</a>
);
}
},
{
name: 'edit',
link: function(newsletter) {
return (
<a href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }>
{MailPoet.I18n.t('edit')}
</a>
);
}
},
{
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'duplicate',
data: newsletter.id
}).done(function(response) {
if (response !== false && response.subject !== undefined) {
MailPoet.Notice.success(
(MailPoet.I18n.t('newsletterDuplicated')).replace(
'%$1s', response.subject
)
);
}
refresh();
});
}
},
{
name: 'trash'
}
];
const NewsletterListNotification = React.createClass({
updateStatus: function(e) {
// make the event persist so that we can still override the selected value
// in the ajax callback
e.persist();
MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'setStatus',
data: {
id: ~~(e.target.getAttribute('data-id')),
status: e.target.value
}
}).done(function(response) {
if (response.result === false) {
MailPoet.Notice.error(MailPoet.I18n.t('postNotificationActivationFailed'));
// reset value to actual newsletter's status
e.target.value = response.status;
} else {
if (response.status === 'active') {
MailPoet.Notice.success(MailPoet.I18n.t('postNotificationActivated'));
}
// force refresh of listing so that groups are updated
this.forceUpdate();
}
}.bind(this));
},
renderStatus: function(newsletter) {
return (
<select
data-id={ newsletter.id }
defaultValue={ newsletter.status }
onChange={ this.updateStatus }
>
<option value="active">{ MailPoet.I18n.t('active') }</option>
<option value="draft">{ MailPoet.I18n.t('inactive') }</option>
</select>
);
},
renderSettings: function(newsletter) {
let sendingFrequency;
let sendingToSegments;
// get list of segments' name
const segments = newsletter.segments.map(function(segment) {
return segment.name
});
// check if the user has specified segments to send to
if(segments.length === 0) {
return (
<span className="mailpoet_error">
{ MailPoet.I18n.t('sendingToSegmentsNotSpecified') }
</span>
);
} else {
sendingToSegments = MailPoet.I18n.t('ifNewContentToSegments').replace(
'%$1s', segments.join(', ')
);
// set sending frequency
switch (newsletter.options.intervalType) {
case 'daily':
sendingFrequency = MailPoet.I18n.t('sendDaily').replace(
'%$1s', timeOfDayValues[newsletter.options.timeOfDay]
);
break;
case 'weekly':
sendingFrequency = MailPoet.I18n.t('sendWeekly').replace(
'%$1s', weekDayValues[newsletter.options.weekDay]
).replace(
'%$2s', timeOfDayValues[newsletter.options.timeOfDay]
);
break;
case 'monthly':
sendingFrequency = MailPoet.I18n.t('sendMonthly').replace(
'%$1s', monthDayValues[newsletter.options.monthDay]
).replace(
'%$2s', timeOfDayValues[newsletter.options.timeOfDay]
);
break;
case 'nthWeekDay':
sendingFrequency = MailPoet.I18n.t('sendNthWeekDay').replace(
'%$1s', nthWeekDayValues[newsletter.options.nthWeekDay]
).replace(
'%$2s', weekDayValues[newsletter.options.weekDay]
).replace(
'%$3s', timeOfDayValues[newsletter.options.timeOfDay]
);
break;
case 'immediately':
sendingFrequency = MailPoet.I18n.t('sendImmediately');
break;
}
}
return (
<span>
{ sendingFrequency } { sendingToSegments }
</span>
);
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
return (
<div>
<td className={ rowClasses }>
<strong>
<a
className="row-title"
href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }
>{ newsletter.subject }</a>
</strong>
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('settings') }>
{ this.renderSettings(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('history') }>
<a href="#TODO">{ MailPoet.I18n.t('viewHistory') }</a>
</td>
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<ListingTabs tab="notification" />
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="notification"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ newsletter_actions }
messages={ messages }
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
/>
</div>
);
}
});
module.exports = NewsletterListNotification;

View File

@ -0,0 +1,339 @@
import React from 'react'
import { Router, Link } from 'react-router'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const messages = {
onTrash(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterTrashed')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersTrashed')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onDelete(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterDeleted')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersDeleted')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterRestored')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersRestored')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
}
};
const columns = [
{
name: 'subject',
label: MailPoet.I18n.t('subject'),
sortable: true
},
{
name: 'status',
label: MailPoet.I18n.t('status')
},
{
name: 'segments',
label: MailPoet.I18n.t('lists')
},
{
name: 'statistics',
label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled
},
{
name: 'updated_at',
label: MailPoet.I18n.t('lastModifiedOn'),
sortable: true
}
];
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
return (
<a href={ newsletter.preview_url } target="_blank">
{MailPoet.I18n.t('preview')}
</a>
);
}
},
{
name: 'edit',
link: function(newsletter) {
return (
<a href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }>
{MailPoet.I18n.t('edit')}
</a>
);
}
},
{
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'duplicate',
data: newsletter.id
}).done(function(response) {
if (response !== false && response.subject !== undefined) {
MailPoet.Notice.success(
(MailPoet.I18n.t('newsletterDuplicated')).replace(
'%$1s', response.subject
)
);
}
refresh();
});
}
},
{
name: 'trash'
}
];
const NewsletterListStandard = React.createClass({
pauseSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'pause',
data: newsletter.id
}).done(function() {
jQuery('#resume_'+newsletter.id).show();
jQuery('#pause_'+newsletter.id).hide();
});
},
resumeSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'resume',
data: newsletter.id
}).done(function() {
jQuery('#pause_'+newsletter.id).show();
jQuery('#resume_'+newsletter.id).hide();
});
},
renderStatus: function(newsletter) {
if (!newsletter.queue) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else {
if (newsletter.queue.status === 'scheduled') {
return (
<span>{MailPoet.I18n.t('scheduledFor')} { MailPoet.Date.format(newsletter.queue.scheduled_at) } </span>
)
}
const progressClasses = classNames(
'mailpoet_progress',
{ 'mailpoet_progress_complete': newsletter.queue.status === 'completed'}
);
// calculate percentage done
const percentage = Math.round(
(newsletter.queue.count_processed * 100) / (newsletter.queue.count_total)
);
let label;
if (newsletter.queue.status === 'completed') {
label = (
<span>
{
MailPoet.I18n.t('newsletterQueueCompleted')
.replace("%$1d", newsletter.queue.count_processed - newsletter.queue.count_failed)
.replace("%$2d", newsletter.queue.count_total)
}
</span>
);
} else {
label = (
<span>
{ newsletter.queue.count_processed } / { newsletter.queue.count_total }
&nbsp;&nbsp;
<a
id={ 'resume_'+newsletter.id }
className="button"
style={{ display: (newsletter.queue.status === 'paused') ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.resumeSending.bind(null, newsletter) }
>{MailPoet.I18n.t('resume')}</a>
<a
id={ 'pause_'+newsletter.id }
className="button mailpoet_pause"
style={{ display: (newsletter.queue.status === null) ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.pauseSending.bind(null, newsletter) }
>{MailPoet.I18n.t('pause')}</a>
</span>
);
}
return (
<div>
<div className={ progressClasses }>
<span
className="mailpoet_progress_bar"
style={ { width: percentage + "%"} }
></span>
<span className="mailpoet_progress_label">
{ percentage + "%" }
</span>
</div>
<p style={{ textAlign:'center' }}>
{ label }
</p>
</div>
);
}
},
renderStatistics: function(newsletter) {
if (mailpoet_tracking_enabled === false) {
return;
}
if (newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled') {
const total_sent = ~~(newsletter.queue.count_processed);
let percentage_clicked = 0;
let percentage_opened = 0;
let percentage_unsubscribed = 0;
if (total_sent > 0) {
percentage_clicked = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
}
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
const segments = newsletter.segments.map(function(segment) {
return segment.name
}).join(', ');
return (
<div>
<td className={ rowClasses }>
<strong>
<a
className="row-title"
href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }
>{ newsletter.subject }</a>
</strong>
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<ListingTabs tab="standard" />
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="standard"
onRenderItem={this.renderItem}
columns={columns}
bulk_actions={ bulk_actions }
item_actions={ newsletter_actions }
messages={ messages }
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
/>
</div>
);
}
});
module.exports = NewsletterListStandard;

View File

@ -0,0 +1,53 @@
import React from 'react'
import { Link } from 'react-router'
import classNames from 'classnames'
import MailPoet from 'mailpoet'
const ListingTabs = React.createClass({
getInitialState() {
return {
tab: null,
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, index) => {
const tabClasses = classNames(
'nav-tab',
{ 'nav-tab-active': (this.props.tab === tab.name) }
);
return (
<Link
key={ 'tab-'+index }
className={ tabClasses }
to={ tab.link }
>{ tab.label }</Link>
);
});
return (
<h2 className="nav-tab-wrapper">
{ tabs }
</h2>
);
}
});
module.exports = ListingTabs;

View File

@ -0,0 +1,361 @@
import React from 'react'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import _ from 'underscore'
const mailpoet_roles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {};
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const messages = {
onTrash(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterTrashed')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersTrashed')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onDelete(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterDeleted')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersDeleted')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
},
onRestore(response) {
const count = ~~response;
let message = null;
if (count === 1) {
message = (
MailPoet.I18n.t('oneNewsletterRestored')
);
} else {
message = (
MailPoet.I18n.t('multipleNewslettersRestored')
).replace('%$1d', count);
}
MailPoet.Notice.success(message);
}
};
const columns = [
{
name: 'subject',
label: MailPoet.I18n.t('subject'),
sortable: true
},
{
name: 'status',
label: MailPoet.I18n.t('status'),
width: 145
},
{
name: 'settings',
label: MailPoet.I18n.t('settings')
},
{
name: 'statistics',
label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled
},
{
name: 'updated_at',
label: MailPoet.I18n.t('lastModifiedOn'),
sortable: true
}
];
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
return (
<a href={ newsletter.preview_url } target="_blank">
{MailPoet.I18n.t('preview')}
</a>
);
}
},
{
name: 'edit',
link: function(newsletter) {
return (
<a href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }>
{MailPoet.I18n.t('edit')}
</a>
);
}
},
{
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'duplicate',
data: newsletter.id
}).done(function(response) {
if (response !== false && response.subject !== undefined) {
MailPoet.Notice.success(
(MailPoet.I18n.t('newsletterDuplicated')).replace(
'%$1s', response.subject
)
);
}
refresh();
});
}
},
{
name: 'trash'
}
];
const NewsletterListWelcome = React.createClass({
updateStatus: function(e) {
// make the event persist so that we can still override the selected value
// in the ajax callback
e.persist();
MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'setStatus',
data: {
id: ~~(e.target.getAttribute('data-id')),
status: e.target.value
}
}).done(function(response) {
if (response.result === false) {
MailPoet.Notice.error(MailPoet.I18n.t('welcomeEmailActivationFailed'));
// reset value to actual newsletter's status
e.target.value = response.status;
} else {
if (response.status === 'active') {
MailPoet.Notice.success(MailPoet.I18n.t('welcomeEmailActivated'));
}
// force refresh of listing so that groups are updated
this.forceUpdate();
}
}.bind(this));
},
renderStatus: function(newsletter) {
let total_sent;
total_sent = (
MailPoet.I18n.t('sentToXSubscribers')
.replace('%$1d', newsletter.total_sent.toLocaleString())
);
return (
<div>
<p>
<select
data-id={ newsletter.id }
defaultValue={ newsletter.status }
onChange={ this.updateStatus }
>
<option value="active">{ MailPoet.I18n.t('active') }</option>
<option value="draft">{ MailPoet.I18n.t('inactive') }</option>
</select>
</p>
<p>{ total_sent }</p>
</div>
);
},
renderSettings: function(newsletter) {
let sendingEvent;
let sendingDelay;
// set sending event
switch (newsletter.options.event) {
case 'user':
// WP User
if (newsletter.options.role === 'mailpoet_all') {
sendingEvent = MailPoet.I18n.t('welcomeEventWPUserAnyRole');
} else {
sendingEvent = MailPoet.I18n.t('welcomeEventWPUserWithRole').replace(
'%$1s', mailpoet_roles[newsletter.options.role]
);
}
break;
case 'segment':
// get segment
const segment = _.find(mailpoet_segments, function(segment) {
return (~~(segment.id) === ~~(newsletter.options.segment));
});
if (segment === undefined) {
return (
<span className="mailpoet_error">
{ MailPoet.I18n.t('sendingToSegmentsNotSpecified') }
</span>
);
} else {
sendingEvent = MailPoet.I18n.t('welcomeEventSegment').replace(
'%$1s', segment.name
);
}
break;
}
// set sending delay
if (sendingEvent) {
if (newsletter.options.afterTimeType !== 'immediate') {
switch (newsletter.options.afterTimeType) {
case 'hours':
sendingDelay = MailPoet.I18n.t('sendingDelayHours').replace(
'%$1d', newsletter.options.afterTimeNumber
);
break;
case 'days':
sendingDelay = MailPoet.I18n.t('sendingDelayDays').replace(
'%$1d', newsletter.options.afterTimeNumber
);
break;
case 'weeks':
sendingDelay = MailPoet.I18n.t('sendingDelayWeeks').replace(
'%$1d', newsletter.options.afterTimeNumber
);
break;
}
sendingEvent += ' [' + sendingDelay + ']';
}
// add a "period" at the end if we do have a sendingEvent
sendingEvent += '.';
}
return (
<span>
{ sendingEvent }
</span>
);
},
renderStatistics: function(newsletter) {
if (mailpoet_tracking_enabled === false) {
return;
}
if (newsletter.total_sent > 0 && newsletter.statistics) {
const total_sent = ~~(newsletter.total_sent);
const percentage_clicked = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
const percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
const percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
return (
<div>
<td className={ rowClasses }>
<strong>
<a
className="row-title"
href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }
>{ newsletter.subject }</a>
</strong>
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('settings') }>
{ this.renderSettings(newsletter) }
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h1 className="title">
{ MailPoet.I18n.t('pageTitle') } <Link className="page-title-action" to="/new">{ MailPoet.I18n.t('new') }</Link>
</h1>
<ListingTabs tab="welcome" />
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="welcome"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ newsletter_actions }
messages={ messages }
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
/>
</div>
);
}
});
module.exports = NewsletterListWelcome;

View File

@ -1,14 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import NewsletterList from 'newsletters/list.jsx'
import NewsletterTypes from 'newsletters/types.jsx'
import NewsletterTemplates from 'newsletters/templates.jsx'
import NewsletterSend from 'newsletters/send.jsx'
import NewsletterStandard from 'newsletters/types/standard.jsx'
import NewsletterWelcome from 'newsletters/types/welcome/welcome.jsx'
import NewsletterNotification from 'newsletters/types/notification/notification.jsx'
import NewsletterTypeStandard from 'newsletters/types/standard.jsx'
import NewsletterTypeWelcome from 'newsletters/types/welcome/welcome.jsx'
import NewsletterTypeNotification from 'newsletters/types/notification/notification.jsx'
import NewsletterListStandard from 'newsletters/listings/standard.jsx'
import NewsletterListWelcome from 'newsletters/listings/welcome.jsx'
import NewsletterListNotification from 'newsletters/listings/notification.jsx'
const history = useRouterHistory(createHashHistory)({ queryKey: false });
@ -24,15 +29,24 @@ if(container) {
ReactDOM.render((
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRoute component={ NewsletterList } />
<IndexRedirect to="standard" />
{/* Listings */}
<Route name="listing/standard" path="standard" component={ NewsletterListStandard } />
<Route name="listing/welcome" path="welcome" component={ NewsletterListWelcome } />
<Route name="listing/notification" path="notification" component={ NewsletterListNotification } />
<Route path="standard/*" component={ NewsletterListStandard } />
<Route path="welcome/*" component={ NewsletterListWelcome } />
<Route path="notification/*" component={ NewsletterListNotification } />
{/* Newsletter: type selection */}
<Route path="new" component={ NewsletterTypes } />
<Route name="standard" path="new/standard" component={ NewsletterStandard } />
<Route name="welcome" path="new/welcome" component={ NewsletterWelcome } />
<Route name="notification" path="new/notification" component={ NewsletterNotification } />
{/* New newsletter: types */}
<Route name="new/standard" path="new/standard" component={ NewsletterTypeStandard } />
<Route name="new/welcome" path="new/welcome" component={ NewsletterTypeWelcome } />
<Route name="new/notification" path="new/notification" component={ NewsletterTypeNotification } />
{/* Template selection */}
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
{/* Sending options */}
<Route path="send/:id" component={ NewsletterSend } />
<Route path="filter[:filter]" component={ NewsletterList } />
<Route path="*" component={ NewsletterList } />
</Route>
</Router>
), container);

View File

@ -0,0 +1,82 @@
import _ from 'underscore'
import MailPoet from 'mailpoet'
const timeFormat = window.mailpoet_time_format || 'H:i';
// welcome emails
const _timeDelayValues = {
'immediate': MailPoet.I18n.t('delayImmediately'),
'hours': MailPoet.I18n.t('delayHoursAfter'),
'days': MailPoet.I18n.t('delayDaysAfter'),
'weeks': MailPoet.I18n.t('delayWeeksAfter')
};
const _intervalValues = {
'daily': MailPoet.I18n.t('daily'),
'weekly': MailPoet.I18n.t('weekly'),
'monthly': MailPoet.I18n.t('monthly'),
'nthWeekDay': MailPoet.I18n.t('monthlyEvery'),
'immediately': MailPoet.I18n.t('immediately')
};
// notification emails
const SECONDS_IN_DAY = 86400;
const TIME_STEP_SECONDS = 3600;
const numberOfTimeSteps = SECONDS_IN_DAY / TIME_STEP_SECONDS;
const _timeOfDayValues = _.object(_.map(
_.times(numberOfTimeSteps,function(step) {
return step * TIME_STEP_SECONDS;
}), function(seconds) {
let date = new Date(null);
date.setSeconds(seconds);
const timeLabel = MailPoet.Date.format(date, { format: timeFormat, offset: 0 });
return [seconds, timeLabel];
})
);
const _weekDayValues = {
0: MailPoet.I18n.t('sunday'),
1: MailPoet.I18n.t('monday'),
2: MailPoet.I18n.t('tuesday'),
3: MailPoet.I18n.t('wednesday'),
4: MailPoet.I18n.t('thursday'),
5: MailPoet.I18n.t('friday'),
6: MailPoet.I18n.t('saturday')
};
const NUMBER_OF_DAYS_IN_MONTH = 28;
const _monthDayValues = _.object(
_.map(
_.times(NUMBER_OF_DAYS_IN_MONTH, function(day) {
return day;
}), function(day) {
const labels = {
0: MailPoet.I18n.t('first'),
1: MailPoet.I18n.t('second'),
2: MailPoet.I18n.t('third')
};
let label;
if (labels[day] !== undefined) {
label = labels[day];
} else {
label = MailPoet.I18n.t('nth').replace("%$1d", day + 1);
}
return [day, label];
}
)
);
const _nthWeekDayValues = {
'1': MailPoet.I18n.t('first'),
'2': MailPoet.I18n.t('second'),
'3': MailPoet.I18n.t('third'),
'L': MailPoet.I18n.t('last')
};
export { _timeDelayValues as timeDelayValues };
export { _intervalValues as intervalValues };
export { _timeOfDayValues as timeOfDayValues };
export { _weekDayValues as weekDayValues };
export { _monthDayValues as monthDayValues };
export { _nthWeekDayValues as nthWeekDayValues };

View File

@ -34,15 +34,20 @@ define(
};
},
getFieldsByNewsletter: function(newsletter) {
var type = this.getSubtype(newsletter);
return type.getFields(newsletter);
},
getSendButtonOptions: function() {
var type = this.getSubtype(this.state.item);
return type.getSendButtonOptions(this.state.item);
},
getSubtype: function(newsletter) {
switch(newsletter.type) {
case 'notification': return NotificationNewsletterFields;
case 'welcome': return WelcomeNewsletterFields;
default: return StandardNewsletterFields;
}
},
isAutomatedNewsletter: function() {
return this.state.item.type !== 'standard';
},
isValid: function() {
return jQuery('#mailpoet_newsletter').parsley().isValid();
},
@ -105,8 +110,9 @@ define(
}
}).done((response) => {
this.setState({ loading: false });
if(response.result === true) {
this.context.router.push('/');
this.context.router.push(`/${ this.state.item.type || '' }`);
MailPoet.Notice.success(response.data.message);
} else {
if(response.errors) {
@ -123,9 +129,22 @@ define(
},
handleSave: function(e) {
e.preventDefault();
this._save(e).done(() => {
this.context.router.push(`/${ this.state.item.type || '' }`);
});
},
handleRedirectToDesign: function(e) {
e.preventDefault();
var redirectTo = e.target.href;
this._save(e).done(() => {
window.location = redirectTo;
});
},
_save: function(e) {
this.setState({ loading: true });
MailPoet.Ajax.post({
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'save',
data: this.state.item,
@ -133,7 +152,6 @@ define(
this.setState({ loading: false });
if(response.result === true) {
this.context.router.push('/');
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterUpdated')
);
@ -175,10 +193,9 @@ define(
className="button button-primary"
type="button"
onClick={ this.handleSend }
value={
this.isAutomatedNewsletter()
? MailPoet.I18n.t('activate')
: MailPoet.I18n.t('send')} />
value={MailPoet.I18n.t('send')}
{...this.getSendButtonOptions()}
/>
&nbsp;
<input
className="button button-secondary"
@ -188,7 +205,8 @@ define(
<a
href={
'?page=mailpoet-newsletter-editor&id='+this.props.params.id
}>
}
onClick={this.handleRedirectToDesign}>
{MailPoet.I18n.t('goBackToDesign')}
</a>.
</p>

View File

@ -23,7 +23,7 @@ define(
},
{
name: 'options',
label: MailPoet.I18n.t('selectPeriodicity'),
label: MailPoet.I18n.t('selectFrequency'),
type: 'reactComponent',
component: Scheduling,
},
@ -95,6 +95,15 @@ define(
}
];
return {
getFields: function(newsletter) {
return fields;
},
getSendButtonOptions: function(newsletter) {
return {
value: MailPoet.I18n.t('activate')
};
},
};
}
);

View File

@ -253,7 +253,13 @@ define(
var StandardScheduling = React.createClass({
_getCurrentValue: function() {
return this.props.item[this.props.field.name] || {};
return _.defaults(
this.props.item[this.props.field.name] || {},
{
isScheduled: '0',
scheduledAt: defaultDateTime
}
);
},
handleValueChange: function(event) {
var oldValue = this._getCurrentValue(),
@ -401,6 +407,30 @@ define(
}
];
return {
getFields: function(newsletter) {
return fields;
},
getSendButtonOptions: function(newsletter) {
newsletter = newsletter || {};
let isScheduled = (
typeof newsletter.options === 'object'
&& newsletter.options.isScheduled === '1'
);
let options = {
value: (isScheduled
? MailPoet.I18n.t('schedule')
: MailPoet.I18n.t('send'))
};
if (newsletter.status === 'sent'
|| newsletter.status === 'sending') {
options['disabled'] = 'disabled';
}
return options;
},
};
}
);

View File

@ -71,7 +71,16 @@ define(
}
];
return {
getFields: function(newsletter) {
return fields;
},
getSendButtonOptions: function(newsletter) {
return {
value: MailPoet.I18n.t('activate')
};
},
};
}
);

View File

@ -18,7 +18,6 @@ define(
var field = {
name: 'options',
label: 'Periodicity',
type: 'reactComponent',
component: Scheduling,
};
@ -72,7 +71,7 @@ define(
<h1>{MailPoet.I18n.t('postNotificationNewsletterTypeTitle')}</h1>
<Breadcrumb step="type" />
<h3>{MailPoet.I18n.t('selectPeriodicity')}</h3>
<h3>{MailPoet.I18n.t('selectFrequency')}</h3>
<Scheduling
item={this.state}

View File

@ -1,100 +1,48 @@
define(
[
'underscore',
'react',
'react-router',
'mailpoet',
'form/fields/select.jsx'
],
function(
_,
React,
Router,
MailPoet,
Select
) {
import _ from 'underscore'
import React from 'react'
import MailPoet from 'mailpoet'
import Select from 'form/fields/select.jsx'
import {
intervalValues,
timeOfDayValues,
weekDayValues,
monthDayValues,
nthWeekDayValues
} from 'newsletters/scheduling/common.jsx'
var intervalField = {
const intervalField = {
name: 'intervalType',
values: {
'daily': MailPoet.I18n.t('daily'),
'weekly': MailPoet.I18n.t('weekly'),
'monthly': MailPoet.I18n.t('monthly'),
'nthWeekDay': MailPoet.I18n.t('monthlyEvery'),
'immediately': MailPoet.I18n.t('immediately'),
},
values: intervalValues
};
var SECONDS_IN_DAY = 86400;
var TIME_STEP_SECONDS = 3600; // Default: 3600
var numberOfTimeSteps = SECONDS_IN_DAY / TIME_STEP_SECONDS;
var timeOfDayValues = _.object(_.map(
_.times(numberOfTimeSteps, function(step) { return step * TIME_STEP_SECONDS; }),
function(seconds) {
var date = new Date(null);
date.setSeconds(seconds);
var timeLabel = date.toISOString().substr(11, 5);
return [seconds, timeLabel];
}
));
var timeOfDayField = {
const timeOfDayField = {
name: 'timeOfDay',
values: timeOfDayValues,
values: timeOfDayValues
};
var weekDayField = {
const weekDayField = {
name: 'weekDay',
values: {
0: MailPoet.I18n.t('sunday'),
1: MailPoet.I18n.t('monday'),
2: MailPoet.I18n.t('tuesday'),
3: MailPoet.I18n.t('wednesday'),
4: MailPoet.I18n.t('thursday'),
5: MailPoet.I18n.t('friday'),
6: MailPoet.I18n.t('saturday')
},
values: weekDayValues
};
var NUMBER_OF_DAYS_IN_MONTH = 28; // 28 for compatibility with MP2
var monthDayField = {
const monthDayField = {
name: 'monthDay',
values: _.object(_.map(
_.times(NUMBER_OF_DAYS_IN_MONTH, function(day) { return day; }),
function(day) {
var labels = {
0: MailPoet.I18n.t('first'),
1: MailPoet.I18n.t('second'),
2: MailPoet.I18n.t('third')
},
label;
if (labels[day] !== undefined) {
label = labels[day];
} else {
label = MailPoet.I18n.t('nth').replace("%$1d", day + 1);
}
return [day, label];
},
)),
values: monthDayValues
};
var nthWeekDayField = {
const nthWeekDayField = {
name: 'nthWeekDay',
values: {
'1': MailPoet.I18n.t('first'),
'2': MailPoet.I18n.t('second'),
'3': MailPoet.I18n.t('third'),
'L': MailPoet.I18n.t('last'),
},
values: nthWeekDayValues
};
var NotificationScheduling = React.createClass({
const NotificationScheduling = React.createClass({
_getCurrentValue: function() {
return this.props.item[this.props.field.name] || {};
return (this.props.item[this.props.field.name] || {});
},
handleValueChange: function(name, value) {
var oldValue = this._getCurrentValue(),
newValue = {};
const oldValue = this._getCurrentValue();
let newValue = {};
newValue[name] = value;
return this.props.onValueChange({
@ -135,12 +83,11 @@ define(
);
},
render: function() {
var value = this._getCurrentValue(),
timeOfDaySelection,
weekDaySelection,
monthDaySelection,
nthWeekDaySelection;
const value = this._getCurrentValue();
let timeOfDaySelection;
let weekDaySelection;
let monthDaySelection;
let nthWeekDaySelection;
if (value.intervalType !== 'immediately') {
timeOfDaySelection = (
@ -151,8 +98,7 @@ define(
);
}
if (value.intervalType === 'weekly'
|| value.intervalType === 'nthWeekDay') {
if (value.intervalType === 'weekly' || value.intervalType === 'nthWeekDay') {
weekDaySelection = (
<Select
field={weekDayField}
@ -192,9 +138,7 @@ define(
{timeOfDaySelection}
</div>
);
},
}
});
return NotificationScheduling;
}
);
module.exports = NotificationScheduling;

View File

@ -1,79 +1,71 @@
define(
[
'underscore',
'react',
'react-router',
'mailpoet',
'form/fields/select.jsx',
'form/fields/text.jsx',
'newsletters/breadcrumb.jsx'
],
function(
_,
React,
Router,
MailPoet,
Select,
Text,
Breadcrumb
) {
import _ from 'underscore'
import React from 'react'
import MailPoet from 'mailpoet'
import Select from 'form/fields/select.jsx'
import Text from 'form/fields/text.jsx'
import {
timeDelayValues,
intervalValues
} from 'newsletters/scheduling/common.jsx'
var availableRoles = window.mailpoet_roles || {};
var availableSegments = window.mailpoet_segments || {};
const availableRoles = window.mailpoet_roles || {};
const availableSegments = _.filter(
window.mailpoet_segments || [],
function (segment) {
return segment.type === 'default';
}
);
var events = {
const events = {
name: 'event',
values: {
'segment': MailPoet.I18n.t('onSubscriptionToList'),
'user': MailPoet.I18n.t('onWordpressUserRegistration'),
'user': MailPoet.I18n.t('onWPUserRegistration'),
}
};
var availableSegmentValues = _.object(_.map(
const availableSegmentValues = _.object(_.map(
availableSegments,
function(segment) {
var name = segment.name;
let name = segment.name;
if (segment.subscribers > 0) {
name += ' (%$1s)'.replace('%$1s', parseInt(segment.subscribers).toLocaleString());
}
return [segment.id, name];
}
));
var segmentField = {
const segmentField = {
name: 'segment',
values: availableSegmentValues,
sortBy: (key, value) => value.toLowerCase()
};
var roleField = {
const roleField = {
name: 'role',
values: availableRoles,
values: availableRoles
};
var afterTimeNumberField = {
const afterTimeNumberField = {
name: 'afterTimeNumber',
size: 3,
size: 3
};
var afterTimeTypeField = {
const afterTimeTypeField = {
name: 'afterTimeType',
values: {
'immediate': MailPoet.I18n.t('delayImmediately'),
'hours': MailPoet.I18n.t('delayHoursAfter'),
'days': MailPoet.I18n.t('delayDaysAfter'),
'weeks': MailPoet.I18n.t('delayWeeksAfter'),
}
values: timeDelayValues
};
var WelcomeScheduling = React.createClass({
const WelcomeScheduling = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
_getCurrentValue: function() {
return this.props.item[this.props.field.name] || {};
return (this.props.item[this.props.field.name] || {});
},
handleValueChange: function(name, value) {
var oldValue = this._getCurrentValue(),
newValue = {};
const oldValue = this._getCurrentValue();
let newValue = {};
newValue[name] = value;
return this.props.onValueChange({
@ -119,8 +111,8 @@ define(
action: 'create',
data: {
type: 'welcome',
options: this.state,
},
options: this.state
}
}).done(function(response) {
if (response.result && response.newsletter.id) {
this.showTemplateSelection(response.newsletter.id);
@ -137,8 +129,9 @@ define(
this.context.router.push(`/template/${ newsletterId }`);
},
render: function() {
var value = this._getCurrentValue(),
roleSegmentSelection, timeNumber;
const value = this._getCurrentValue();
let roleSegmentSelection;
let timeNumber;
if (value.event === 'user') {
roleSegmentSelection = (
@ -184,7 +177,4 @@ define(
},
});
return WelcomeScheduling;
}
);
module.exports = WelcomeScheduling;

View File

@ -1,6 +1,5 @@
import React from 'react'
import { Router, Link } from 'react-router'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
@ -15,23 +14,19 @@ var columns = [
},
{
name: 'description',
label: MailPoet.I18n.t('description'),
sortable: false
label: MailPoet.I18n.t('description')
},
{
name: 'subscribed',
label: MailPoet.I18n.t('subscribed'),
sortable: false
label: MailPoet.I18n.t('subscribed')
},
{
name: 'unconfirmed',
label: MailPoet.I18n.t('unconfirmed'),
sortable: false
label: MailPoet.I18n.t('unconfirmed')
},
{
name: 'unsubscribed',
label: MailPoet.I18n.t('unsubscribed'),
sortable: false
label: MailPoet.I18n.t('unsubscribed')
},
{
name: 'created_at',
@ -191,13 +186,20 @@ const SegmentList = React.createClass({
const unconfirmed = ~~(segment.subscribers_count.unconfirmed || 0);
const unsubscribed = ~~(segment.subscribers_count.unsubscribed || 0);
let segment_name = (
<Link to={ `/edit/${segment.id}` }>{ segment.name }</Link>
);
let segment_name;
// the WP users segment is not editable so just display its name
if (segment.type === 'wp_users') {
segment_name = segment.name;
// the WP users segment is not editable so just display its name
segment_name = (
<span className="row-title">{ segment.name }</span>
);
} else {
segment_name = (
<Link
className="row-title"
to={ `/edit/${segment.id}` }
>{ segment.name }</Link>
);
}
return (
@ -244,6 +246,8 @@ const SegmentList = React.createClass({
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ item_actions }
sort_by="name"
sort_order="asc"
/>
</div>
);

View File

@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import SegmentList from 'segments/list.jsx'
import SegmentForm from 'segments/form.jsx'

View File

@ -34,16 +34,13 @@ define(
// show sending methods
jQuery('.mailpoet_sending_methods').fadeIn();
} else {
// toggle SPF/DKIM (hidden if the sending method is MailPoet)
// toggle SPF (hidden if the sending method is MailPoet)
jQuery('#mailpoet_mta_spf')[
(group === 'mailpoet')
? 'hide'
: 'show'
]();
// (HIDDEN FOR BETA)
jQuery('#mailpoet_mta_dkim').hide();
// hide sending methods
jQuery('.mailpoet_sending_methods').hide();

View File

@ -73,7 +73,7 @@ define(
});
},
filter: function(segment) {
return !!(!segment.deleted_at);
return !!(!segment.deleted_at && segment.type === 'default');
},
getSearchLabel: function(segment, subscriber) {
let label = '';

View File

@ -154,8 +154,7 @@ define(
.replace('%1$s', '<strong>' + parseInt(response.data.totalExported).toLocaleString() + '</strong>')
.replace('[link]', '<a href="' + response.data.exportFileURL + '" target="_blank" >')
.replace('[/link]', '</a>');
jQuery('#export_result_notice > ul > li').html(resultMessage);
jQuery('#export_result_notice').show();
jQuery('#export_result_notice').html('<p>' + resultMessage + '</p>').show();
window.location.href = response.data.exportFileURL;
}
})

View File

@ -6,9 +6,10 @@ define(
'mailpoet',
'handlebars',
'papaparse',
'select2',
'asyncqueue',
'xss'
'xss',
'moment',
'select2'
],
function (
Backbone,
@ -18,7 +19,8 @@ define(
Handlebars,
Papa,
AsyncQueue,
xss
xss,
Moment
) {
if (!jQuery('#mailpoet_subscribers_import').length) {
return;
@ -69,7 +71,7 @@ define(
jQuery('#method_paste > div.mailpoet_method_process')
.find('a.mailpoet_process'),
mailChimpKeyInputElement = jQuery('#mailchimp_key'),
mailChimpKeyVerifyButtonEelement = jQuery('#mailchimp_key_verify'),
mailChimpKeyVerifyButtonElement = jQuery('#mailchimp_key_verify'),
mailChimpListsContainerElement = jQuery('#mailchimp_lists'),
mailChimpProcessButtonElement =
jQuery('#method_mailchimp > div.mailpoet_method_process')
@ -176,15 +178,11 @@ define(
jQuery('.mailpoet_mailchimp-key-status')
.html('')
.removeClass('mailpoet_mailchimp-ok mailpoet_mailchimp-error');
mailChimpKeyVerifyButtonEelement.prop('disabled', true);
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
}
else {
mailChimpKeyVerifyButtonEelement.prop('disabled', false);
}
});
mailChimpKeyVerifyButtonEelement.click(function () {
mailChimpKeyVerifyButtonElement.click(function () {
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
endpoint: 'ImportExport',
@ -307,25 +305,25 @@ define(
// trim spaces, commas, periods,
// single/double quotes and convert to lowercase
detectAndCleanupEmail = function (email) {
email = email.toLowerCase();
var test,
cleanEmail =
email
var test;
// decode HTML entities
email = jQuery('<div />').html(email).text();
email = email
.toLowerCase()
// left/right trim spaces, punctuation (e.g., " 'email@email.com'; ")
// right trim non-printable characters (e.g., "email@email.com<6F>")
.replace(/^["';.,\s]+|[^\x20-\x7E]+$|["';.,_\s]+$/g, '')
// remove spaces (e.g., "email @ email . com")
// remove urlencoded characters
.replace(/\s+|%\d+|,+/g, '')
.toLowerCase();
// detect e-mails that will otherwise be rejected by ^email_regex$
.replace(/\s+|%\d+|,+/g, '');
// detect e-mails that will be otherwise rejected by email regex
if (test = /<(.*?)>/.exec(email)) {
// is email inside angle brackets (e.g., 'some@email.com <some@email.com>')?
return test[1].trim();
// is the email inside angle brackets (e.g., 'some@email.com <some@email.com>')?
email = test[1].trim();
}
else if (test = /mailto:(?:\s+)?(.*)/.exec(email)) {
// is email in 'mailto:email' format?
return test[1].trim();
if (test = /mailto:(?:\s+)?(.*)/.exec(email)) {
// is the email in 'mailto:email' format?
email = test[1].trim();
}
return email;
};
@ -527,6 +525,7 @@ define(
segmentSelectElement
.html('')
.select2('destroy');
toggleNextStepButton('off');
}
segmentSelectElement
.select2({
@ -924,31 +923,9 @@ define(
// DATE filter: if column type is date, check if we can recognize it
if (column.type === 'date' && matchedColumn !== -1) {
jQuery.map(subscribersClone.subscribers, function (data, position) {
var rowData = data[matchedColumn],
date = new Date(rowData.replace(/-/g, '/')), // IE doesn't like
// dashes as date separators
month_name = [
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')
];
var rowData = data[matchedColumn];
if (position !== fillterPosition) {
// check for valid date:
// * invalid date object returns NaN for getTime() and NaN
// is the only object not strictly equal to itself
// * date must have period/dash/slash OR be at least 4
// characters long (e.g., year)
// * must be before now
// check if date exists
if (rowData.trim() === '') {
data[matchedColumn] =
'<span class="mailpoet_data_match mailpoet_import_error" title="'
@ -958,25 +935,12 @@ define(
preventNextStep = true;
return;
}
else if (date.getTime() === date.getTime() &&
(/[.-\/]/.test(rowData) || rowData.length >= 4) &&
date.getTime() < (new Date()).getTime()
) {
date = '/ '
+ month_name[date.getMonth()]
+ ' ' + date.getDate() + ', '
+ date.getFullYear() + ' '
+ date.getHours() + ':'
+ ((date.getMinutes() < 10 ? '0' : '')
+ date.getMinutes()) + ' '
+ ((date.getHours() >= 12)
? MailPoet.I18n.t('pm')
: MailPoet.I18n.t('am')
);
// check if date is valid and is before today
if (Moment(rowData).isValid() && Moment(rowData).isBefore(Moment())) {
data[matchedColumn] +=
'<span class="mailpoet_data_match" title="'
+ MailPoet.I18n.t('verifyDateMatch') + '">'
+ MailPoet.Date.format(date) + '</span>';
+ MailPoet.Date.format(rowData) + '</span>';
}
else {
data[matchedColumn] +=
@ -1083,6 +1047,7 @@ define(
importResults.created = response.data.created;
importResults.updated = response.data.updated;
importResults.segments = response.data.segments;
importResults.added_to_segment_with_welcome_notification = response.data.added_to_segment_with_welcome_notification;
}
queue.run();
})
@ -1112,7 +1077,6 @@ define(
});
importData.step2 = importResults;
enableSegmentSelection(mailpoetSegments);
toggleNextStepButton('off');
router.navigate('step3', {trigger: true});
}
});
@ -1151,9 +1115,8 @@ define(
.replace('%1$s', '<strong>' + importData.step2.updated.toLocaleString() + '</strong>')
.replace('%2$s', '"' + importData.step2.segments.join('", "') + '"')
: false,
noaction: (!importData.step2.updated && !importData.step2.created)
? true
: false
no_action: (!importData.step2.created && !importData.step2.updated),
added_to_segment_with_welcome_notification: importData.step2.added_to_segment_with_welcome_notification
};
jQuery('#subscribers_data_import_results')

View File

@ -21,8 +21,7 @@ const columns = [
},
{
name: 'segments',
label: MailPoet.I18n.t('lists'),
sortable: false
label: MailPoet.I18n.t('lists')
},
{
@ -48,7 +47,7 @@ const messages = {
} else if (~~response > 1) {
message = (
MailPoet.I18n.t('multipleSubscribersTrashed')
).replace('%$1d', ~~response);
).replace('%$1d', (~~response).toLocaleString());
}
if (message !== null) {
@ -84,7 +83,7 @@ const messages = {
} else if (~~response > 1) {
message = (
MailPoet.I18n.t('multipleSubscribersRestored')
).replace('%$1d', ~~response);
).replace('%$1d', (~~response).toLocaleString());
}
if (message !== null) {
@ -106,11 +105,6 @@ const bulk_actions = [
return !!(
!segment.deleted_at && segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -126,7 +120,7 @@ const bulk_actions = [
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersMovedToList')
.replace('%$1d', ~~response.subscribers)
.replace('%$1d', (~~(response.subscribers)).toLocaleString())
.replace('%$2s', response.segment)
);
}
@ -142,11 +136,6 @@ const bulk_actions = [
return !!(
!segment.deleted_at && segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -162,7 +151,7 @@ const bulk_actions = [
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersAddedToList')
.replace('%$1d', ~~response.subscribers)
.replace('%$1d', (~~response.subscribers).toLocaleString())
.replace('%$2s', response.segment)
);
}
@ -178,11 +167,6 @@ const bulk_actions = [
return !!(
segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -198,7 +182,7 @@ const bulk_actions = [
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersRemovedFromList')
.replace('%$1d', ~~response.subscribers)
.replace('%$1d', (~~response.subscribers).toLocaleString())
.replace('%$2s', response.segment)
);
}
@ -209,7 +193,7 @@ const bulk_actions = [
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersRemovedFromAllLists')
.replace('%$1d', ~~response)
.replace('%$1d', (~~response).toLocaleString())
);
}
},
@ -219,7 +203,7 @@ const bulk_actions = [
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleConfirmationEmailsSent')
.replace('%$1d', ~~response)
.replace('%$1d', (~~response).toLocaleString())
);
}
},
@ -234,14 +218,17 @@ const item_actions = [
{
name: 'edit',
label: MailPoet.I18n.t('edit'),
link: function(item) {
link: function(subscriber) {
return (
<Link to={ `/edit/${item.id}` }>{MailPoet.I18n.t('edit')}</Link>
<Link to={ `/edit/${subscriber.id}` }>{MailPoet.I18n.t('edit')}</Link>
);
}
},
{
name: 'trash'
name: 'trash',
display: function(subscriber) {
return !!(~~subscriber.wp_user_id === 0);
}
}
];
@ -280,15 +267,11 @@ const SubscriberList = React.createClass({
}
let segments = false;
let subscribed_segments = [];
// WordPress Users
if (~~(subscriber.wp_user_id) > 0) {
subscribed_segments.push(MailPoet.I18n.t('WPUsersSegment'));
}
// Subscriptions
if (subscriber.subscriptions.length > 0) {
let subscribed_segments = [];
subscriber.subscriptions.map((subscription) => {
const segment = this.getSegmentFromId(subscription.segment_id);
if (segment === false) return;
@ -296,13 +279,14 @@ const SubscriberList = React.createClass({
subscribed_segments.push(segment.name);
}
});
}
segments = (
<span>
{ subscribed_segments.join(', ') }
</span>
);
}
let avatar = false;
if (subscriber.avatar_url) {
@ -320,9 +304,12 @@ const SubscriberList = React.createClass({
return (
<div>
<td className={ row_classes }>
<strong><Link to={ `/edit/${ subscriber.id }` }>
{ subscriber.email }
</Link></strong>
<strong>
<Link
className="row-title"
to={ `/edit/${ subscriber.id }` }
>{ subscriber.email }</Link>
</strong>
<p style={{margin: 0}}>
{ subscriber.first_name } { subscriber.last_name }
</p>
@ -366,6 +353,8 @@ const SubscriberList = React.createClass({
item_actions={ item_actions }
messages={ messages }
onGetItems={ this.onGetItems }
sort_by={ 'created_at' }
sort_order={ 'desc' }
/>
</div>
)

View File

@ -26,6 +26,7 @@ coverage:
include:
- lib/*
exclude:
blacklist:
include:
exclude:
- lib/Util/Sudzy/*
- lib/Util/CSS.php
- lib/Util/Helpers.php
- lib/Util/XLSXWriter.php

View File

@ -1,4 +1,10 @@
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mailpoet/html2text"
}
],
"require": {
"php": ">=5.3.3",
"twig/twig": "1.*",
@ -11,7 +17,7 @@
"phpseclib/phpseclib": "*",
"mtdowling/cron-expression": "^1.1",
"nesbot/carbon": "^1.21",
"soundasleep/html2text": "^0.3.0"
"soundasleep/html2text": "dev-master"
},
"require-dev": {
"codeception/codeception": "*",
@ -19,7 +25,8 @@
"codegyre/robo": "*",
"vlucas/phpdotenv": "*",
"umpirsky/twig-gettext-extractor": "1.1.*",
"raveren/kint": "^1.0"
"raveren/kint": "^1.0",
"squizlabs/php_codesniffer": "2.*"
},
"autoload": {
"psr-4": {

1020
composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

83
lib/API/API.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace MailPoet\API;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class API {
public $api_request;
public $endpoint;
public $action;
public $data;
const NAME = 'mailpoet_api';
const ENDPOINT_NAMESPACE = '\MailPoet\API\Endpoints\\';
const RESPONSE_ERROR = 404;
function __construct($api_data = false) {
$api_data = ($api_data) ? $api_data : $_GET;
$this->api_request = isset($api_data[self::NAME]);
$this->endpoint = isset($api_data['endpoint']) ?
Helpers::underscoreToCamelCase($api_data['endpoint']) :
false;
$this->action = isset($api_data['action']) ?
Helpers::underscoreToCamelCase($api_data['action']) :
false;
$this->data = isset($api_data['data']) ?
self::decodeRequestData($api_data['data']) :
false;
}
function init() {
$endpoint = self::ENDPOINT_NAMESPACE . ucfirst($this->endpoint);
if(!$this->api_request) return;
if(!$this->endpoint || !class_exists($endpoint)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
}
$this->callEndpoint(
$endpoint,
$this->action,
$this->data
);
}
function callEndpoint($endpoint, $action, $data) {
if(!method_exists($endpoint, $action)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
}
call_user_func(
array(
$endpoint,
$action
),
$data
);
}
static function decodeRequestData($data) {
$data = base64_decode($data);
return (is_serialized($data)) ?
unserialize($data) :
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API data format.'));
}
static function encodeRequestData($data) {
return rtrim(base64_encode(serialize($data)), '=');
}
static function buildRequest($endpoint, $action, $data) {
$data = self::encodeRequestData($data);
$params = array(
self::NAME => '',
'endpoint' => $endpoint,
'action' => $action,
'data' => $data
);
return add_query_arg($params, home_url());
}
function terminateRequest($code, $message) {
status_header($code, $message);
exit;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Cron\Daemon;
if(!defined('ABSPATH')) exit;
class Queue {
const ENDPOINT = 'queue';
const ACTION_RUN = 'run';
static function run($data) {
$queue = new Daemon($data);
$queue->run();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Subscription as UserSubscription;
if(!defined('ABSPATH')) exit;
class Subscription {
const ENDPOINT = 'subscription';
static function confirm($data) {
$subscription = new UserSubscription\Pages('confirm', $data);
}
static function manage($data) {
$subscription = new UserSubscription\Pages('manage', $data);
}
static function unsubscribe($data) {
$subscription = new UserSubscription\Pages('unsubscribe', $data);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
if(!defined('ABSPATH')) exit;
class Track {
const ENDPOINT = 'track';
const ACTION_CLICK = 'click';
const ACTION_OPEN = 'open';
static function click($data) {
$clicks = new Clicks($data);
$clicks->track();
}
static function open($data) {
$opens = new Opens($data);
$opens->track();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\Newsletter\ViewInBrowser as NewsletterViewInBrowser;
if(!defined('ABSPATH')) exit;
class ViewInBrowser {
const ENDPOINT = 'view_in_browser';
const ACTION_VIEW = 'view';
static function view($data) {
$viewer = new NewsletterViewInBrowser($data);
$viewer->view();
}
}

View File

@ -7,8 +7,6 @@ class Reporter {
'Plugin Version' => 'pluginVersion',
);
function __construct() {}
function getData() {
$_this = $this;

View File

@ -10,8 +10,7 @@ class Analytics {
}
function init() {
// review: this creates a fatal error when mailpoet tables are dropped.
//add_action('admin_enqueue_scripts', array($this, 'setupAdminDependencies'));
add_action('admin_enqueue_scripts', array($this, 'setupAdminDependencies'));
}
function setupAdminDependencies() {

View File

@ -81,6 +81,16 @@ class Hooks {
);
}
}
// Manage subscription
add_action(
'admin_post_mailpoet_subscription_update',
'\MailPoet\Subscription\Manage::onSave'
);
add_action(
'admin_post_nopriv_mailpoet_subscription_update',
'\MailPoet\Subscription\Manage::onSave'
);
}
function setupWPUsers() {

View File

@ -33,7 +33,6 @@ class Initializer {
$this->setupRenderer();
$this->setupLocalizer();
$this->setupMenu();
$this->setupPermissions();
$this->setupAnalytics();
$this->setupChangelog();
$this->setupShortcodes();
@ -49,7 +48,7 @@ class Initializer {
function onInit() {
$this->setupRouter();
$this->setupPublicAPI();
$this->setupAPI();
$this->setupPages();
}
@ -57,11 +56,6 @@ class Initializer {
\ORM::configure(Env::$db_source_name);
\ORM::configure('username', Env::$db_username);
\ORM::configure('password', Env::$db_password);
\ORM::configure('logging', WP_DEBUG);
\ORM::configure('logger', function($query, $time) {
// error_log("\n".$query."\n");
});
\ORM::configure('driver_options', array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
\PDO::MYSQL_ATTR_INIT_COMMAND =>
@ -151,11 +145,6 @@ class Initializer {
$widget->init();
}
function setupPermissions() {
$permissions = new Permissions();
$permissions->init();
}
function setupChangelog() {
$changelog = new Changelog();
$changelog->init();
@ -176,9 +165,9 @@ class Initializer {
$hooks->init();
}
function setupPublicAPI() {
$publicAPI = new PublicAPI();
$publicAPI->init();
function setupAPI() {
$API = new \MailPoet\API\API();
$API->init();
}
function runQueueSupervisor() {
@ -187,6 +176,7 @@ class Initializer {
$supervisor = new Supervisor();
$supervisor->checkDaemon();
} catch(\Exception $e) {
// Prevent Daemon exceptions from breaking out and breaking UI
}
}

View File

@ -7,12 +7,10 @@ use MailPoet\Models\CustomField;
use MailPoet\Models\Form;
use MailPoet\Models\Segment;
use MailPoet\Models\Setting;
use MailPoet\Settings\Charsets;
use MailPoet\Newsletter\Shortcodes\ShortcodesHelper;
use MailPoet\Settings\Hosts;
use MailPoet\Settings\Pages;
use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Util\DKIM;
use MailPoet\Util\Permissions;
use MailPoet\Subscribers\ImportExport\ImportExportFactory;
use MailPoet\Listing;
use MailPoet\WP\DateTime;
@ -35,28 +33,25 @@ class Menu {
}
function setup() {
$main_page_slug = 'mailpoet-newsletters';
add_menu_page(
'MailPoet',
'MailPoet',
'manage_options',
'mailpoet',
array(
$this,
'home'
),
$main_page_slug,
null,
$this->assets_url . '/img/menu_icon.png',
30
);
$newsletters_page = add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle(__('Newsletters')),
__('Newsletters'),
'manage_options',
'mailpoet-newsletters',
array(
$this,
'newsletters'
)
$main_page_slug,
array($this, 'newsletters')
);
// add limit per page to screen options
@ -71,15 +66,12 @@ class Menu {
});
$forms_page = add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle(__('Forms')),
__('Forms'),
'manage_options',
'mailpoet-forms',
array(
$this,
'forms'
)
array($this, 'forms')
);
// add limit per page to screen options
add_action('load-'.$forms_page, function() {
@ -93,15 +85,12 @@ class Menu {
});
$subscribers_page = add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle(__('Subscribers')),
__('Subscribers'),
'manage_options',
'mailpoet-subscribers',
array(
$this,
'subscribers'
)
array($this, 'subscribers')
);
// add limit per page to screen options
add_action('load-'.$subscribers_page, function() {
@ -115,15 +104,12 @@ class Menu {
});
$segments_page = add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle(__('Segments')),
__('Segments'),
'manage_options',
'mailpoet-segments',
array(
$this,
'segments'
)
array($this, 'segments')
);
// add limit per page to screen options
@ -138,15 +124,12 @@ class Menu {
});
add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle( __('Settings')),
__('Settings'),
'manage_options',
'mailpoet-settings',
array(
$this,
'settings'
)
array($this, 'settings')
);
add_submenu_page(
'admin.php?page=mailpoet-subscribers',
@ -154,10 +137,7 @@ class Menu {
__('Import'),
'manage_options',
'mailpoet-import',
array(
$this,
'import'
)
array($this, 'import')
);
add_submenu_page(
@ -166,10 +146,7 @@ class Menu {
__('Export'),
'manage_options',
'mailpoet-export',
array(
$this,
'export'
)
array($this, 'export')
);
add_submenu_page(
@ -178,10 +155,7 @@ class Menu {
__('Welcome'),
'manage_options',
'mailpoet-welcome',
array(
$this,
'welcome'
)
array($this, 'welcome')
);
add_submenu_page(
@ -190,10 +164,7 @@ class Menu {
__('Update'),
'manage_options',
'mailpoet-update',
array(
$this,
'update'
)
array($this, 'update')
);
add_submenu_page(
@ -202,42 +173,28 @@ class Menu {
__('Form editor'),
'manage_options',
'mailpoet-form-editor',
array(
$this,
'formEditor'
)
array($this, 'formEditor')
);
add_submenu_page(
true,
$this->setPageTitle(__('Newsletter')),
__('Newsletter editor'),
__('Newsletter Editor'),
'manage_options',
'mailpoet-newsletter-editor',
array(
$this,
'newletterEditor'
)
array($this, 'newletterEditor')
);
add_submenu_page(
'mailpoet',
$main_page_slug,
$this->setPageTitle(__('Cron')),
__('Cron'),
'manage_options',
'mailpoet-cron',
array(
$this,
'cron'
)
array($this, 'cron')
);
}
function home() {
$data = array();
echo $this->renderer->render('index.html', $data);
}
function welcome() {
if((bool)(defined('DOING_AJAX') && DOING_AJAX)) return;
@ -253,13 +210,14 @@ class Menu {
or
strpos($redirect_url, 'mailpoet') === false
) {
$redirect_url = admin_url('admin.php?page=mailpoet');
$redirect_url = admin_url('admin.php?page=mailpoet-newsletters');
}
$data = array(
'settings' => Setting::getAll(),
'current_user' => wp_get_current_user(),
'redirect_url' => $redirect_url
'redirect_url' => $redirect_url,
'sub_menu' => 'mailpoet-newsletters'
);
echo $this->renderer->render('welcome.html', $data);
}
@ -277,13 +235,14 @@ class Menu {
or
strpos($redirect_url, 'mailpoet') === false
) {
$redirect_url = admin_url('admin.php?page=mailpoet');
$redirect_url = admin_url('admin.php?page=mailpoet-newsletters');
}
$data = array(
'settings' => Setting::getAll(),
'current_user' => wp_get_current_user(),
'redirect_url' => $redirect_url
'redirect_url' => $redirect_url,
'sub_menu' => 'mailpoet-newsletters'
);
echo $this->renderer->render('update.html', $data);
@ -293,29 +252,12 @@ class Menu {
$settings = Setting::getAll();
$flags = $this->_getFlags();
// dkim: check if public/private keys have been generated
if(
empty($settings['dkim'])
or empty($settings['dkim']['public_key'])
or empty($settings['dkim']['private_key'])
) {
// generate public/private keys
$keys = DKIM::generateKeys();
$settings['dkim'] = array(
'public_key' => $keys['public'],
'private_key' => $keys['private'],
'domain' => preg_replace('/^www\./', '', $_SERVER['SERVER_NAME'])
);
}
$data = array(
'settings' => $settings,
'segments' => Segment::getPublic()->findArray(),
'pages' => Pages::getAll(),
'flags' => $flags,
'charsets' => Charsets::getAll(),
'current_user' => wp_get_current_user(),
'permissions' => Permissions::getAll(),
'hosts' => array(
'web' => Hosts::getWebHosts(),
'smtp' => Hosts::getSMTPHosts()
@ -355,7 +297,7 @@ class Menu {
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('subscribers');
$data['segments'] = Segment::getSegmentsWithSubscriberCount();
$data['segments'] = Segment::findArray();
$data['custom_fields'] = array_map(function($field) {
$field['params'] = unserialize($field['params']);
@ -398,7 +340,7 @@ class Menu {
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('newsletters');
$data['segments'] = Segment::getSegmentsWithSubscriberCount();
$data['segments'] = Segment::getSegmentsWithSubscriberCount($type = false);
$data['settings'] = Setting::getAll();
$data['roles'] = $wp_roles->get_names();
$data['roles']['mailpoet_all'] = __('In any WordPress role');
@ -412,6 +354,8 @@ class Menu {
24
);
$data['tracking_enabled'] = Setting::getValue('tracking.enabled');
wp_enqueue_script('jquery-ui');
wp_enqueue_script('jquery-ui-datepicker');
@ -419,15 +363,8 @@ class Menu {
}
function newletterEditor() {
$custom_fields = array_map(function($field) {
return array(
'text' => $field['name'],
'shortcode' => 'field:' . $field['id'],
);
}, CustomField::findArray());
$data = array(
'customFields' => $custom_fields,
'shortcodes' => ShortcodesHelper::getShortcodes(),
'settings' => Setting::getAll(),
'sub_menu' => 'mailpoet-newsletters'
);
@ -438,14 +375,14 @@ class Menu {
}
function import() {
$import = new BootStrapMenu('import');
$import = new ImportExportFactory('import');
$data = $import->bootstrap();
$data['sub_menu'] = 'mailpoet-subscribers';
echo $this->renderer->render('subscribers/importExport/import.html', $data);
}
function export() {
$export = new BootStrapMenu('export');
$export = new ImportExportFactory('export');
$data = $export->bootstrap();
$data['sub_menu'] = 'mailpoet-subscribers';
echo $this->renderer->render('subscribers/importExport/export.html', $data);
@ -484,7 +421,7 @@ class Menu {
);
}
function getLimitPerPage($model = null) {
private function getLimitPerPage($model = null) {
if($model === null) {
return Listing\Handler::DEFAULT_LIMIT_PER_PAGE;
}

View File

@ -1,6 +1,10 @@
<?php
namespace MailPoet\Config;
use MailPoet\Models\Subscriber;
use MailPoet\Models\Newsletter;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@ -38,7 +42,8 @@ class Migrator {
$_this = $this;
$migrate = function($model) use($_this) {
dbDelta($_this->$model());
$modelMethod = Helpers::underscoreToCamelCase($model);
dbDelta($_this->$modelMethod());
};
array_map($migrate, $this->models);
@ -62,8 +67,8 @@ class Migrator {
'type varchar(90) NOT NULL DEFAULT "default",',
'description varchar(250) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
);
@ -75,7 +80,7 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(20) NOT NULL,',
'value longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
@ -83,13 +88,13 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function custom_fields() {
function customFields() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'type varchar(90) NOT NULL,',
'params longtext NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
@ -97,7 +102,7 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function sending_queues() {
function sendingQueues() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
@ -113,7 +118,7 @@ class Migrator {
'scheduled_at TIMESTAMP NULL,',
'processed_at TIMESTAMP NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)',
);
@ -127,9 +132,9 @@ class Migrator {
'first_name tinytext NOT NULL DEFAULT "",',
'last_name tinytext NOT NULL DEFAULT "",',
'email varchar(150) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "unconfirmed",',
'status varchar(12) NOT NULL DEFAULT "' . Subscriber::STATUS_UNCONFIRMED . '",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id),',
'UNIQUE KEY email (email)'
@ -137,28 +142,28 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function subscriber_segment() {
function subscriberSegment() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'subscriber_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "subscribed",',
'status varchar(12) NOT NULL DEFAULT "' . Subscriber::STATUS_SUBSCRIBED . '",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_segment (subscriber_id,segment_id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function subscriber_custom_field() {
function subscriberCustomField() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'subscriber_id mediumint(9) NOT NULL,',
'custom_field_id mediumint(9) NOT NULL,',
'value varchar(255) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_id_custom_field_id (subscriber_id,custom_field_id)'
);
@ -172,19 +177,20 @@ class Migrator {
'type varchar(20) NOT NULL DEFAULT "standard",',
'sender_address varchar(150) NOT NULL DEFAULT "",',
'sender_name varchar(150) NOT NULL DEFAULT "",',
'status varchar(20) NOT NULL DEFAULT "'.Newsletter::STATUS_DRAFT.'",',
'reply_to_address varchar(150) NOT NULL DEFAULT "",',
'reply_to_name varchar(150) NOT NULL DEFAULT "",',
'preheader varchar(250) NOT NULL DEFAULT "",',
'body longtext,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_templates() {
function newsletterTemplates() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(250) NOT NULL,',
@ -193,53 +199,53 @@ class Migrator {
'thumbnail LONGTEXT,',
'readonly TINYINT(1) DEFAULT 0,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_option_fields() {
function newsletterOptionFields() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'newsletter_type varchar(90) NOT NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name_newsletter_type (newsletter_type,name)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_option() {
function newsletterOption() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'option_field_id mediumint(9) NOT NULL,',
'value varchar(255) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_id_option_field_id (newsletter_id,option_field_id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_segment() {
function newsletterSegment() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_segment (newsletter_id,segment_id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_links() {
function newsletterLinks() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
@ -247,18 +253,18 @@ class Migrator {
'url varchar(255) NOT NULL,',
'hash varchar(20) NOT NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_posts() {
function newsletterPosts() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'post_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
@ -273,14 +279,14 @@ class Migrator {
'settings longtext,',
'styles longtext,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_newsletters() {
function statisticsNewsletters() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
@ -292,7 +298,7 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_clicks() {
function statisticsClicks() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
@ -301,42 +307,42 @@ class Migrator {
'link_id mediumint(9) NOT NULL,',
'count mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_opens() {
function statisticsOpens() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_unsubscribes() {
function statisticsUnsubscribes() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_forms() {
function statisticsForms() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'form_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY form_subscriber (form_id,subscriber_id)'
);
@ -344,7 +350,7 @@ class Migrator {
}
private function sqlify($model, $attributes) {
$table = $this->prefix . $model;
$table = $this->prefix . Helpers::camelCaseToUnderscore($model);
$sql = array();
$sql[] = "CREATE TABLE " . $table . " (";

View File

@ -1,43 +0,0 @@
<?php
namespace MailPoet\Config;
class Permissions {
function __construct() {
}
function init() {
add_action(
'admin_init',
array($this, 'setup')
);
}
function setup() {
// administrative roles
$roles = array('administrator', 'super_admin');
// mailpoet capabilities
$capabilities = array(
'mailpoet_newsletters',
'mailpoet_newsletter_styles',
'mailpoet_subscribers',
'mailpoet_settings',
'mailpoet_statistics'
);
foreach($roles as $role_key){
// get role based on role key
$role = get_role($role_key);
// if the role doesn't exist, skip it
if($role !== null) {
// add capability
foreach($capabilities as $capability) {
if(!$role->has_cap($capability)) {
$role->add_cap($capability);
}
}
}
}
}
}

View File

@ -9,6 +9,7 @@ use \MailPoet\Models\Segment;
use \MailPoet\Segments\WP;
use \MailPoet\Models\Setting;
use \MailPoet\Settings\Pages;
use \MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
@ -26,25 +27,6 @@ class Populator {
function up() {
global $wpdb;
$_this = $this;
$populate = function($model) use($_this, $wpdb) {
$fields = $_this->$model();
$table = $_this->prefix . $model;
array_map(function($field) use ($wpdb, $table) {
$column_conditions = array_map(function($key) use ($field) {
return $key . '=' . $field[$key];
}, $field);
if($wpdb->get_var("SELECT COUNT(*) FROM " . $table . " WHERE " . implode(' AND ', $column_conditions)) === 0) {
$wpdb->insert(
$table,
$field
);
}
}, $fields);
};
array_map(array($this, 'populate'), $this->models);
$this->createDefaultSegments();
@ -93,9 +75,12 @@ class Populator {
'address' => $current_user->user_email
);
if(!Setting::getValue('sender')) {
// default from name & address
Setting::setValue('sender', $sender);
}
if(!Setting::getValue('signup_confirmation')) {
// enable signup confirmation by default
Setting::setValue('signup_confirmation', array(
'enabled' => true,
@ -106,22 +91,11 @@ class Populator {
'reply_to' => $sender
));
}
}
private function createDefaultSegments() {
// WP Users segment
$wp_users_segment = Segment::getWPUsers();
if($wp_users_segment === false) {
// create the wp users list
$wp_users_segment = Segment::create();
$wp_users_segment->hydrate(array(
'name' => __('WordPress Users'),
'description' =>
__('The list containing all of your WordPress users.'),
'type' => 'wp_users'
));
$wp_users_segment->save();
}
$wp_segment = Segment::getWPSegment();
// Synchronize WP Users
WP::synchronizeUsers();
@ -132,13 +106,13 @@ class Populator {
$default_segment->hydrate(array(
'name' => __('My First List'),
'description' =>
__('The list created automatically on install of MailPoet')
__('The list is automatically created when you install MailPoet')
));
$default_segment->save();
}
}
function newsletter_option_fields() {
private function newsletterOptionFields() {
return array(
array(
'name' => 'isScheduled',
@ -196,7 +170,7 @@ class Populator {
);
}
private function newsletter_templates() {
private function newsletterTemplates() {
return array(
(new FranksRoastHouseTemplate(Env::$assets_url))->get(),
(new BlankTemplate(Env::$assets_url))->get(),
@ -206,7 +180,8 @@ class Populator {
}
private function populate($model) {
$rows = $this->$model();
$modelMethod = Helpers::underscoreToCamelCase($model);
$rows = $this->$modelMethod();
$table = $this->prefix . $model;
$_this = $this;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,77 +0,0 @@
<?php
namespace MailPoet\Config;
use MailPoet\Cron\Daemon;
use MailPoet\Newsletter\Viewer\ViewInBrowser;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
use MailPoet\Subscription;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class PublicAPI {
public $api;
public $endpoint;
public $action;
public $data;
function __construct() {
# http://example.com/?mailpoet&endpoint=&action=&data=
$this->api = isset($_GET['mailpoet']) ? true : false;
$this->endpoint = isset($_GET['endpoint']) ?
Helpers::underscoreToCamelCase($_GET['endpoint']) :
false;
$this->action = isset($_GET['action']) ?
Helpers::underscoreToCamelCase($_GET['action']) :
false;
$this->data = isset($_GET['data']) ?
unserialize(base64_decode($_GET['data'])) :
false;
}
function init() {
if(!$this->api && !$this->endpoint) return;
$this->_checkAndCallMethod($this, $this->endpoint, $terminate_request = true);
}
function queue() {
$queue = new Daemon($this->data);
$this->_checkAndCallMethod($queue, $this->action);
}
function subscription() {
$subscription = new Subscription\Pages($this->action, $this->data);
$this->_checkAndCallMethod($subscription, $this->action);
}
function track() {
if($this->action === 'click') {
$track_class = new Clicks($this->data);
}
if($this->action === 'open') {
$track_class = new Opens($this->data);
}
if(!isset($track_class)) return;
$track_class->track();
}
function viewInBrowser() {
$viewer = new ViewInBrowser($this->data);
$viewer->view();
}
private function _checkAndCallMethod($class, $method, $terminate_request = false) {
if(!method_exists($class, $method)) {
if(!$terminate_request) return;
header('HTTP/1.0 404 Not Found');
exit;
}
call_user_func(
array(
$class,
$method
)
);
}
}

View File

@ -14,7 +14,8 @@ class Renderer {
$file_system,
array(
'cache' => $this->detectCache(),
'debug' => WP_DEBUG
'debug' => WP_DEBUG,
'auto_reload' => true
)
);
}
@ -26,11 +27,12 @@ class Renderer {
$this->setupHandlebars();
$this->setupGlobalVariables();
$this->setupSyntax();
return $this->renderer;
}
function setupTranslations() {
$this->renderer->addExtension(new Twig\i18n(Env::$plugin_name));
$this->renderer->addExtension(new Twig\I18n(Env::$plugin_name));
}
function setupFunctions() {

View File

@ -114,7 +114,7 @@ class Shortcodes {
function renderArchiveSubject($newsletter) {
return '<a href="TODO" target="_blank" title="'
.esc_attr(__('Preview in new tab')).'">'
.esc_attr(__('Preview in a new tab')).'">'
.esc_attr($newsletter->subject).
'</a>';
}

View File

@ -14,20 +14,16 @@ class BootStrapMenu {
return ($this->daemon) ?
array_merge(
array(
'timeSinceStart' =>
Carbon::createFromFormat(
'timeSinceStart' => Carbon::createFromFormat(
'Y-m-d H:i:s',
$this->daemon->created_at,
'UTC'
)
->diffForHumans(),
'timeSinceUpdate' =>
Carbon::createFromFormat(
)->diffForHumans(),
'timeSinceUpdate' => Carbon::createFromFormat(
'Y-m-d H:i:s',
$this->daemon->updated_at,
'UTC'
)
->diffForHumans()
)->diffForHumans()
),
json_decode($this->daemon->value, true)
) :

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Cron;
use MailPoet\API\API;
use MailPoet\API\Endpoints\Queue as QueueAPI;
use MailPoet\Models\Setting;
use MailPoet\Util\Security;
@ -14,7 +16,6 @@ class CronHelper {
static function createDaemon($token) {
$daemon = array(
'status' => Daemon::STATUS_STARTING,
'counter' => 0,
'token' => $token
);
self::saveDaemon($daemon);
@ -38,17 +39,17 @@ class CronHelper {
}
static function accessDaemon($token, $timeout = self::DAEMON_REQUEST_TIMEOUT) {
$data = serialize(array('token' => $token));
$url = '/?mailpoet&endpoint=queue&action=run&data=' .
base64_encode($data);
$data = array('token' => $token);
$url = API::buildRequest(
QueueAPI::ENDPOINT,
QueueAPI::ACTION_RUN,
$data
);
$args = array(
'timeout' => $timeout,
'user-agent' => 'MailPoet (www.mailpoet.com) Cron'
);
$result = wp_remote_get(
self::getSiteUrl() . $url,
$args
);
$result = wp_remote_get($url, $args);
return wp_remote_retrieve_body($result);
}
@ -72,7 +73,7 @@ class CronHelper {
static function checkExecutionTimer($timer) {
$elapsed_time = microtime(true) - $timer;
if($elapsed_time >= self::DAEMON_EXECUTION_LIMIT) {
throw new \Exception(__('Maximum execution time reached.'));
throw new \Exception(__('Maximum execution time has been reached.'));
}
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace MailPoet\Cron;
use MailPoet\Cron\Workers\Scheduler;
use MailPoet\Cron\Workers\SendingQueue;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -20,7 +20,7 @@ class Daemon {
private $timer;
function __construct($data) {
if(empty($data)) $this->abortWithError(__('Invalid or missing cron data.'));
if(empty($data)) $this->abortWithError(__('Invalid or missing Cron data.'));
ignore_user_abort();
$this->daemon = CronHelper::getDaemon();
$this->token = CronHelper::createToken();
@ -30,7 +30,6 @@ class Daemon {
function run() {
$daemon = $this->daemon;
set_time_limit(0);
if(!$daemon) {
$this->abortWithError(__('Daemon does not exist.'));
}
@ -39,41 +38,43 @@ class Daemon {
) {
$this->abortWithError(__('Invalid or missing token.'));
}
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon);
$this->abortIfStopped($daemon);
try {
$scheduler = new Scheduler();
$scheduler->process($this->timer);
$queue = new SendingQueue();
$queue->process($this->timer);
$scheduler = new SchedulerWorker($this->timer);
$scheduler->process();
$queue = new SendingQueueWorker($this->timer);
$queue->process();
} catch(\Exception $e) {
// continue processing, no need to catch errors
// continue processing, no need to handle errors
}
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time < CronHelper::DAEMON_EXECUTION_LIMIT) {
sleep(CronHelper::DAEMON_EXECUTION_LIMIT - $elapsed_time);
}
// after each execution, re-read daemon data in case it was deleted or
// after each execution, re-read daemon data in case its status was changed
// its status has changed
$daemon = CronHelper::getDaemon();
if(!$daemon || $daemon['token'] !== $this->data['token']) {
exit;
if(!$daemon || $daemon['token'] !== $this->token) {
$this->terminateRequest();
}
$daemon['counter']++;
$this->abortIfStopped($daemon);
if($daemon['status'] === self::STATUS_STARTING) {
$daemon['status'] = self::STATUS_STARTED;
}
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon);
$this->callSelf();
}
function abortIfStopped($daemon) {
if($daemon['status'] === self::STATUS_STOPPED) exit;
if($daemon['status'] === self::STATUS_STOPPED) {
$this->terminateRequest();
}
if($daemon['status'] === self::STATUS_STOPPING) {
$daemon['status'] = self::STATUS_STOPPED;
CronHelper::saveDaemon($daemon);
exit;
$this->terminateRequest();
}
}
@ -83,6 +84,10 @@ class Daemon {
function callSelf() {
CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT);
$this->terminateRequest();
}
function terminateRequest() {
exit;
}
}

View File

@ -26,24 +26,22 @@ class Supervisor {
$daemon['status'] === Daemon::STATUS_STOPPED
) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
$elapsed_time = time() - (int)$daemon['updated_at'];
// if it's been less than 40 seconds since last execution and we're not
// force-running the daemon, return its status and do nothing
if($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT && !$this->force_run) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
// if it's been less than 40 seconds since last execution, we are
// force-running the daemon and it's either being started or stopped,
// return its status and do nothing
elseif($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT &&
} elseif($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT &&
$this->force_run &&
in_array($daemon['status'], array(
Daemon::STATUS_STOPPING,
Daemon::STATUS_STARTING
))
) {
// if it's been less than 40 seconds since last execution, we are
// force-running the daemon and it's either being started or stopped,
// return its status and do nothing
return $this->formatDaemonStatusMessage($daemon['status']);
}
// re-create (restart) daemon

View File

@ -150,7 +150,7 @@ class Scheduler {
function verifyWPSubscriber($subscriber_id, $newsletter, $queue) {
// check if user has the proper role
$subscriber = Subscriber::findOne($subscriber_id);
if(!$subscriber || $subscriber->wp_user_id === null) {
if(!$subscriber || $subscriber->isWPUser() === false) {
$queue->delete();
return false;
}
@ -172,6 +172,5 @@ class Scheduler {
$queue->scheduled_at = $next_run_date;
$queue->save();
}
return;
}
}

View File

@ -1,398 +0,0 @@
<?php
namespace MailPoet\Cron\Workers;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterPost;
use MailPoet\Models\Setting;
use MailPoet\Models\StatisticsNewsletters;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class SendingQueue {
public $mta_config;
public $mta_log;
public $processing_method;
private $timer;
const BATCH_SIZE = 50;
const DIVIDER = '***MailPoet***';
const STATUS_COMPLETED = 'completed';
function __construct($timer = false) {
$this->mta_config = $this->getMailerConfig();
$this->mta_log = $this->getMailerLog();
$this->processing_method = ($this->mta_config['method'] === 'MailPoet') ?
'processBulkSubscribers' :
'processIndividualSubscriber';
$this->timer = ($timer) ? $timer : microtime(true);
CronHelper::checkExecutionTimer($this->timer);
}
function process() {
foreach($this->getQueues() as $queue) {
$newsletter = Newsletter::findOne($queue->newsletter_id);
if(!$newsletter) {
$queue->delete();
continue;
}
$newsletter = $newsletter->asArray();
$newsletter['body'] = $this->getOrRenderNewsletterBody($queue, $newsletter);
if($newsletter['type'] === 'notification' &&
strpos($newsletter['body']['html'], 'data-post-id') === false
){
$queue->delete();
continue;
}
$queue->subscribers = (object) unserialize($queue->subscribers);
if(!isset($queue->subscribers->processed)) {
$queue->subscribers->processed = array();
}
if(!isset($queue->subscribers->failed)) {
$queue->subscribers->failed = array();
}
$mailer = $this->configureMailer($newsletter);
foreach(array_chunk($queue->subscribers->to_process, self::BATCH_SIZE) as
$subscribers_ids) {
$subscribers = Subscriber::whereIn('id', $subscribers_ids)
->findArray();
if(count($subscribers_ids) !== count($subscribers)) {
$queue->subscribers->to_process = $this->recalculateSubscriberCount(
Helpers::arrayColumn($subscribers, 'id'),
$subscribers_ids,
$queue->subscribers->to_process
);
}
if(!count($queue->subscribers->to_process)) {
$this->updateQueue($queue);
continue;
}
$queue->subscribers = call_user_func_array(
array(
$this,
$this->processing_method
),
array(
$mailer,
$newsletter,
$subscribers,
$queue
)
);
}
}
}
function getOrRenderNewsletterBody($queue, $newsletter) {
// check if newsletter has been rendered, in which case return its contents
// or render and save for future reuse
if($queue->newsletter_rendered_body === null) {
if((boolean) Setting::getValue('tracking.enabled')) {
// insert tracking code
add_filter('mailpoet_rendering_post_process', function($template) {
return OpenTracking::process($template);
});
// render newsletter
$rendered_newsletter = $this->renderNewsletter($newsletter);
// process link shortcodes, extract and save links in the database
$processed_newsletter = $this->processLinksAndShortcodes(
$this->joinObject($rendered_newsletter),
$newsletter['id'],
$queue->id
);
list($newsletter['body']['html'], $newsletter['body']['text']) =
$this->splitObject($processed_newsletter);
}
else {
// render newsletter
$newsletter['body'] = $this->renderNewsletter($newsletter);
}
$this->extractAndSaveNewsletterPosts(
$newsletter['id'],
$newsletter['body']['html']
);
$queue->newsletter_rendered_body = json_encode($newsletter['body']);
$queue->save();
} else {
$newsletter['body'] = json_decode($queue->newsletter_rendered_body);
}
return (array) $newsletter['body'];
}
function processBulkSubscribers($mailer, $newsletter, $subscribers, $queue) {
foreach($subscribers as $subscriber) {
$processed_newsletters[] =
$this->processNewsletterBeforeSending($newsletter, $subscriber, $queue);
if(!$queue->newsletter_rendered_subject) {
$queue->newsletter_rendered_subject = $processed_newsletters[0]['subject'];
}
$transformed_subscribers[] =
$mailer->transformSubscriber($subscriber);
}
$result = $this->sendNewsletter(
$mailer,
$processed_newsletters,
$transformed_subscribers
);
$subscribers_ids = Helpers::arrayColumn($subscribers, 'id');
if(!$result) {
$queue->subscribers->failed = array_merge(
$queue->subscribers->failed,
$subscribers_ids
);
} else {
$newsletter_statistics =
array_map(function($data) use ($newsletter, $subscribers_ids, $queue) {
return array(
$newsletter['id'],
$subscribers_ids[$data],
$queue->id
);
}, range(0, count($transformed_subscribers) - 1));
$newsletter_statistics = Helpers::flattenArray($newsletter_statistics);
$this->updateMailerLog();
$this->updateNewsletterStatistics($newsletter_statistics);
$queue->subscribers->processed = array_merge(
$queue->subscribers->processed,
$subscribers_ids
);
}
$this->updateQueue($queue);
$this->checkSendingLimit();
CronHelper::checkExecutionTimer($this->timer);
return $queue->subscribers;
}
function processIndividualSubscriber($mailer, $newsletter, $subscribers, $queue) {
foreach($subscribers as $subscriber) {
$this->checkSendingLimit();
$processed_newsletter = $this->processNewsletterBeforeSending($newsletter, $subscriber, $queue);
if(!$queue->newsletter_rendered_subject) {
$queue->newsletter_rendered_subject = $processed_newsletter['subject'];
}
$transformed_subscriber = $mailer->transformSubscriber($subscriber);
$result = $this->sendNewsletter(
$mailer,
$processed_newsletter,
$transformed_subscriber
);
if(!$result) {
$queue->subscribers->failed[] = $subscriber['id'];
} else {
$queue->subscribers->processed[] = $subscriber['id'];
$newsletter_statistics = array(
$newsletter['id'],
$subscriber['id'],
$queue->id
);
$this->updateMailerLog();
$this->updateNewsletterStatistics($newsletter_statistics);
}
$this->updateQueue($queue);
CronHelper::checkExecutionTimer($this->timer);
}
return $queue->subscribers;
}
function updateNewsletterStatistics($data) {
return StatisticsNewsletters::createMultiple($data);
}
function renderNewsletter($newsletter) {
$renderer = new Renderer($newsletter);
return $renderer->render();
}
function processLinksAndShortcodes($content, $newsletter_id, $queue_id) {
// process only link shortcodes
$shortcodes = new Shortcodes($newsletter = false, $subscriber = false, $queue_id);
$content = $shortcodes->replace(
$content,
$categories = array('link')
);
// extract and save links and link shortcodes
list($content, $processed_links) =
Links::process(
$content,
$links = false,
$process_link_shortcodes = true,
$queue_id
);
Links::save($processed_links, $newsletter_id, $queue_id);
return $content;
}
function processNewsletterBeforeSending($newsletter, $subscriber = false, $queue) {
$data_for_shortcodes = array(
$newsletter['subject'],
$newsletter['body']['html'],
$newsletter['body']['text']
);
$processed_newsletter = $this->replaceShortcodes(
$newsletter,
$subscriber,
$queue,
$this->joinObject($data_for_shortcodes)
);
if((boolean) Setting::getValue('tracking.enabled')) {
$processed_newsletter = Links::replaceSubscriberData(
$newsletter['id'],
$subscriber['id'],
$queue->id,
$processed_newsletter
);
}
list($newsletter['subject'],
$newsletter['body']['html'],
$newsletter['body']['text']
) = $this->splitObject($processed_newsletter);
return $newsletter;
}
function replaceShortcodes($newsletter, $subscriber, $queue, $body) {
$shortcodes = new Shortcodes(
$newsletter,
$subscriber,
$queue
);
return $shortcodes->replace($body);
}
function sendNewsletter($mailer, $newsletter, $subscriber) {
return $mailer->mailer_instance->send(
$newsletter,
$subscriber
);
}
function configureMailer($newsletter) {
$sender['address'] = (!empty($newsletter['sender_address'])) ?
$newsletter['sender_address'] :
false;
$sender['name'] = (!empty($newsletter['sender_name'])) ?
$newsletter['sender_name'] :
false;
$reply_to['address'] = (!empty($newsletter['reply_to_address'])) ?
$newsletter['reply_to_address'] :
false;
$reply_to['name'] = (!empty($newsletter['reply_to_name'])) ?
$newsletter['reply_to_name'] :
false;
if(!$sender['address']) {
$sender = false;
}
if(!$reply_to['address']) {
$reply_to = false;
}
$mailer = new Mailer($method = false, $sender, $reply_to);
return $mailer;
}
function getQueues() {
return \MailPoet\Models\SendingQueue::orderByDesc('priority')
->whereNull('deleted_at')
->whereNull('status')
->findResultSet();
}
function updateQueue($queue) {
$queue = clone($queue);
$queue->subscribers->to_process = array_diff(
$queue->subscribers->to_process,
array_merge(
$queue->subscribers->processed,
$queue->subscribers->failed
)
);
$queue->subscribers->to_process = array_values(
$queue->subscribers->to_process
);
$queue->count_processed =
count($queue->subscribers->processed) + count($queue->subscribers->failed);
$queue->count_to_process = count($queue->subscribers->to_process);
$queue->count_failed = count($queue->subscribers->failed);
$queue->count_total =
$queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) {
$queue->processed_at = current_time('mysql');
$queue->status = self::STATUS_COMPLETED;
}
$queue->subscribers = serialize((array) $queue->subscribers);
$queue->save();
}
function updateMailerLog() {
$this->mta_log['sent']++;
return Setting::setValue('mta_log', $this->mta_log);
}
function getMailerConfig() {
$mta_config = Setting::getValue('mta');
if(!$mta_config) {
throw new \Exception(__('Mailer is not configured.'));
}
return $mta_config;
}
function getMailerLog() {
$mta_log = Setting::getValue('mta_log');
if(!$mta_log) {
$mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $mta_log);
}
return $mta_log;
}
function checkSendingLimit() {
$frequency_interval = (int)$this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int)$this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int)$this->mta_log['started'];
if($this->mta_log['sent'] === $frequency_limit &&
$elapsed_time <= $frequency_interval
) {
throw new \Exception(__('Sending frequency limit reached.'));
}
if($elapsed_time > $frequency_interval) {
$this->mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $this->mta_log);
}
return;
}
function recalculateSubscriberCount(
$found_subscriber, $existing_subscribers, $subscribers_to_process) {
$subscibers_to_exclude = array_diff($existing_subscribers, $found_subscriber);
return array_diff($subscribers_to_process, $subscibers_to_exclude);
}
function extractAndSaveNewsletterPosts($newletter_id, $content) {
preg_match_all('/data-post-id="(\d+)"/ism', $content, $posts);
$posts = $posts[1];
foreach($posts as $post) {
$newletter_post = NewsletterPost::create();
$newletter_post->newsletter_id = $newletter_id;
$newletter_post->post_id = $post;
$newletter_post->save();
}
}
private function joinObject($object = array()) {
return implode(self::DIVIDER, $object);
}
private function splitObject($object = array()) {
return explode(self::DIVIDER, $object);
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Subscribers as SubscribersTask;
use MailPoet\Models\Newsletter as NewsletterModel;
use MailPoet\Models\SendingQueue as SendingQueueModel;
use MailPoet\Models\StatisticsNewsletters as StatisticsNewslettersModel;
use MailPoet\Models\Subscriber as SubscriberModel;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class SendingQueue {
public $mailer_task;
public $newsletter_task;
private $timer;
const BATCH_SIZE = 50;
function __construct($timer = false) {
$this->mailer_task = new MailerTask();
$this->newsletter_task = new NewsletterTask();
$this->timer = ($timer) ? $timer : microtime(true);
}
function process() {
$this->mailer_task->checkSendingLimit();
foreach($this->getQueues() as $queue) {
// get and pre-process newsletter (render, replace shortcodes/links, etc.)
$newsletter = $this->newsletter_task->getAndPreProcess($queue->asArray());
if(!$newsletter) {
$queue->delete();
continue;
}
// configure mailer
$this->mailer_task->configureMailer($newsletter);
if(is_null($queue->newsletter_rendered_body)) {
$queue->newsletter_rendered_body = json_encode($newsletter['rendered_body']);
$queue->save();
}
// get subscribers
$queue->subscribers = $queue->getSubscribers();
$subscriber_batches = array_chunk(
$queue->subscribers['to_process'],
self::BATCH_SIZE
);
foreach($subscriber_batches as $subscribers_to_process_ids) {
$found_subscribers = SubscriberModel::whereIn('id', $subscribers_to_process_ids)
->findArray();
$found_subscribers_ids = Helpers::arrayColumn($found_subscribers, 'id');
// if some subscribers weren't found, remove them from the processing list
if(count($found_subscribers_ids) !== count($subscribers_to_process_ids)) {
$queue->subscribers = SubscribersTask::updateToProcessList(
$found_subscribers_ids,
$subscribers_to_process_ids,
$queue->subscribers
);
}
if(!count($queue->subscribers['to_process'])) {
$this->updateQueue($queue);
continue;
}
$queue = $this->processQueue(
$queue,
$newsletter,
$found_subscribers
);
}
}
}
function processQueue($queue, $newsletter, $subscribers) {
// determine if processing is done in bulk or individually
$processing_method = $this->mailer_task->getProcessingMethod();
$prepared_newsletters = array();
$prepared_subscribers = array();
$prepared_subscribers_ids = array();
$statistics = array();
foreach($subscribers as $subscriber) {
// render shortcodes and replace subscriber data in tracked links
$prepared_newsletters[] =
$this->newsletter_task->prepareNewsletterForSending(
$newsletter,
$subscriber,
$queue->asArray()
);
if(!$queue->newsletter_rendered_subject) {
$queue->newsletter_rendered_subject = $prepared_newsletters[0]['subject'];
}
// format subscriber name/address according to mailer settings
$prepared_subscribers[] = $this->mailer_task->prepareSubscriberForSending(
$subscriber
);
$prepared_subscribers_ids[] = $subscriber['id'];
// keep track of values for statistics purposes
$statistics[] = array(
'newsletter_id' => $newsletter['id'],
'subscriber_id' => $subscriber['id'],
'queue_id' => $queue->id
);
if($processing_method === 'individual') {
$queue = $this->sendNewsletters(
$queue,
$prepared_subscribers_ids,
$prepared_newsletters[0],
$prepared_subscribers[0],
$statistics
);
$prepared_newsletters = array();
$prepared_subscribers = array();
$prepared_subscribers_ids = array();
$statistics = array();
}
}
if($processing_method === 'bulk') {
$queue = $this->sendNewsletters(
$queue,
$prepared_subscribers_ids,
$prepared_newsletters,
$prepared_subscribers,
$statistics
);
}
return $queue;
}
function sendNewsletters(
$queue, $prepared_subscribers_ids, $prepared_newsletters,
$prepared_subscribers, $statistics
) {
// send newsletter
$send_result = $this->mailer_task->send(
$prepared_newsletters,
$prepared_subscribers
);
if(!$send_result) {
// update failed/to process list
$queue->subscribers = SubscribersTask::updateFailedList(
$prepared_subscribers_ids,
$queue->subscribers
);
} else {
// update processed/to process list
$queue->subscribers = SubscribersTask::updateProcessedList(
$prepared_subscribers_ids,
$queue->subscribers
);
// log statistics
StatisticsNewslettersModel::createMultiple($statistics);
// keep track of sent items
$this->mailer_task->updateMailerLog();
$subscribers_to_process_count = count($queue->subscribers['to_process']);
}
$queue = $this->updateQueue($queue);
if($subscribers_to_process_count) {
$this->mailer_task->checkSendingLimit();
}
CronHelper::checkExecutionTimer($this->timer);
return $queue;
}
function getQueues() {
return SendingQueueModel::orderByDesc('priority')
->whereNull('deleted_at')
->whereNull('status')
->findMany();
}
function updateQueue($queue) {
$queue->count_processed =
count($queue->subscribers['processed']) + count($queue->subscribers['failed']);
$queue->count_to_process = count($queue->subscribers['to_process']);
$queue->count_failed = count($queue->subscribers['failed']);
$queue->count_total =
$queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) {
$queue->processed_at = current_time('mysql');
$queue->status = SendingQueueModel::STATUS_COMPLETED;
// set newsletter status to sent
$newsletter = NewsletterModel::findOne($queue->newsletter_id);
// if it's a standard newsletter, update its status
if($newsletter->type === NewsletterModel::TYPE_STANDARD) {
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
}
}
return $queue->save();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class Links {
static function process(array $newsletter, array $queue) {
list($newsletter, $links) = self::hashAndReplaceLinks($newsletter, $queue);
self::saveLinks($links, $newsletter, $queue);
return $newsletter;
}
static function hashAndReplaceLinks(array $newsletter, array $queue) {
// join HTML and TEXT rendered body into a text string
$content = Helpers::joinObject($newsletter['rendered_body']);
list($content, $links) = NewsletterLinks::process($content);
// split the processed body with hashed links back to HTML and TEXT
list($newsletter['rendered_body']['html'], $newsletter['rendered_body']['text'])
= Helpers::splitObject($content);
return array(
$newsletter,
$links
);
}
static function saveLinks($links, $newsletter, $queue) {
return NewsletterLinks::save($links, $newsletter['id'], $queue['id']);
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Mailer\Mailer as MailerFactory;
use MailPoet\Models\Setting;
if(!defined('ABSPATH')) exit;
class Mailer {
public $mta_config;
public $mta_log;
public $mailer;
function __construct() {
$this->mta_config = $this->getMailerConfig();
$this->mta_log = $this->getMailerLog();
$this->mailer = $this->configureMailer();
}
function configureMailer(array $newsletter = null) {
$sender['address'] = (!empty($newsletter['sender_address'])) ?
$newsletter['sender_address'] :
false;
$sender['name'] = (!empty($newsletter['sender_name'])) ?
$newsletter['sender_name'] :
false;
$reply_to['address'] = (!empty($newsletter['reply_to_address'])) ?
$newsletter['reply_to_address'] :
false;
$reply_to['name'] = (!empty($newsletter['reply_to_name'])) ?
$newsletter['reply_to_name'] :
false;
if(!$sender['address']) {
$sender = false;
}
if(!$reply_to['address']) {
$reply_to = false;
}
$this->mailer = new MailerFactory($method = false, $sender, $reply_to);
return $this->mailer;
}
function getMailerConfig() {
$mta_config = Setting::getValue('mta');
if(!$mta_config) {
throw new \Exception(__('Mailer is not configured.'));
}
return $mta_config;
}
function getMailerLog() {
$mta_log = Setting::getValue('mta_log');
if(!$mta_log) {
$mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $mta_log);
}
return $mta_log;
}
function updateMailerLog() {
$this->mta_log['sent']++;
Setting::setValue('mta_log', $this->mta_log);
}
function getProcessingMethod() {
return ($this->mta_config['method'] === 'MailPoet') ?
'bulk' :
'individual';
}
function prepareSubscriberForSending(array $subscriber) {
return $this->mailer->transformSubscriber($subscriber);
}
function send($prepared_newsletters, $prepared_subscribers) {
return $this->mailer->mailer_instance->send(
$prepared_newsletters,
$prepared_subscribers
);
}
function checkSendingLimit() {
if($this->mta_config['method'] === 'MailPoet') return;
$frequency_interval = (int)$this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int)$this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int)$this->mta_log['started'];
if($this->mta_log['sent'] === $frequency_limit &&
$elapsed_time <= $frequency_interval
) {
throw new \Exception(__('Sending frequency limit has been reached.'));
}
if($elapsed_time > $frequency_interval) {
$this->mta_log = array(
'sent' => 0,
'started' => time()
);
Setting::setValue('mta_log', $this->mta_log);
}
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Links as LinksTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Posts as PostsTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Shortcodes as ShortcodesTask;
use MailPoet\Models\Newsletter as NewsletterModel;
use MailPoet\Models\Setting;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class Newsletter {
public $tracking_enabled;
public $tracking_image_inserted;
function __construct() {
$this->tracking_enabled = (boolean)Setting::getValue('tracking.enabled');
$this->tracking_image_inserted = false;
}
function get($newsletter_id) {
$newsletter = NewsletterModel::findOne($newsletter_id);
return ($newsletter) ? $newsletter->asArray() : false;
}
function getAndPreProcess(array $queue) {
$newsletter = $this->get($queue['newsletter_id']);
if(!$newsletter) {
return false;
}
// if the newsletter was previously rendered, return it
// otherwise, process/render it
if(!is_null($queue['newsletter_rendered_body'])) {
$newsletter['rendered_body'] = json_decode($queue['newsletter_rendered_body'], true);
return $newsletter;
}
// if tracking is enabled, do additional processing
if($this->tracking_enabled) {
// hook once to the newsletter post-processing filter and add tracking image
if(!$this->tracking_image_inserted) {
$this->tracking_image_inserted = OpenTracking::addTrackingImage();
}
// render newsletter
$newsletter = $this->render($newsletter);
// hash and save all links
$newsletter = LinksTask::process($newsletter, $queue);
} else {
// render newsletter
$newsletter = $this->render($newsletter);
}
// check if this is a post notification and if it contains posts
$newsletter_contains_posts = strpos($newsletter['rendered_body']['html'], 'data-post-id');
if($newsletter['type'] === 'notification' && !$newsletter_contains_posts) {
return false;
}
// save all posts
$newsletter = PostsTask::extractAndSave($newsletter);
return $newsletter;
}
function render($newsletter) {
$renderer = new Renderer($newsletter);
$newsletter['rendered_body'] = $renderer->render();
return $newsletter;
}
function prepareNewsletterForSending(
array $newsletter, array $subscriber, array $queue
) {
// shortcodes and links will be replaced in the subject, html and text body
// to speed the processing, join content into a continuous string
$prepared_newsletter = Helpers::joinObject(
array(
$newsletter['subject'],
$newsletter['rendered_body']['html'],
$newsletter['rendered_body']['text']
)
);
$prepared_newsletter = ShortcodesTask::process(
$prepared_newsletter,
$newsletter,
$subscriber,
$queue
);
if($this->tracking_enabled) {
$prepared_newsletter = NewsletterLinks::replaceSubscriberData(
$newsletter['id'],
$subscriber['id'],
$queue['id'],
$prepared_newsletter
);
}
$prepared_newsletter = Helpers::splitObject($prepared_newsletter);
return array(
'subject' => $prepared_newsletter[0],
'body' => array(
'html' => $prepared_newsletter[1],
'text' => $prepared_newsletter[2]
)
);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Models\NewsletterPost;
if(!defined('ABSPATH')) exit;
class Posts {
static function extractAndSave(array $newsletter) {
if(empty($newsletter['rendered_body']['html']) || empty($newsletter['id'])) {
return;
}
preg_match_all(
'/data-post-id="(\d+)"/ism',
$newsletter['rendered_body']['html'],
$matched_posts_ids);
$matched_posts_ids = $matched_posts_ids[1];
if(!count($matched_posts_ids)) {
return $newsletter;
}
foreach($matched_posts_ids as $post_id) {
$newletter_post = NewsletterPost::create();
$newletter_post->newsletter_id = $newsletter['id'];
$newletter_post->post_id = $post_id;
$newletter_post->save();
}
}
}

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